mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-04-26 10:09:12 -04:00
367 lines
14 KiB
Dart
367 lines
14 KiB
Dart
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<bool> 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<Future<bool> Function(AccountSpec)>.has(
|
|
'onSubmit', onSubmit))
|
|
..add(ObjectFlagProperty<void Function(bool p1)?>.has(
|
|
'onModifiedState', onModifiedState))
|
|
..add(DiagnosticsProperty<AccountSpec>('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<EditProfileForm> {
|
|
final _formKey = GlobalKey<FormBuilderState>();
|
|
|
|
@override
|
|
void initState() {
|
|
_savedValue = widget.initialValue;
|
|
_currentValueName = widget.initialValue.name;
|
|
_currentValueAutoAway = widget.initialValue.autoAway;
|
|
|
|
super.initState();
|
|
}
|
|
|
|
FormBuilderDropdown<proto.Availability> _availabilityDropDown(
|
|
BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final scale = theme.extension<ScaleScheme>()!;
|
|
|
|
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<proto.Availability>(
|
|
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<proto.Availability>(
|
|
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<ScaleScheme>()!;
|
|
final scaleConfig = theme.extension<ScaleConfig>()!;
|
|
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<ConnectionStateCubit>()
|
|
.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;
|
|
}
|