ui cleanup

This commit is contained in:
Christien Rioux 2025-03-17 00:51:16 -04:00
parent d460a0388c
commit 77c68aa45f
57 changed files with 1158 additions and 914 deletions

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@ -10,17 +11,18 @@ import 'package:veilid_support/veilid_support.dart';
import '../../layout/default_app_bar.dart';
import '../../notifications/notifications.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart';
import 'edit_profile_form.dart';
const _kDoBackArrow = 'doBackArrow';
class EditAccountPage extends StatefulWidget {
const EditAccountPage(
{required this.superIdentityRecordKey,
required this.existingAccount,
required this.initialValue,
required this.accountRecord,
super.key});
@ -28,7 +30,7 @@ class EditAccountPage extends StatefulWidget {
State createState() => _EditAccountPageState();
final TypedKey superIdentityRecordKey;
final proto.Account existingAccount;
final AccountSpec initialValue;
final OwnedDHTRecordPointer accountRecord;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -36,8 +38,7 @@ class EditAccountPage extends StatefulWidget {
properties
..add(DiagnosticsProperty<TypedKey>(
'superIdentityRecordKey', superIdentityRecordKey))
..add(DiagnosticsProperty<proto.Account>(
'existingAccount', existingAccount))
..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue))
..add(DiagnosticsProperty<OwnedDHTRecordPointer>(
'accountRecord', accountRecord));
}
@ -49,36 +50,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
titleBarStyle: TitleBarStyle.normal,
orientationCapability: OrientationCapability.portraitOnly);
Widget _editAccountForm(BuildContext context,
{required Future<void> Function(AccountSpec) onUpdate}) =>
EditProfileForm(
EditProfileForm _editAccountForm(BuildContext context) => EditProfileForm(
header: translate('edit_account_page.header'),
instructions: translate('edit_account_page.instructions'),
submitText: translate('edit_account_page.update'),
submitText: translate('button.update'),
submitDisabledText: translate('button.waiting_for_network'),
onUpdate: onUpdate,
initialValueCallback: (key) => switch (key) {
EditProfileForm.formFieldName => widget.existingAccount.profile.name,
EditProfileForm.formFieldPronouns =>
widget.existingAccount.profile.pronouns,
EditProfileForm.formFieldAbout =>
widget.existingAccount.profile.about,
EditProfileForm.formFieldAvailability =>
widget.existingAccount.profile.availability,
EditProfileForm.formFieldFreeMessage =>
widget.existingAccount.freeMessage,
EditProfileForm.formFieldAwayMessage =>
widget.existingAccount.awayMessage,
EditProfileForm.formFieldBusyMessage =>
widget.existingAccount.busyMessage,
EditProfileForm.formFieldAvatar =>
widget.existingAccount.profile.avatar,
EditProfileForm.formFieldAutoAway =>
widget.existingAccount.autodetectAway,
EditProfileForm.formFieldAutoAwayTimeout =>
widget.existingAccount.autoAwayTimeoutMin.toString(),
String() => throw UnimplementedError(),
},
onSubmit: _onSubmit,
onModifiedState: _onModifiedState,
initialValue: widget.initialValue,
);
Future<void> _onRemoveAccount() async {
@ -88,8 +67,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(translate('edit_account_page.remove_account_confirm_message'))
.paddingLTRB(24, 24, 24, 0),
Text(translate('edit_account_page.confirm_are_you_sure'))
.paddingAll(8),
Text(translate('confirmation.are_you_sure')).paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton(
onPressed: () {
@ -156,8 +134,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
Text(translate(
'edit_account_page.destroy_account_confirm_message_details'))
.paddingLTRB(24, 24, 24, 0),
Text(translate('edit_account_page.confirm_are_you_sure'))
.paddingAll(8),
Text(translate('confirmation.are_you_sure')).paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton(
onPressed: () {
@ -214,26 +191,51 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
}
}
Future<void> _onUpdate(AccountSpec accountSpec) async {
// Look up account cubit for this specific account
final perAccountCollectionBlocMapCubit =
context.read<PerAccountCollectionBlocMapCubit>();
final accountRecordCubit = await perAccountCollectionBlocMapCubit.operate(
widget.superIdentityRecordKey,
closure: (c) async => c.accountRecordCubit);
if (accountRecordCubit == null) {
return;
}
// Update account profile DHT record
// This triggers ConversationCubits to update
accountRecordCubit.updateAccount(accountSpec, () async {
// Update local account profile
await AccountRepository.instance
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
void _onModifiedState(bool isModified) {
setState(() {
_isModified = isModified;
});
}
Future<bool> _onSubmit(AccountSpec accountSpec) async {
try {
setState(() {
_isInAsyncCall = true;
});
try {
// Look up account cubit for this specific account
final perAccountCollectionBlocMapCubit =
context.read<PerAccountCollectionBlocMapCubit>();
final accountRecordCubit = await perAccountCollectionBlocMapCubit
.operate(widget.superIdentityRecordKey,
closure: (c) async => c.accountRecordCubit);
if (accountRecordCubit == null) {
return false;
}
// Update account profile DHT record
// This triggers ConversationCubits to update
accountRecordCubit.updateAccount(accountSpec, () async {
// Update local account profile
await AccountRepository.instance
.updateLocalAccount(widget.superIdentityRecordKey, accountSpec);
});
return true;
} finally {
setState(() {
_isInAsyncCall = false;
});
}
} on Exception catch (e, st) {
if (mounted) {
await showErrorStacktraceModal(
context: context, error: e, stackTrace: st);
}
}
return false;
}
@override
Widget build(BuildContext context) {
final displayModalHUD = _isInAsyncCall;
@ -246,9 +248,23 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
? IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
)
singleFuture((this, _kDoBackArrow), () async {
if (_isModified) {
final ok = await showConfirmModal(
context: context,
title:
translate('confirmation.discard_changes'),
text: translate(
'confirmation.are_you_sure_discard'));
if (!ok) {
return;
}
}
if (context.mounted) {
Navigator.pop(context);
}
});
})
: null,
actions: [
const SignalStrengthMeterWidget(),
@ -261,10 +277,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
]),
body: SingleChildScrollView(
child: Column(children: [
_editAccountForm(
context,
onUpdate: _onUpdate,
).paddingLTRB(0, 0, 0, 32),
_editAccountForm(context).paddingLTRB(0, 0, 0, 32),
OptionBox(
instructions:
translate('edit_account_page.remove_account_description'),
@ -286,4 +299,5 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
////////////////////////////////////////////////////////////////////////////
bool _isInAsyncCall = false;
bool _isModified = false;
}

View file

@ -13,7 +13,7 @@ import '../../theme/theme.dart';
import '../../veilid_processor/veilid_processor.dart';
import '../models/models.dart';
const _kDoUpdateSubmit = 'doUpdateSubmit';
const _kDoSubmitEditProfile = 'doSubmitEditProfile';
class EditProfileForm extends StatefulWidget {
const EditProfileForm({
@ -21,9 +21,9 @@ class EditProfileForm extends StatefulWidget {
required this.instructions,
required this.submitText,
required this.submitDisabledText,
required this.initialValueCallback,
this.onUpdate,
this.onSubmit,
required this.initialValue,
required this.onSubmit,
this.onModifiedState,
super.key,
});
@ -32,11 +32,11 @@ class EditProfileForm extends StatefulWidget {
final String header;
final String instructions;
final Future<void> Function(AccountSpec)? onUpdate;
final Future<void> Function(AccountSpec)? onSubmit;
final Future<bool> Function(AccountSpec) onSubmit;
final void Function(bool)? onModifiedState;
final String submitText;
final String submitDisabledText;
final Object Function(String key) initialValueCallback;
final AccountSpec initialValue;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -44,14 +44,13 @@ class EditProfileForm extends StatefulWidget {
properties
..add(StringProperty('header', header))
..add(StringProperty('instructions', instructions))
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
'onUpdate', onUpdate))
..add(StringProperty('submitText', submitText))
..add(StringProperty('submitDisabledText', submitDisabledText))
..add(ObjectFlagProperty<Object Function(String key)?>.has(
'initialValueCallback', initialValueCallback))
..add(ObjectFlagProperty<Future<void> Function(AccountSpec)?>.has(
'onSubmit', onSubmit));
..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';
@ -71,8 +70,9 @@ class _EditProfileFormState extends State<EditProfileForm> {
@override
void initState() {
_autoAwayEnabled =
widget.initialValueCallback(EditProfileForm.formFieldAutoAway) as bool;
_savedValue = widget.initialValue;
_currentValueName = widget.initialValue.name;
_currentValueAutoAway = widget.initialValue.autoAway;
super.initState();
}
@ -82,13 +82,10 @@ class _EditProfileFormState extends State<EditProfileForm> {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final initialValueX =
widget.initialValueCallback(EditProfileForm.formFieldAvailability)
as proto.Availability;
final initialValue =
initialValueX == proto.Availability.AVAILABILITY_UNSPECIFIED
_savedValue.availability == proto.Availability.AVAILABILITY_UNSPECIFIED
? proto.Availability.AVAILABILITY_FREE
: initialValueX;
: _savedValue.availability;
final availabilities = [
proto.Availability.AVAILABILITY_FREE,
@ -109,7 +106,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
value: x,
child: Row(mainAxisSize: MainAxisSize.min, children: [
AvailabilityWidget.availabilityIcon(
x, scale.primaryScale.primaryText),
x, scale.primaryScale.appText),
Text(x == proto.Availability.AVAILABILITY_OFFLINE
? translate('availability.always_show_offline')
: AvailabilityWidget.availabilityName(x))
@ -138,6 +135,12 @@ class _EditProfileFormState extends State<EditProfileForm> {
.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!
@ -153,11 +156,21 @@ class _EditProfileFormState extends State<EditProfileForm> {
freeMessage: freeMessage,
awayMessage: awayMessage,
busyMessage: busyMessage,
avatar: null,
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,
) {
@ -176,24 +189,32 @@ class _EditProfileFormState extends State<EditProfileForm> {
return FormBuilder(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
onChanged: _onChanged,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AvatarWidget(
name: _formKey.currentState?.value[EditProfileForm.formFieldName]
as String? ??
'?',
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),
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: widget
.initialValueCallback(EditProfileForm.formFieldName) as String,
initialValue: _savedValue.name,
onChanged: (x) {
setState(() {
_currentValueName = x ?? '';
});
},
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_name'),
@ -204,23 +225,20 @@ class _EditProfileFormState extends State<EditProfileForm> {
FormBuilderValidators.required(),
]),
textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange),
),
FormBuilderTextField(
name: EditProfileForm.formFieldPronouns,
initialValue:
widget.initialValueCallback(EditProfileForm.formFieldPronouns)
as String,
initialValue: _savedValue.pronouns,
maxLength: 64,
decoration: InputDecoration(
floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_pronouns'),
hintText: translate('account.empty_pronouns')),
textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange),
),
FormBuilderTextField(
name: EditProfileForm.formFieldAbout,
initialValue: widget
.initialValueCallback(EditProfileForm.formFieldAbout) as String,
initialValue: _savedValue.about,
maxLength: 1024,
maxLines: 8,
minLines: 1,
@ -229,74 +247,69 @@ class _EditProfileFormState extends State<EditProfileForm> {
labelText: translate('account.form_about'),
hintText: translate('account.empty_about')),
textInputAction: TextInputAction.newline,
).onFocusChange(_onFocusChange),
_availabilityDropDown(context)
.paddingLTRB(0, 0, 0, 16)
.onFocusChange(_onFocusChange),
),
_availabilityDropDown(context).paddingLTRB(0, 0, 0, 16),
FormBuilderTextField(
name: EditProfileForm.formFieldFreeMessage,
initialValue: widget.initialValueCallback(
EditProfileForm.formFieldFreeMessage) as String,
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,
).onFocusChange(_onFocusChange),
),
FormBuilderTextField(
name: EditProfileForm.formFieldAwayMessage,
initialValue: widget.initialValueCallback(
EditProfileForm.formFieldAwayMessage) as String,
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,
).onFocusChange(_onFocusChange),
),
FormBuilderTextField(
name: EditProfileForm.formFieldBusyMessage,
initialValue: widget.initialValueCallback(
EditProfileForm.formFieldBusyMessage) as String,
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,
).onFocusChange(_onFocusChange),
),
FormBuilderCheckbox(
name: EditProfileForm.formFieldAutoAway,
initialValue:
widget.initialValueCallback(EditProfileForm.formFieldAutoAway)
as bool,
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(() {
_autoAwayEnabled = v ?? false;
_currentValueAutoAway = v ?? false;
});
},
).onFocusChange(_onFocusChange),
),
FormBuilderTextField(
name: EditProfileForm.formFieldAutoAwayTimeout,
enabled: _autoAwayEnabled,
initialValue: widget.initialValueCallback(
EditProfileForm.formFieldAutoAwayTimeout) as String,
enabled: _currentValueAutoAway,
initialValue: _savedValue.autoAwayTimeout.toString(),
decoration: InputDecoration(
labelText: translate('account.form_auto_away_timeout'),
),
validator: FormBuilderValidators.positiveNumber(),
textInputAction: TextInputAction.next,
).onFocusChange(_onFocusChange),
),
Row(children: [
const Spacer(),
Text(widget.instructions).toCenter().flexible(flex: 6),
const Spacer(),
]).paddingSymmetric(vertical: 4),
if (widget.onSubmit != null)
]).paddingSymmetric(vertical: 16),
Row(children: [
const Spacer(),
Builder(builder: (context) {
final networkReady = context
.watch<ConnectionStateCubit>()
@ -307,7 +320,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
false;
return ElevatedButton(
onPressed: networkReady ? _doSubmit : null,
onPressed: (networkReady && _isModified) ? _doSubmit : null,
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
Text(networkReady
@ -317,36 +330,24 @@ class _EditProfileFormState extends State<EditProfileForm> {
]),
);
}),
const Spacer()
])
],
),
);
}
void _onFocusChange(bool focused) {
if (!focused) {
_doUpdate();
}
}
void _doUpdate() {
final onUpdate = widget.onUpdate;
if (onUpdate != null) {
singleFuture((this, _kDoUpdateSubmit), () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final aus = _makeAccountSpec();
await onUpdate(aus);
}
});
}
}
void _doSubmit() {
final onSubmit = widget.onSubmit;
if (onSubmit != null) {
singleFuture((this, _kDoUpdateSubmit), () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
final aus = _makeAccountSpec();
await onSubmit(aus);
if (_formKey.currentState?.saveAndValidate() ?? false) {
singleFuture((this, _kDoSubmitEditProfile), () async {
final updatedAccountSpec = _makeAccountSpec();
final saved = await onSubmit(updatedAccountSpec);
if (saved) {
setState(() {
_savedValue = updatedAccountSpec;
});
_onChanged();
}
});
}
@ -358,5 +359,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
);
///////////////////////////////////////////////////////////////////////////
late bool _autoAwayEnabled;
late AccountSpec _savedValue;
late bool _currentValueAutoAway;
late String _currentValueName;
bool _isModified = false;
}

View file

@ -8,7 +8,6 @@ import 'package:go_router/go_router.dart';
import '../../layout/default_app_bar.dart';
import '../../notifications/cubits/notifications_cubit.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart';
@ -28,33 +27,6 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
titleBarStyle: TitleBarStyle.normal,
orientationCapability: OrientationCapability.portraitOnly);
Object _defaultAccountValues(String key) {
switch (key) {
case EditProfileForm.formFieldName:
return '';
case EditProfileForm.formFieldPronouns:
return '';
case EditProfileForm.formFieldAbout:
return '';
case EditProfileForm.formFieldAvailability:
return proto.Availability.AVAILABILITY_FREE;
case EditProfileForm.formFieldFreeMessage:
return '';
case EditProfileForm.formFieldAwayMessage:
return '';
case EditProfileForm.formFieldBusyMessage:
return '';
// case EditProfileForm.formFieldAvatar:
// return null;
case EditProfileForm.formFieldAutoAway:
return false;
case EditProfileForm.formFieldAutoAwayTimeout:
return '15';
default:
throw StateError('missing form element');
}
}
Widget _newAccountForm(
BuildContext context,
) =>
@ -63,10 +35,10 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
instructions: translate('new_account_page.instructions'),
submitText: translate('new_account_page.create'),
submitDisabledText: translate('button.waiting_for_network'),
initialValueCallback: _defaultAccountValues,
initialValue: const AccountSpec.empty(),
onSubmit: _onSubmit);
Future<void> _onSubmit(AccountSpec accountSpec) async {
Future<bool> _onSubmit(AccountSpec accountSpec) async {
// dismiss the keyboard by unfocusing the textfield
FocusScope.of(context).unfocus();
@ -88,13 +60,15 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
context.read<NotificationsCubit>().error(
text: translate('new_account_page.network_is_offline'),
title: translate('new_account_page.error'));
return;
return false;
}
final writableSuperIdentity = await AccountRepository.instance
.createWithNewSuperIdentity(accountSpec);
GoRouterHelper(context).pushReplacement('/new_account/recovery_key',
extra: [writableSuperIdentity, accountSpec.name]);
return true;
} finally {
if (mounted) {
setState(() {
@ -108,6 +82,7 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
context: context, error: e, stackTrace: st);
}
}
return false;
}
@override