import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../../veilid_processor/veilid_processor.dart'; import '../models/models.dart'; const _kDoSubmitEditProfile = 'doSubmitEditProfile'; class EditProfileForm extends StatefulWidget { const EditProfileForm({ required this.header, required this.instructions, required this.submitText, required this.submitDisabledText, required this.initialValue, required this.onSubmit, this.onModifiedState, super.key, }); @override State createState() => _EditProfileFormState(); final String header; final String instructions; final Future Function(AccountSpec) onSubmit; final void Function(bool)? onModifiedState; final String submitText; final String submitDisabledText; final AccountSpec initialValue; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(StringProperty('header', header)) ..add(StringProperty('instructions', instructions)) ..add(StringProperty('submitText', submitText)) ..add(StringProperty('submitDisabledText', submitDisabledText)) ..add(ObjectFlagProperty Function(AccountSpec)>.has( 'onSubmit', onSubmit)) ..add(ObjectFlagProperty.has( 'onModifiedState', onModifiedState)) ..add(DiagnosticsProperty('initialValue', initialValue)); } static const String formFieldName = 'name'; static const String formFieldPronouns = 'pronouns'; static const String formFieldAbout = 'about'; static const String formFieldAvailability = 'availability'; static const String formFieldFreeMessage = 'free_message'; static const String formFieldAwayMessage = 'away_message'; static const String formFieldBusyMessage = 'busy_message'; static const String formFieldAvatar = 'avatar'; static const String formFieldAutoAway = 'auto_away'; static const String formFieldAutoAwayTimeout = 'auto_away_timeout'; } class _EditProfileFormState extends State { final _formKey = GlobalKey(); @override void initState() { _savedValue = widget.initialValue; _currentValueName = widget.initialValue.name; _currentValueAutoAway = widget.initialValue.autoAway; super.initState(); } FormBuilderDropdown _availabilityDropDown( BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final initialValue = _savedValue.availability == proto.Availability.AVAILABILITY_UNSPECIFIED ? proto.Availability.AVAILABILITY_FREE : _savedValue.availability; final availabilities = [ proto.Availability.AVAILABILITY_FREE, proto.Availability.AVAILABILITY_AWAY, proto.Availability.AVAILABILITY_BUSY, proto.Availability.AVAILABILITY_OFFLINE, ]; return FormBuilderDropdown( name: EditProfileForm.formFieldAvailability, initialValue: initialValue, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_availability'), hintText: translate('account.empty_busy_message')), items: availabilities .map((x) => DropdownMenuItem( value: x, child: Row(mainAxisSize: MainAxisSize.min, children: [ AvailabilityWidget.availabilityIcon( x, scale.primaryScale.appText), Text(x == proto.Availability.AVAILABILITY_OFFLINE ? translate('availability.always_show_offline') : AvailabilityWidget.availabilityName(x)) .paddingLTRB(8, 0, 0, 0), ]))) .toList(), ); } AccountSpec _makeAccountSpec() { final name = _formKey .currentState!.fields[EditProfileForm.formFieldName]!.value as String; final pronouns = _formKey.currentState! .fields[EditProfileForm.formFieldPronouns]!.value as String; final about = _formKey .currentState!.fields[EditProfileForm.formFieldAbout]!.value as String; final availability = _formKey .currentState! .fields[EditProfileForm.formFieldAvailability]! .value as proto.Availability; final invisible = availability == proto.Availability.AVAILABILITY_OFFLINE; final freeMessage = _formKey.currentState! .fields[EditProfileForm.formFieldFreeMessage]!.value as String; final awayMessage = _formKey.currentState! .fields[EditProfileForm.formFieldAwayMessage]!.value as String; final busyMessage = _formKey.currentState! .fields[EditProfileForm.formFieldBusyMessage]!.value as String; const proto.DataReference? avatar = null; // final avatar = _formKey.currentState! // .fields[EditProfileForm.formFieldAvatar]!.value //as proto.DataReference?; final autoAway = _formKey .currentState!.fields[EditProfileForm.formFieldAutoAway]!.value as bool; final autoAwayTimeoutString = _formKey.currentState! .fields[EditProfileForm.formFieldAutoAwayTimeout]!.value as String; final autoAwayTimeout = int.parse(autoAwayTimeoutString); return AccountSpec( name: name, pronouns: pronouns, about: about, availability: availability, invisible: invisible, freeMessage: freeMessage, awayMessage: awayMessage, busyMessage: busyMessage, avatar: avatar, autoAway: autoAway, autoAwayTimeout: autoAwayTimeout); } // Check if everything is the same and update state void _onChanged() { final currentValue = _makeAccountSpec(); _isModified = currentValue != _savedValue; final onModifiedState = widget.onModifiedState; if (onModifiedState != null) { onModifiedState(_isModified); } } Widget _editProfileForm( BuildContext context, ) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; final textTheme = theme.textTheme; late final Color border; if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { border = scale.primaryScale.elementBackground; } else { border = scale.primaryScale.border; } return FormBuilder( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, onChanged: _onChanged, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row(children: [ const Spacer(), AvatarWidget( name: _currentValueName, size: 128, borderColor: border, foregroundColor: scale.primaryScale.primaryText, backgroundColor: scale.primaryScale.primary, scaleConfig: scaleConfig, textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64), ).paddingLTRB(0, 0, 0, 16), const Spacer() ]), FormBuilderTextField( autofocus: true, name: EditProfileForm.formFieldName, initialValue: _savedValue.name, onChanged: (x) { setState(() { _currentValueName = x ?? ''; }); }, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_name'), hintText: translate('account.empty_name')), maxLength: 64, // The validator receives the text that the user has entered. validator: FormBuilderValidators.compose([ FormBuilderValidators.required(), ]), textInputAction: TextInputAction.next, ), FormBuilderTextField( name: EditProfileForm.formFieldPronouns, initialValue: _savedValue.pronouns, maxLength: 64, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_pronouns'), hintText: translate('account.empty_pronouns')), textInputAction: TextInputAction.next, ), FormBuilderTextField( name: EditProfileForm.formFieldAbout, initialValue: _savedValue.about, maxLength: 1024, maxLines: 8, minLines: 1, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_about'), hintText: translate('account.empty_about')), textInputAction: TextInputAction.newline, ), _availabilityDropDown(context).paddingLTRB(0, 0, 0, 16), FormBuilderTextField( name: EditProfileForm.formFieldFreeMessage, initialValue: _savedValue.freeMessage, maxLength: 128, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_free_message'), hintText: translate('account.empty_free_message')), textInputAction: TextInputAction.next, ), FormBuilderTextField( name: EditProfileForm.formFieldAwayMessage, initialValue: _savedValue.awayMessage, maxLength: 128, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_away_message'), hintText: translate('account.empty_away_message')), textInputAction: TextInputAction.next, ), FormBuilderTextField( name: EditProfileForm.formFieldBusyMessage, initialValue: _savedValue.busyMessage, maxLength: 128, decoration: InputDecoration( floatingLabelBehavior: FloatingLabelBehavior.always, labelText: translate('account.form_busy_message'), hintText: translate('account.empty_busy_message')), textInputAction: TextInputAction.next, ), FormBuilderCheckbox( name: EditProfileForm.formFieldAutoAway, initialValue: _savedValue.autoAway, side: BorderSide(color: scale.primaryScale.border, width: 2), checkColor: scale.primaryScale.borderText, activeColor: scale.primaryScale.border, title: Text(translate('account.form_auto_away'), style: textTheme.labelMedium), onChanged: (v) { setState(() { _currentValueAutoAway = v ?? false; }); }, ), FormBuilderTextField( name: EditProfileForm.formFieldAutoAwayTimeout, enabled: _currentValueAutoAway, initialValue: _savedValue.autoAwayTimeout.toString(), decoration: InputDecoration( labelText: translate('account.form_auto_away_timeout'), ), validator: FormBuilderValidators.positiveNumber(), textInputAction: TextInputAction.next, ), Row(children: [ const Spacer(), Text(widget.instructions).toCenter().flexible(flex: 6), const Spacer(), ]).paddingSymmetric(vertical: 16), Row(children: [ const Spacer(), Builder(builder: (context) { final networkReady = context .watch() .state .asData ?.value .isPublicInternetReady ?? false; return ElevatedButton( onPressed: (networkReady && _isModified) ? _doSubmit : null, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), Text(networkReady ? widget.submitText : widget.submitDisabledText) .paddingLTRB(0, 0, 4, 0) ]), ); }), const Spacer() ]) ], ), ); } void _doSubmit() { final onSubmit = widget.onSubmit; if (_formKey.currentState?.saveAndValidate() ?? false) { singleFuture((this, _kDoSubmitEditProfile), () async { final updatedAccountSpec = _makeAccountSpec(); final saved = await onSubmit(updatedAccountSpec); if (saved) { setState(() { _savedValue = updatedAccountSpec; }); _onChanged(); } }); } } @override Widget build(BuildContext context) => _editProfileForm( context, ); /////////////////////////////////////////////////////////////////////////// late AccountSpec _savedValue; late bool _currentValueAutoAway; late String _currentValueName; bool _isModified = false; }