From 77c68aa45f4c94ce10383b2fdd34a3e81c6f35ac Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Mon, 17 Mar 2025 00:51:16 -0400 Subject: [PATCH] ui cleanup --- assets/i18n/en.json | 28 +- assets/images/grid.svg | 1 + .../cubits/account_record_cubit.dart | 46 +--- lib/account_manager/models/account_spec.dart | 102 +++++-- .../views/edit_account_page.dart | 134 +++++----- .../views/edit_profile_form.dart | 186 ++++++------- .../views/new_account_page.dart | 37 +-- lib/app.dart | 46 ++-- lib/chat/views/chat_component_widget.dart | 3 +- lib/chat/views/no_conversation_widget.dart | 6 +- .../chat_single_contact_item_widget.dart | 16 +- .../cubits/contact_invitation_list_cubit.dart | 4 +- .../cubits/invitation_generator_cubit.dart | 2 +- .../views/contact_invitation_display.dart | 45 +++- .../views/contact_invitation_item_widget.dart | 14 +- .../views/create_invitation_dialog.dart | 47 ++-- .../views/new_contact_bottom_sheet.dart | 71 ----- lib/contact_invitation/views/views.dart | 1 - lib/contacts/contacts.dart | 1 + lib/contacts/cubits/contact_list_cubit.dart | 17 +- lib/contacts/models/contact_spec.dart | 37 +++ lib/contacts/models/models.dart | 1 + lib/contacts/views/availability_widget.dart | 6 +- .../views/contact_details_widget.dart | 39 ++- lib/contacts/views/contact_item_widget.dart | 6 +- lib/contacts/views/contacts_browser.dart | 22 +- lib/contacts/views/contacts_dialog.dart | 126 +++++---- lib/contacts/views/edit_contact_form.dart | 252 +++++++++++------- lib/layout/home/drawer_menu/drawer_menu.dart | 118 +++----- .../home/drawer_menu/menu_item_widget.dart | 61 +++-- lib/layout/home/home_screen.dart | 5 +- .../views/notifications_preferences.dart | 6 + lib/proto/veilidchat.pb.dart | 15 ++ lib/proto/veilidchat.pbjson.dart | 4 +- lib/proto/veilidchat.proto | 2 + lib/router/cubits/router_cubit.dart | 5 +- lib/settings/settings_page.dart | 3 +- lib/theme/models/chat_theme.dart | 2 +- lib/theme/models/contrast_generator.dart | 94 +++---- lib/theme/models/radix_generator.dart | 68 +---- lib/theme/models/scale_theme/scale_color.dart | 7 + .../scale_custom_dropdown_theme.dart | 1 - .../scale_input_decorator_theme.dart | 117 ++++++-- .../models/scale_theme/scale_scheme.dart | 13 +- lib/theme/models/scale_theme/scale_theme.dart | 83 ++++++ .../models/scale_theme/scale_tile_theme.dart | 5 +- .../models/scale_theme/scale_toast_theme.dart | 16 +- lib/theme/views/avatar_widget.dart | 21 +- lib/theme/views/slider_tile.dart | 11 +- lib/theme/views/styled_alert.dart | 35 --- lib/theme/views/styled_dialog.dart | 2 +- lib/theme/views/widget_helpers.dart | 61 +++-- lib/veilid_processor/views/developer.dart | 4 +- pubspec.lock | 8 + pubspec.yaml | 9 +- build.bat => update_generated_files.bat | 0 build.sh => update_generated_files.sh | 0 57 files changed, 1158 insertions(+), 914 deletions(-) create mode 100644 assets/images/grid.svg delete mode 100644 lib/contact_invitation/views/new_contact_bottom_sheet.dart create mode 100644 lib/contacts/models/contact_spec.dart create mode 100644 lib/contacts/models/models.dart rename build.bat => update_generated_files.bat (100%) rename build.sh => update_generated_files.sh (100%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 4334a6b..ef4c44c 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -50,7 +50,6 @@ "edit_account_page": { "titlebar": "Edit Account", "header": "Account Profile", - "update": "Update", "instructions": "This information will be shared with the people you invite to connect with you on VeilidChat.", "error": "Account modification error", "name": "Name", @@ -64,7 +63,6 @@ "destroy_account_description": "Destroy account, removing it completely from all devices everywhere", "destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!", "destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n", - "confirm_are_you_sure": "Are you sure you want to do this?", "failed_to_remove_title": "Failed to remove account", "try_again_network": "Try again when you have a more stable network connection", "failed_to_destroy_title": "Failed to destroy account", @@ -84,6 +82,12 @@ "view": "View", "share": "Share" }, + "confirmation": { + "confirm": "Confirm", + "discard_changes": "Discard changes?", + "are_you_sure_discard": "Are you sure you want to discard your changes?", + "are_you_sure": "Are you sure you want to do this?" + }, "button": { "ok": "Ok", "cancel": "Cancel", @@ -95,10 +99,10 @@ "close": "Close", "yes": "Yes", "no": "No", + "update": "Update", "waiting_for_network": "Waiting For Network" }, "toast": { - "confirm": "Confirm", "error": "Error", "info": "Info" }, @@ -142,9 +146,7 @@ "form_nickname": "Nickname", "form_notes": "Notes", "form_fingerprint": "Fingerprint", - "form_show_availability": "Show availability", - "save": "Save", - "save_disabled": "Save" + "form_show_availability": "Show availability" }, "availability": { "unspecified": "Unspecified", @@ -172,18 +174,21 @@ "create_invitation_dialog": { "title": "Create Contact Invitation", "me": "me", - "fingerprint": "Fingerprint:", + "recipient_name": "Contact Name", + "recipient_hint": "Enter the recipient's name", + "recipient_helper": "Name of the person you are inviting to chat", + "message_hint": "Enter message for contact (optional)", + "message_label": "Message", + "message_helper": "Message to send with invitation", + "fingerprint": "Fingerprint", "connect_with_me": "Connect with {name} on VeilidChat!", - "enter_message_hint": "Enter message for contact (optional)", - "message_to_contact": "Message to send with invitation (not encrypted)", "generate": "Generate Invitation", - "message": "Message", "unlocked": "Unlocked", "pin": "PIN", "password": "Password", "protect_this_invitation": "Protect this invitation:", "note": "Note:", - "note_text": "Contact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.", + "note_text": "Do not post contact invitations publicly.\n\nContact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.", "pin_description": "Choose a PIN to protect the contact invite.\n\nThis level of security is appropriate only for casual connections in public environments for 'shoulder surfing' protection.", "password_description": "Choose a strong password to protect the contact invite.\n\nThis level of security is appropriate when you must be sure the contact invitation is only accepted by its intended recipient. Share this password over a different medium than the invite itself.", "pin_does_not_match": "PIN does not match", @@ -193,6 +198,7 @@ "invitation_copied": "Invitation Copied" }, "invitation_dialog": { + "to": "To", "message_from_contact": "Message from contact", "validating": "Validating...", "failed_to_accept": "Failed to accept contact invitation", diff --git a/assets/images/grid.svg b/assets/images/grid.svg new file mode 100644 index 0000000..f30d577 --- /dev/null +++ b/assets/images/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/account_manager/cubits/account_record_cubit.dart b/lib/account_manager/cubits/account_record_cubit.dart index 9a73246..16ab2e0 100644 --- a/lib/account_manager/cubits/account_record_cubit.dart +++ b/lib/account_manager/cubits/account_record_cubit.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:async_tools/async_tools.dart'; -import 'package:protobuf/protobuf.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; @@ -47,53 +46,30 @@ class AccountRecordCubit extends DefaultDHTRecordCubit { // Public Interface void updateAccount( - AccountSpec accountSpec, Future Function() onSuccess) { - _sspUpdate.updateState((accountSpec, onSuccess), (state) async { + AccountSpec accountSpec, Future Function() onChanged) { + _sspUpdate.updateState((accountSpec, onChanged), (state) async { await _updateAccountAsync(state.$1, state.$2); }); } Future _updateAccountAsync( - AccountSpec accountSpec, Future Function() onSuccess) async { - var changed = false; - + AccountSpec accountSpec, Future Function() onChanged) async { + var changed = true; await record?.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async { - changed = false; if (old == null) { return null; } - final newAccount = old.deepCopy() - ..profile.name = accountSpec.name - ..profile.pronouns = accountSpec.pronouns - ..profile.about = accountSpec.about - ..profile.availability = accountSpec.availability - ..profile.status = accountSpec.status - //..profile.avatar = - ..profile.timestamp = Veilid.instance.now().toInt64() - ..invisible = accountSpec.invisible - ..autodetectAway = accountSpec.autoAway - ..autoAwayTimeoutMin = accountSpec.autoAwayTimeout - ..freeMessage = accountSpec.freeMessage - ..awayMessage = accountSpec.awayMessage - ..busyMessage = accountSpec.busyMessage; + final oldAccountSpec = AccountSpec.fromProto(old); + changed = oldAccountSpec != accountSpec; + if (!changed) { + return null; + } - if (newAccount.profile != old.profile || - newAccount.invisible != old.invisible || - newAccount.autodetectAway != old.autodetectAway || - newAccount.autoAwayTimeoutMin != old.autoAwayTimeoutMin || - newAccount.freeMessage != old.freeMessage || - newAccount.busyMessage != old.busyMessage || - newAccount.awayMessage != old.awayMessage) { - changed = true; - } - if (changed) { - return newAccount; - } - return null; + return accountSpec.updateProto(old); }); if (changed) { - await onSuccess(); + await onChanged(); } } diff --git a/lib/account_manager/models/account_spec.dart b/lib/account_manager/models/account_spec.dart index 539b8d0..918e192 100644 --- a/lib/account_manager/models/account_spec.dart +++ b/lib/account_manager/models/account_spec.dart @@ -1,12 +1,16 @@ -import 'package:flutter/widgets.dart'; +import 'package:equatable/equatable.dart'; +import 'package:meta/meta.dart'; +import 'package:protobuf/protobuf.dart'; +import 'package:veilid_support/veilid_support.dart'; import '../../proto/proto.dart' as proto; /// Profile and Account configurable fields /// Some are publicly visible via the proto.Profile /// Some are privately held as proto.Account configurations -class AccountSpec { - AccountSpec( +@immutable +class AccountSpec extends Equatable { + const AccountSpec( {required this.name, required this.pronouns, required this.about, @@ -19,37 +23,99 @@ class AccountSpec { required this.autoAway, required this.autoAwayTimeout}); + const AccountSpec.empty() + : name = '', + pronouns = '', + about = '', + availability = proto.Availability.AVAILABILITY_FREE, + invisible = false, + freeMessage = '', + awayMessage = '', + busyMessage = '', + avatar = null, + autoAway = false, + autoAwayTimeout = 15; + + AccountSpec.fromProto(proto.Account p) + : name = p.profile.name, + pronouns = p.profile.pronouns, + about = p.profile.about, + availability = p.profile.availability, + invisible = p.invisible, + freeMessage = p.freeMessage, + awayMessage = p.awayMessage, + busyMessage = p.busyMessage, + avatar = p.profile.hasAvatar() ? p.profile.avatar : null, + autoAway = p.autodetectAway, + autoAwayTimeout = p.autoAwayTimeoutMin; + String get status { late final String status; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: status = awayMessage; - break; case proto.Availability.AVAILABILITY_BUSY: status = busyMessage; - break; case proto.Availability.AVAILABILITY_FREE: status = freeMessage; - break; case proto.Availability.AVAILABILITY_UNSPECIFIED: case proto.Availability.AVAILABILITY_OFFLINE: status = ''; - break; } return status; } + Future updateProto(proto.Account old) async { + final newProto = old.deepCopy() + ..profile.name = name + ..profile.pronouns = pronouns + ..profile.about = about + ..profile.availability = availability + ..profile.status = status + ..profile.timestamp = Veilid.instance.now().toInt64() + ..invisible = invisible + ..autodetectAway = autoAway + ..autoAwayTimeoutMin = autoAwayTimeout + ..freeMessage = freeMessage + ..awayMessage = awayMessage + ..busyMessage = busyMessage; + + final newAvatar = avatar; + if (newAvatar != null) { + newProto.profile.avatar = newAvatar; + } else { + newProto.profile.clearAvatar(); + } + + return newProto; + } + //////////////////////////////////////////////////////////////////////////// - String name; - String pronouns; - String about; - proto.Availability availability; - bool invisible; - String freeMessage; - String awayMessage; - String busyMessage; - ImageProvider? avatar; - bool autoAway; - int autoAwayTimeout; + final String name; + final String pronouns; + final String about; + final proto.Availability availability; + final bool invisible; + final String freeMessage; + final String awayMessage; + final String busyMessage; + final proto.DataReference? avatar; + final bool autoAway; + final int autoAwayTimeout; + + @override + List get props => [ + name, + pronouns, + about, + availability, + invisible, + freeMessage, + awayMessage, + busyMessage, + avatar, + autoAway, + autoAwayTimeout + ]; } diff --git a/lib/account_manager/views/edit_account_page.dart b/lib/account_manager/views/edit_account_page.dart index 918c4ca..81b67eb 100644 --- a/lib/account_manager/views/edit_account_page.dart +++ b/lib/account_manager/views/edit_account_page.dart @@ -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( 'superIdentityRecordKey', superIdentityRecordKey)) - ..add(DiagnosticsProperty( - 'existingAccount', existingAccount)) + ..add(DiagnosticsProperty('initialValue', initialValue)) ..add(DiagnosticsProperty( 'accountRecord', accountRecord)); } @@ -49,36 +50,14 @@ class _EditAccountPageState extends WindowSetupState { titleBarStyle: TitleBarStyle.normal, orientationCapability: OrientationCapability.portraitOnly); - Widget _editAccountForm(BuildContext context, - {required Future 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 _onRemoveAccount() async { @@ -88,8 +67,7 @@ class _EditAccountPageState extends WindowSetupState { 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 { 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 { } } - Future _onUpdate(AccountSpec accountSpec) async { - // Look up account cubit for this specific account - final perAccountCollectionBlocMapCubit = - context.read(); - 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 _onSubmit(AccountSpec accountSpec) async { + try { + setState(() { + _isInAsyncCall = true; + }); + try { + // Look up account cubit for this specific account + final perAccountCollectionBlocMapCubit = + context.read(); + 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 { ? 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 { ]), 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 { //////////////////////////////////////////////////////////////////////////// bool _isInAsyncCall = false; + bool _isModified = false; } diff --git a/lib/account_manager/views/edit_profile_form.dart b/lib/account_manager/views/edit_profile_form.dart index 1774bff..d6bb504 100644 --- a/lib/account_manager/views/edit_profile_form.dart +++ b/lib/account_manager/views/edit_profile_form.dart @@ -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 Function(AccountSpec)? onUpdate; - final Future Function(AccountSpec)? onSubmit; + final Future 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 Function(AccountSpec)?>.has( - 'onUpdate', onUpdate)) ..add(StringProperty('submitText', submitText)) ..add(StringProperty('submitDisabledText', submitDisabledText)) - ..add(ObjectFlagProperty.has( - 'initialValueCallback', initialValueCallback)) - ..add(ObjectFlagProperty Function(AccountSpec)?>.has( - 'onSubmit', onSubmit)); + ..add(ObjectFlagProperty Function(AccountSpec)>.has( + 'onSubmit', onSubmit)) + ..add(ObjectFlagProperty.has( + 'onModifiedState', onModifiedState)) + ..add(DiagnosticsProperty('initialValue', initialValue)); } static const String formFieldName = 'name'; @@ -71,8 +70,9 @@ class _EditProfileFormState extends State { @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 { final theme = Theme.of(context); final scale = theme.extension()!; - 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 { 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 { .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 { 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 { 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 { 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 { 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() @@ -307,7 +320,7 @@ class _EditProfileFormState extends State { 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 { ]), ); }), + 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 { ); /////////////////////////////////////////////////////////////////////////// - late bool _autoAwayEnabled; + late AccountSpec _savedValue; + late bool _currentValueAutoAway; + late String _currentValueName; + bool _isModified = false; } diff --git a/lib/account_manager/views/new_account_page.dart b/lib/account_manager/views/new_account_page.dart index ccd5b00..a739094 100644 --- a/lib/account_manager/views/new_account_page.dart +++ b/lib/account_manager/views/new_account_page.dart @@ -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 { 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 { 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 _onSubmit(AccountSpec accountSpec) async { + Future _onSubmit(AccountSpec accountSpec) async { // dismiss the keyboard by unfocusing the textfield FocusScope.of(context).unfocus(); @@ -88,13 +60,15 @@ class _NewAccountPageState extends WindowSetupState { context.read().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 { context: context, error: e, stackTrace: st); } } + return false; } @override diff --git a/lib/app.dart b/lib/app.dart index 7ef0911..fade2c8 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:provider/provider.dart'; @@ -163,23 +164,34 @@ class VeilidChatApp extends StatelessWidget { scale.primaryScale.subtleBackground, ]); - return DecoratedBox( - decoration: BoxDecoration(gradient: gradient), - child: MaterialApp.router( - scrollBehavior: const ScrollBehaviorModified(), - debugShowCheckedModeBanner: false, - routerConfig: context.read().router(), - title: translate('app.title'), - theme: theme, - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FormBuilderLocalizations.delegate, - localizationDelegate - ], - supportedLocales: localizationDelegate.supportedLocales, - locale: localizationDelegate.currentLocale, - )); + return Stack( + fit: StackFit.expand, + alignment: Alignment.center, + children: [ + DecoratedBox( + decoration: BoxDecoration(gradient: gradient)), + SvgPicture.asset( + 'assets/images/grid.svg', + fit: BoxFit.cover, + colorFilter: overlayFilter, + ), + MaterialApp.router( + scrollBehavior: const ScrollBehaviorModified(), + debugShowCheckedModeBanner: false, + routerConfig: context.read().router(), + title: translate('app.title'), + theme: theme, + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FormBuilderLocalizations.delegate, + localizationDelegate + ], + supportedLocales: + localizationDelegate.supportedLocales, + locale: localizationDelegate.currentLocale, + ) + ]); })), )), ); diff --git a/lib/chat/views/chat_component_widget.dart b/lib/chat/views/chat_component_widget.dart index c4816ae..79d1f1c 100644 --- a/lib/chat/views/chat_component_widget.dart +++ b/lib/chat/views/chat_component_widget.dart @@ -147,8 +147,7 @@ class ChatComponentWidget extends StatelessWidget { ]), ), DecoratedBox( - decoration: - BoxDecoration(color: scale.primaryScale.subtleBackground), + decoration: const BoxDecoration(color: Colors.transparent), child: NotificationListener( onNotification: (notification) { if (chatComponentCubit.scrollOffset != 0) { diff --git a/lib/chat/views/no_conversation_widget.dart b/lib/chat/views/no_conversation_widget.dart index e246fee..df1b6d3 100644 --- a/lib/chat/views/no_conversation_widget.dart +++ b/lib/chat/views/no_conversation_widget.dart @@ -16,7 +16,7 @@ class NoConversationWidget extends StatelessWidget { return DecoratedBox( decoration: BoxDecoration( - color: scale.primaryScale.appBackground, + color: scale.primaryScale.appBackground.withAlpha(192), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -24,14 +24,14 @@ class NoConversationWidget extends StatelessWidget { children: [ Icon( Icons.diversity_3, - color: scale.primaryScale.subtleBorder, + color: scale.primaryScale.appText.withAlpha(127), size: 48, ), Text( textAlign: TextAlign.center, translate('chat.start_a_conversation'), style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: scale.primaryScale.subtleBorder, + color: scale.primaryScale.appText.withAlpha(127), ), ), ], diff --git a/lib/chat_list/views/chat_single_contact_item_widget.dart b/lib/chat_list/views/chat_single_contact_item_widget.dart index 826fe37..3bdf645 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -44,20 +44,22 @@ class ChatSingleContactItemWidget extends StatelessWidget { : _contact.profile.availability; final scaleTileTheme = scaleTheme.tileTheme( - disabled: _disabled, - selected: selected, - scaleKind: ScaleKind.secondary); + disabled: _disabled, + selected: selected, + ); final avatar = AvatarWidget( name: name, size: 34, - borderColor: scaleTileTheme.borderColor, + borderColor: scaleTheme.config.useVisualIndicators + ? scaleTheme.scheme.primaryScale.primaryText + : scaleTheme.scheme.primaryScale.subtleBorder, foregroundColor: _disabled ? scaleTheme.scheme.grayScale.primaryText - : scaleTheme.scheme.secondaryScale.primaryText, + : scaleTheme.scheme.primaryScale.primaryText, backgroundColor: _disabled ? scaleTheme.scheme.grayScale.primary - : scaleTheme.scheme.secondaryScale.primary, + : scaleTheme.scheme.primaryScale.primary, scaleConfig: scaleTheme.config, textStyle: theme.textTheme.titleLarge!, ); @@ -66,7 +68,7 @@ class ChatSingleContactItemWidget extends StatelessWidget { key: ValueKey(_localConversationRecordKey), disabled: _disabled, selected: selected, - tileScale: ScaleKind.secondary, + tileScale: ScaleKind.primary, title: title, subtitle: subtitle, leading: avatar, diff --git a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart index 959445c..768cf2f 100644 --- a/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart +++ b/lib/contact_invitation/cubits/contact_invitation_list_cubit.dart @@ -59,6 +59,7 @@ class ContactInvitationListCubit {required proto.Profile profile, required EncryptionKeyType encryptionKeyType, required String encryptionKey, + required String recipient, required String message, required Timestamp? expiration}) async { final pool = DHTRecordPool.instance; @@ -154,7 +155,8 @@ class ContactInvitationListCubit ..localConversationRecordKey = localConversation.key.toProto() ..expiration = expiration?.toInt64() ?? Int64.ZERO ..invitation = signedContactInvitationBytes - ..message = message; + ..message = message + ..recipient = recipient; // Add ContactInvitationRecord to account's list await operateWriteEventual((writer) async { diff --git a/lib/contact_invitation/cubits/invitation_generator_cubit.dart b/lib/contact_invitation/cubits/invitation_generator_cubit.dart index 5c0fa15..8d2226c 100644 --- a/lib/contact_invitation/cubits/invitation_generator_cubit.dart +++ b/lib/contact_invitation/cubits/invitation_generator_cubit.dart @@ -5,5 +5,5 @@ import 'package:veilid_support/veilid_support.dart'; class InvitationGeneratorCubit extends FutureCubit<(Uint8List, TypedKey)> { InvitationGeneratorCubit(super.fut); - InvitationGeneratorCubit.value(super.v) : super.value(); + InvitationGeneratorCubit.value(super.state) : super.value(); } diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 83b80d0..b3f048a 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:basic_utils/basic_utils.dart'; import 'package:flutter/foundation.dart'; @@ -20,11 +21,13 @@ import '../contact_invitation.dart'; class ContactInvitationDisplayDialog extends StatelessWidget { const ContactInvitationDisplayDialog._({ required this.locator, + required this.recipient, required this.message, required this.fingerprint, }); final Locator locator; + final String recipient; final String message; final String fingerprint; @@ -32,18 +35,22 @@ class ContactInvitationDisplayDialog extends StatelessWidget { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties + ..add(StringProperty('recipient', recipient)) ..add(StringProperty('message', message)) ..add(DiagnosticsProperty('locator', locator)) ..add(StringProperty('fingerprint', fingerprint)); } - String makeTextInvite(String message, Uint8List data) { + String makeTextInvite(String recipient, String message, Uint8List data) { final invite = StringUtils.addCharAtPosition( base64UrlNoPadEncode(data), '\n', 40, repeat: true); + final to = recipient.isNotEmpty + ? '${translate('invitiation_dialog.to')}: $recipient\n' + : ''; final msg = message.isNotEmpty ? '$message\n' : ''; - - return '$msg' + return '$to' + '$msg' '--- BEGIN VEILIDCHAT CONTACT INVITE ----\n' '$invite\n' '---- END VEILIDCHAT CONTACT INVITE -----\n' @@ -62,6 +69,10 @@ class ContactInvitationDisplayDialog extends StatelessWidget { final cardsize = min(MediaQuery.of(context).size.shortestSide - 48.0, 400); + final fingerprintText = + '${translate('create_invitation_dialog.fingerprint')}\n' + '$fingerprint'; + return BlocListener( bloc: locator(), @@ -110,14 +121,21 @@ class ContactInvitationDisplayDialog extends StatelessWidget { errorCorrectLevel: QrErrorCorrectLevel.L)), ).expanded(), - Text(message, - softWrap: true, - style: textTheme.labelLarge! - .copyWith(color: Colors.black)) - .paddingAll(8), - Text( - '${translate('create_invitation_dialog.fingerprint')}\n' - '$fingerprint', + if (recipient.isNotEmpty) + AutoSizeText(recipient, + softWrap: true, + maxLines: 2, + style: textTheme.labelLarge! + .copyWith(color: Colors.black)) + .paddingAll(8), + if (message.isNotEmpty) + Text(message, + softWrap: true, + maxLines: 2, + style: textTheme.labelMedium! + .copyWith(color: Colors.black)) + .paddingAll(8), + Text(fingerprintText, softWrap: true, textAlign: TextAlign.center, style: textTheme.labelSmall!.copyWith( @@ -137,7 +155,8 @@ class ContactInvitationDisplayDialog extends StatelessWidget { text: translate('create_invitation_dialog' '.invitation_copied')); await Clipboard.setData(ClipboardData( - text: makeTextInvite(message, data.$1))); + text: makeTextInvite( + recipient, message, data.$1))); }, ).paddingAll(16), ]), @@ -148,6 +167,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { required BuildContext context, required Locator locator, required InvitationGeneratorCubit Function(BuildContext) create, + required String recipient, required String message, }) async { final fingerprint = @@ -159,6 +179,7 @@ class ContactInvitationDisplayDialog extends StatelessWidget { create: create, child: ContactInvitationDisplayDialog._( locator: locator, + recipient: recipient, message: message, fingerprint: fingerprint, ))); diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index 6e6dfcf..779f962 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -37,14 +37,19 @@ class ContactInvitationItemWidget extends StatelessWidget { final tileDisabled = disabled || context.watch().isBusy; + var title = translate('contact_list.invitation'); + if (contactInvitationRecord.recipient.isNotEmpty) { + title = contactInvitationRecord.recipient; + } else if (contactInvitationRecord.message.isNotEmpty) { + title = contactInvitationRecord.message; + } + return SliderTile( key: ObjectKey(contactInvitationRecord), disabled: tileDisabled, selected: selected, tileScale: ScaleKind.primary, - title: contactInvitationRecord.message.isEmpty - ? translate('contact_list.invitation') - : contactInvitationRecord.message, + title: title, leading: const Icon(Icons.person_add), onTap: () async { if (!context.mounted) { @@ -53,6 +58,7 @@ class ContactInvitationItemWidget extends StatelessWidget { await ContactInvitationDisplayDialog.show( context: context, locator: context.read, + recipient: contactInvitationRecord.recipient, message: contactInvitationRecord.message, create: (context) => InvitationGeneratorCubit.value(( Uint8List.fromList(contactInvitationRecord.invitation), @@ -62,7 +68,7 @@ class ContactInvitationItemWidget extends StatelessWidget { }, endActions: [ SliderTileAction( - icon: Icons.delete, + // icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, onPressed: (context) async { diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index f1115d7..d835de8 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -18,7 +18,7 @@ class CreateInvitationDialog extends StatefulWidget { const CreateInvitationDialog._({required this.locator}); @override - CreateInvitationDialogState createState() => CreateInvitationDialogState(); + State createState() => _CreateInvitationDialogState(); static Future show(BuildContext context) async { await StyledDialog.show( @@ -36,8 +36,9 @@ class CreateInvitationDialog extends StatefulWidget { } } -class CreateInvitationDialogState extends State { +class _CreateInvitationDialogState extends State { late final TextEditingController _messageTextController; + late final TextEditingController _recipientTextController; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; String _encryptionKey = ''; @@ -51,6 +52,7 @@ class CreateInvitationDialogState extends State { _messageTextController = TextEditingController( text: translate('create_invitation_dialog.connect_with_me', args: {'name': name})); + _recipientTextController = TextEditingController(); super.initState(); } @@ -154,6 +156,7 @@ class CreateInvitationDialogState extends State { profile: profile, encryptionKeyType: _encryptionKeyType, encryptionKey: _encryptionKey, + recipient: _recipientTextController.text, message: _messageTextController.text, expiration: _expiration); @@ -162,6 +165,7 @@ class CreateInvitationDialogState extends State { await ContactInvitationDisplayDialog.show( context: context, locator: widget.locator, + recipient: _recipientTextController.text, message: _messageTextController.text, create: (context) => InvitationGeneratorCubit(generator)); } @@ -176,6 +180,7 @@ class CreateInvitationDialogState extends State { final theme = Theme.of(context); //final scale = theme.extension()!; final textTheme = theme.textTheme; + return ConstrainedBox( constraints: BoxConstraints(maxHeight: maxDialogHeight, maxWidth: maxDialogWidth), @@ -185,19 +190,34 @@ class CreateInvitationDialogState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text( - translate('create_invitation_dialog.message_to_contact'), + TextField( + controller: _recipientTextController, + onChanged: (value) { + setState(() {}); + }, + inputFormatters: [ + LengthLimitingTextInputFormatter(128), + ], + decoration: InputDecoration( + hintText: + translate('create_invitation_dialog.recipient_hint'), + labelText: + translate('create_invitation_dialog.recipient_name'), + helperText: + translate('create_invitation_dialog.recipient_helper')), ).paddingAll(8), + const SizedBox(height: 10), TextField( controller: _messageTextController, inputFormatters: [ LengthLimitingTextInputFormatter(128), ], decoration: InputDecoration( - //border: const OutlineInputBorder(), - hintText: - translate('create_invitation_dialog.enter_message_hint'), - labelText: translate('create_invitation_dialog.message')), + hintText: translate('create_invitation_dialog.message_hint'), + labelText: + translate('create_invitation_dialog.message_label'), + helperText: + translate('create_invitation_dialog.message_helper')), ).paddingAll(8), const SizedBox(height: 10), Text(translate('create_invitation_dialog.protect_this_invitation'), @@ -228,7 +248,9 @@ class CreateInvitationDialogState extends State { Container( padding: const EdgeInsets.all(8), child: ElevatedButton( - onPressed: _onGenerateButtonPressed, + onPressed: _recipientTextController.text.isNotEmpty + ? _onGenerateButtonPressed + : null, child: Text( translate('create_invitation_dialog.generate'), ).paddingAll(16), @@ -244,11 +266,4 @@ class CreateInvitationDialogState extends State { ), ); } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty( - 'messageTextController', _messageTextController)); - } } diff --git a/lib/contact_invitation/views/new_contact_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_bottom_sheet.dart deleted file mode 100644 index a79a07f..0000000 --- a/lib/contact_invitation/views/new_contact_bottom_sheet.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:awesome_extensions/awesome_extensions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_translate/flutter_translate.dart'; - -import '../../theme/theme.dart'; -import 'create_invitation_dialog.dart'; -import 'paste_invitation_dialog.dart'; -import 'scan_invitation_dialog.dart'; - -Widget newContactBottomSheetBuilder( - BuildContext sheetContext, BuildContext context) { - final theme = Theme.of(sheetContext); - final scale = theme.extension()!; - - return KeyboardListener( - focusNode: FocusNode(), - onKeyEvent: (ke) { - if (ke.logicalKey == LogicalKeyboardKey.escape) { - Navigator.pop(sheetContext); - } - }, - child: styledBottomSheet( - context: context, - title: translate('add_contact_sheet.new_contact'), - child: SizedBox( - height: 160, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(sheetContext); - await CreateInvitationDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.contact_page), - color: scale.primaryScale.hoverBorder), - Text( - translate('add_contact_sheet.create_invite'), - ) - ]), - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(sheetContext); - await ScanInvitationDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.qr_code_scanner), - color: scale.primaryScale.hoverBorder), - Text( - translate('add_contact_sheet.scan_invite'), - ) - ]), - Column(children: [ - IconButton( - onPressed: () async { - Navigator.pop(sheetContext); - await PasteInvitationDialog.show(context); - }, - iconSize: 64, - icon: const Icon(Icons.paste), - color: scale.primaryScale.hoverBorder), - Text( - translate('add_contact_sheet.paste_invite'), - ) - ]) - ]).paddingAll(16)))); -} diff --git a/lib/contact_invitation/views/views.dart b/lib/contact_invitation/views/views.dart index 726f0b9..241513d 100644 --- a/lib/contact_invitation/views/views.dart +++ b/lib/contact_invitation/views/views.dart @@ -3,6 +3,5 @@ export 'contact_invitation_item_widget.dart'; export 'contact_invitation_list_widget.dart'; export 'create_invitation_dialog.dart'; export 'invitation_dialog.dart'; -export 'new_contact_bottom_sheet.dart'; export 'paste_invitation_dialog.dart'; export 'scan_invitation_dialog.dart'; diff --git a/lib/contacts/contacts.dart b/lib/contacts/contacts.dart index 6acdd43..08ae2e7 100644 --- a/lib/contacts/contacts.dart +++ b/lib/contacts/contacts.dart @@ -1,2 +1,3 @@ export 'cubits/cubits.dart'; +export 'models/models.dart'; export 'views/views.dart'; diff --git a/lib/contacts/cubits/contact_list_cubit.dart b/lib/contacts/cubits/contact_list_cubit.dart index d3c6483..df70cc0 100644 --- a/lib/contacts/cubits/contact_list_cubit.dart +++ b/lib/contacts/cubits/contact_list_cubit.dart @@ -8,6 +8,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../proto/proto.dart' as proto; import '../../tools/tools.dart'; +import '../models/models.dart'; ////////////////////////////////////////////////// // Mutable state for per-account contacts @@ -81,9 +82,7 @@ class ContactListCubit extends DHTShortArrayCubit { Future updateContactFields({ required TypedKey localConversationRecordKey, - String? nickname, - String? notes, - bool? showAvailability, + required ContactSpec updatedContactSpec, }) async { // Update contact's locally-modifiable fields await operateWriteEventual((writer) async { @@ -92,17 +91,7 @@ class ContactListCubit extends DHTShortArrayCubit { if (c != null && c.localConversationRecordKey.toVeilid() == localConversationRecordKey) { - final newContact = c.deepCopy(); - - if (nickname != null) { - newContact.nickname = nickname; - } - if (notes != null) { - newContact.notes = notes; - } - if (showAvailability != null) { - newContact.showAvailability = showAvailability; - } + final newContact = await updatedContactSpec.updateProto(c); final updated = await writer.tryWriteItemProtobuf( proto.Contact.fromBuffer, pos, newContact); diff --git a/lib/contacts/models/contact_spec.dart b/lib/contacts/models/contact_spec.dart new file mode 100644 index 0000000..1596434 --- /dev/null +++ b/lib/contacts/models/contact_spec.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:protobuf/protobuf.dart'; + +import '../../proto/proto.dart' as proto; + +@immutable +class ContactSpec extends Equatable { + const ContactSpec({ + required this.nickname, + required this.notes, + required this.showAvailability, + }); + + ContactSpec.fromProto(proto.Contact p) + : nickname = p.nickname, + notes = p.notes, + showAvailability = p.showAvailability; + + Future updateProto(proto.Contact old) async { + final newProto = old.deepCopy() + ..nickname = nickname + ..notes = notes + ..showAvailability = showAvailability; + + return newProto; + } + + //////////////////////////////////////////////////////////////////////////// + + final String nickname; + final String notes; + final bool showAvailability; + + @override + List get props => [nickname, notes, showAvailability]; +} diff --git a/lib/contacts/models/models.dart b/lib/contacts/models/models.dart new file mode 100644 index 0000000..d489632 --- /dev/null +++ b/lib/contacts/models/models.dart @@ -0,0 +1 @@ +export 'contact_spec.dart'; diff --git a/lib/contacts/views/availability_widget.dart b/lib/contacts/views/availability_widget.dart index cf3e51a..a79f774 100644 --- a/lib/contacts/views/availability_widget.dart +++ b/lib/contacts/views/availability_widget.dart @@ -10,11 +10,11 @@ class AvailabilityWidget extends StatelessWidget { {required this.availability, required this.color, this.vertical = true, - this.iconSize = 32, + this.iconSize = 24, super.key}); static Widget availabilityIcon(proto.Availability availability, Color color, - {double size = 32}) { + {double size = 24}) { late final Widget iconData; switch (availability) { case proto.Availability.AVAILABILITY_AWAY: @@ -70,7 +70,7 @@ class AvailabilityWidget extends StatelessWidget { ]) : Row(mainAxisSize: MainAxisSize.min, children: [ icon, - Text(name, style: textTheme.labelSmall!.copyWith(color: color)) + Text(name, style: textTheme.labelLarge!.copyWith(color: color)) .paddingLTRB(8, 0, 0, 0) ]); } diff --git a/lib/contacts/views/contact_details_widget.dart b/lib/contacts/views/contact_details_widget.dart index bd4376f..7b5416e 100644 --- a/lib/contacts/views/contact_details_widget.dart +++ b/lib/contacts/views/contact_details_widget.dart @@ -1,13 +1,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; +import '../../tools/tools.dart'; import '../contacts.dart'; class ContactDetailsWidget extends StatefulWidget { - const ContactDetailsWidget({required this.contact, super.key}); - final proto.Contact contact; + const ContactDetailsWidget( + {required this.contact, this.onModifiedState, super.key}); @override State createState() => _ContactDetailsWidgetState(); @@ -15,8 +17,14 @@ class ContactDetailsWidget extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', contact)); + properties + ..add(DiagnosticsProperty('contact', contact)) + ..add(ObjectFlagProperty.has( + 'onModifiedState', onModifiedState)); } + + final proto.Contact contact; + final void Function(bool)? onModifiedState; } class _ContactDetailsWidgetState extends State @@ -24,18 +32,21 @@ class _ContactDetailsWidgetState extends State @override Widget build(BuildContext context) => SingleChildScrollView( child: EditContactForm( - formKey: GlobalKey(), contact: widget.contact, - onSubmit: (fbs) async { + submitText: translate('button.update'), + submitDisabledText: translate('button.waiting_for_network'), + onModifiedState: widget.onModifiedState, + onSubmit: (updatedContactSpec) async { final contactList = context.read(); - await contactList.updateContactFields( - localConversationRecordKey: - widget.contact.localConversationRecordKey.toVeilid(), - nickname: fbs.currentState - ?.value[EditContactForm.formFieldNickname] as String, - notes: fbs.currentState?.value[EditContactForm.formFieldNotes] - as String, - showAvailability: fbs.currentState - ?.value[EditContactForm.formFieldShowAvailability] as bool); + try { + await contactList.updateContactFields( + localConversationRecordKey: + widget.contact.localConversationRecordKey.toVeilid(), + updatedContactSpec: updatedContactSpec); + } on Exception catch (e) { + log.debug('error updating contact: $e', e); + return false; + } + return true; })); } diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 4cb874d..a0f2fbc 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -40,7 +40,7 @@ class ContactItemWidget extends StatelessWidget { size: 34, borderColor: _disabled ? scale.grayScale.primaryText - : scale.primaryScale.primaryText, + : scale.primaryScale.subtleBorder, foregroundColor: _disabled ? scale.grayScale.primaryText : scale.primaryScale.primaryText, @@ -71,7 +71,7 @@ class ContactItemWidget extends StatelessWidget { endActions: [ if (_onDoubleTap != null) SliderTileAction( - icon: Icons.edit, + //icon: Icons.edit, label: translate('button.edit'), actionScale: ScaleKind.secondary, onPressed: (_context) => @@ -81,7 +81,7 @@ class ContactItemWidget extends StatelessWidget { ), if (_onDelete != null) SliderTileAction( - icon: Icons.delete, + //icon: Icons.delete, label: translate('button.delete'), actionScale: ScaleKind.tertiary, onPressed: (_context) => diff --git a/lib/contacts/views/contacts_browser.dart b/lib/contacts/views/contacts_browser.dart index 89cea88..7040af5 100644 --- a/lib/contacts/views/contacts_browser.dart +++ b/lib/contacts/views/contacts_browser.dart @@ -74,13 +74,10 @@ class _ContactsBrowserState extends State final menuIconColor = scaleConfig.preferBorders ? scale.primaryScale.hoverBorder - : scale.primaryScale.borderText; + : scale.primaryScale.hoverBorder; final menuBackgroundColor = scaleConfig.preferBorders ? scale.primaryScale.elementBackground - : scale.primaryScale.border; - // final menuHoverColor = scaleConfig.preferBorders - // ? scale.primaryScale.hoverElementBackground - // : scale.primaryScale.hoverBorder; + : scale.primaryScale.elementBackground; final menuBorderColor = scale.primaryScale.hoverBorder; @@ -149,13 +146,12 @@ class _ContactsBrowserState extends State }, iconSize: 32, icon: const Icon(Icons.contact_page), - color: scale.primaryScale.hoverBorder, + color: menuIconColor, ), Text(translate('add_contact_sheet.create_invite'), maxLines: 2, textAlign: TextAlign.center, - style: textTheme.labelSmall! - .copyWith(color: scale.primaryScale.hoverBorder)) + style: textTheme.labelSmall!.copyWith(color: menuIconColor)) ]), StarMenu( items: receiveInviteMenuItems, @@ -171,13 +167,12 @@ class _ContactsBrowserState extends State icon: ImageIcon( const AssetImage('assets/images/handshake.png'), size: 32, - color: scale.primaryScale.hoverBorder, + color: menuIconColor, )), Text(translate('add_contact_sheet.receive_invite'), maxLines: 2, textAlign: TextAlign.center, - style: textTheme.labelSmall! - .copyWith(color: scale.primaryScale.hoverBorder)) + style: textTheme.labelSmall!.copyWith(color: menuIconColor)) ]), ), ]).paddingAll(16); @@ -274,8 +269,9 @@ class _ContactsBrowserState extends State case ContactsBrowserElementKind.invitation: final invitation = element.invitation!; return invitation.message - .toLowerCase() - .contains(lowerValue); + .toLowerCase() + .contains(lowerValue) || + invitation.recipient.toLowerCase().contains(lowerValue); } }).toList() }; diff --git a/lib/contacts/views/contacts_dialog.dart b/lib/contacts/views/contacts_dialog.dart index e6e5391..ec85df3 100644 --- a/lib/contacts/views/contacts_dialog.dart +++ b/lib/contacts/views/contacts_dialog.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,8 @@ import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; import '../contacts.dart'; +const _kDoBackArrow = 'doBackArrow'; + class ContactsDialog extends StatefulWidget { const ContactsDialog._({required this.modalContext}); @@ -44,13 +47,8 @@ class _ContactsDialogState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - // final textTheme = theme.textTheme; final scale = theme.extension()!; - final scaleConfig = theme.extension()!; - - final appBarIconColor = scaleConfig.useVisualIndicators - ? scale.secondaryScale.border - : scale.secondaryScale.borderText; + final appBarIconColor = scale.primaryScale.borderText; final enableSplit = !isMobileWidth(context); final enableLeft = enableSplit || _selectedContact == null; @@ -63,20 +61,22 @@ class _ContactsDialogState extends State { title: Text(!enableSplit && enableRight ? translate('contacts_dialog.edit_contact') : translate('contacts_dialog.contacts')), - leading: Navigator.canPop(context) - ? IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - if (!enableSplit && enableRight) { - setState(() { - _selectedContact = null; - }); - } else { + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + singleFuture((this, _kDoBackArrow), () async { + final confirmed = await _onContactSelected(null); + if (!enableSplit && enableRight) { + } else { + if (confirmed) { + if (context.mounted) { Navigator.pop(context); } - }, - ) - : null, + } + } + }); + }, + ), actions: [ if (_selectedContact != null) FittedBox( @@ -85,9 +85,10 @@ class _ContactsDialogState extends State { Column(mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.chat_bubble), + color: appBarIconColor, tooltip: translate('contacts_dialog.new_chat'), onPressed: () async { - await onChatStarted(_selectedContact!); + await _onChatStarted(_selectedContact!); }), Text(translate('contacts_dialog.new_chat'), style: theme.textTheme.labelSmall! @@ -100,10 +101,11 @@ class _ContactsDialogState extends State { Column(mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: const Icon(Icons.close), + color: appBarIconColor, tooltip: translate('contacts_dialog.close_contact'), onPressed: () async { - await onContactSelected(null); + await _onContactSelected(null); }), Text(translate('contacts_dialog.close_contact'), style: theme.textTheme.labelSmall! @@ -115,41 +117,68 @@ class _ContactsDialogState extends State { return ColoredBox( color: scale.primaryScale.appBackground, - child: Row(children: [ - Offstage( - offstage: !enableLeft, - child: SizedBox( - width: enableLeft && !enableRight - ? maxWidth - : (maxWidth / 3).clamp(200, 500), - child: DecoratedBox( - decoration: BoxDecoration( - color: scale.primaryScale.subtleBackground), - child: ContactsBrowser( - selectedContactRecordKey: _selectedContact - ?.localConversationRecordKey - .toVeilid(), - onContactSelected: onContactSelected, - onChatStarted: onChatStarted, - ).paddingLTRB(8, 0, 8, 8)))), - if (enableRight) - if (_selectedContact == null) - const NoContactWidget().expanded() - else - ContactDetailsWidget(contact: _selectedContact!) - .paddingAll(8) - .expanded(), - ])); + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Offstage( + offstage: !enableLeft, + child: SizedBox( + width: enableLeft && !enableRight + ? maxWidth + : (maxWidth / 3).clamp(200, 500), + child: DecoratedBox( + decoration: BoxDecoration( + color: scale + .primaryScale.subtleBackground), + child: ContactsBrowser( + selectedContactRecordKey: _selectedContact + ?.localConversationRecordKey + .toVeilid(), + onContactSelected: _onContactSelected, + onChatStarted: _onChatStarted, + ).paddingLTRB(8, 0, 8, 8)))), + if (enableRight && enableLeft) + Container( + constraints: const BoxConstraints( + minWidth: 1, maxWidth: 1), + color: scale.primaryScale.subtleBorder), + if (enableRight) + if (_selectedContact == null) + const NoContactWidget().expanded() + else + ContactDetailsWidget( + contact: _selectedContact!, + onModifiedState: _onModifiedState) + .paddingLTRB(16, 16, 16, 16) + .expanded(), + ])); }))); } - Future onContactSelected(proto.Contact? contact) async { + void _onModifiedState(bool isModified) { setState(() { - _selectedContact = contact; + _isModified = isModified; }); } - Future onChatStarted(proto.Contact contact) async { + Future _onContactSelected(proto.Contact? contact) async { + if (contact != _selectedContact && _isModified) { + final ok = await showConfirmModal( + context: context, + title: translate('confirmation.discard_changes'), + text: translate('confirmation.are_you_sure_discard')); + if (!ok) { + return false; + } + } + setState(() { + _selectedContact = contact; + _isModified = false; + }); + return true; + } + + Future _onChatStarted(proto.Contact contact) async { final chatListCubit = context.read(); await chatListCubit.getOrCreateChatSingleContact(contact: contact); @@ -163,4 +192,5 @@ class _ContactsDialogState extends State { } proto.Contact? _selectedContact; + bool _isModified = false; } diff --git a/lib/contacts/views/edit_contact_form.dart b/lib/contacts/views/edit_contact_form.dart index 7803ab2..5477c60 100644 --- a/lib/contacts/views/edit_contact_form.dart +++ b/lib/contacts/views/edit_contact_form.dart @@ -1,3 +1,4 @@ +import 'package:async_tools/async_tools.dart'; import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -6,13 +7,18 @@ import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; +import '../models/contact_spec.dart'; import 'availability_widget.dart'; +const _kDoSubmitEditContact = 'doSubmitEditContact'; + class EditContactForm extends StatefulWidget { const EditContactForm({ - required this.formKey, required this.contact, - this.onSubmit, + required this.onSubmit, + required this.submitText, + required this.submitDisabledText, + this.onModifiedState, super.key, }); @@ -20,19 +26,22 @@ class EditContactForm extends StatefulWidget { State createState() => _EditContactFormState(); final proto.Contact contact; - final Future Function(GlobalKey)? onSubmit; - final GlobalKey formKey; + final String submitText; + final String submitDisabledText; + final Future Function(ContactSpec) onSubmit; + final void Function(bool)? onModifiedState; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(ObjectFlagProperty< - Future Function( - GlobalKey p1)?>.has('onSubmit', onSubmit)) + ..add(ObjectFlagProperty Function(ContactSpec p1)>.has( + 'onSubmit', onSubmit)) + ..add(ObjectFlagProperty.has( + 'onModifiedState', onModifiedState)) ..add(DiagnosticsProperty('contact', contact)) - ..add( - DiagnosticsProperty>('formKey', formKey)); + ..add(StringProperty('submitText', submitText)) + ..add(StringProperty('submitDisabledText', submitDisabledText)); } static const String formFieldNickname = 'nickname'; @@ -41,16 +50,46 @@ class EditContactForm extends StatefulWidget { } class _EditContactFormState extends State { + final _formKey = GlobalKey(); + @override void initState() { + _savedValue = ContactSpec.fromProto(widget.contact); + _currentValueNickname = _savedValue.nickname; + super.initState(); } - Widget _availabilityWidget(proto.Availability availability, Color color) => - AvailabilityWidget(availability: availability, color: color); + ContactSpec _makeContactSpec() { + final nickname = _formKey.currentState! + .fields[EditContactForm.formFieldNickname]!.value as String; + final notes = _formKey + .currentState!.fields[EditContactForm.formFieldNotes]!.value as String; + final showAvailability = _formKey.currentState! + .fields[EditContactForm.formFieldShowAvailability]!.value as bool; - @override - Widget build(BuildContext context) { + return ContactSpec( + nickname: nickname, notes: notes, showAvailability: showAvailability); + } + + // Check if everything is the same and update state + void _onChanged() { + final currentValue = _makeContactSpec(); + _isModified = currentValue != _savedValue; + final onModifiedState = widget.onModifiedState; + if (onModifiedState != null) { + onModifiedState(_isModified); + } + } + + Widget _availabilityWidget(proto.Availability availability, Color color) => + AvailabilityWidget( + availability: availability, + color: color, + vertical: false, + ); + + Widget _editContactForm(BuildContext context) { final theme = Theme.of(context); final scale = theme.extension()!; final scaleConfig = theme.extension()!; @@ -60,75 +99,94 @@ class _EditContactFormState extends State { if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) { border = scale.primaryScale.elementBackground; } else { - border = scale.primaryScale.border; + border = scale.primaryScale.subtleBorder; } return FormBuilder( - key: widget.formKey, + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + onChanged: _onChanged, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - AvatarWidget( - name: widget.contact.profile.name, - 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), - SelectableText(widget.contact.profile.name, - style: textTheme.headlineMedium) - .noEditDecoratorLabel( - context, - translate('contact_form.form_name'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - SelectableText(widget.contact.profile.pronouns, - style: textTheme.headlineSmall) - .noEditDecoratorLabel( - context, - translate('contact_form.form_pronouns'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - Row(mainAxisSize: MainAxisSize.min, children: [ - _availabilityWidget(widget.contact.profile.availability, - scale.primaryScale.primaryText), - SelectableText(widget.contact.profile.status, - style: textTheme.bodyMedium) - .paddingSymmetric(horizontal: 8) - ]) - .noEditDecoratorLabel( - context, - translate('contact_form.form_status'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - SelectableText(widget.contact.profile.about, - minLines: 1, maxLines: 8, style: textTheme.bodyMedium) - .noEditDecoratorLabel( - context, - translate('contact_form.form_about'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - SelectableText( - widget.contact.identityPublicKey.value.toVeilid().toString(), - style: textTheme.labelMedium! - .copyWith(fontFamily: 'Source Code Pro')) - .noEditDecoratorLabel( - context, - translate('contact_form.form_fingerprint'), - scale: scale.secondaryScale, - ) - .paddingSymmetric(vertical: 4), - Divider(color: border).paddingLTRB(8, 0, 8, 8), + styledCard( + context: context, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row(children: [ + const Spacer(), + AvatarWidget( + name: _currentValueNickname.isNotEmpty + ? _currentValueNickname + : widget.contact.profile.name, + 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() + ]), + SelectableText(widget.contact.profile.name, + style: textTheme.bodyLarge) + .noEditDecoratorLabel( + context, + translate('contact_form.form_name'), + ) + .paddingSymmetric(vertical: 4), + SelectableText(widget.contact.profile.pronouns, + style: textTheme.bodyLarge) + .noEditDecoratorLabel( + context, + translate('contact_form.form_pronouns'), + ) + .paddingSymmetric(vertical: 4), + Row(mainAxisSize: MainAxisSize.min, children: [ + _availabilityWidget( + widget.contact.profile.availability, + scale.primaryScale.appText), + SelectableText(widget.contact.profile.status, + style: textTheme.bodyMedium) + .paddingSymmetric(horizontal: 8) + ]) + .noEditDecoratorLabel( + context, + translate('contact_form.form_status'), + ) + .paddingSymmetric(vertical: 4), + SelectableText(widget.contact.profile.about, + minLines: 1, + maxLines: 8, + style: textTheme.bodyMedium) + .noEditDecoratorLabel( + context, + translate('contact_form.form_about'), + ) + .paddingSymmetric(vertical: 4), + SelectableText( + widget.contact.identityPublicKey.value + .toVeilid() + .toString(), + style: textTheme.bodyMedium! + .copyWith(fontFamily: 'Source Code Pro')) + .noEditDecoratorLabel( + context, + translate('contact_form.form_fingerprint'), + ) + .paddingSymmetric(vertical: 4), + ]).paddingAll(16)) + .paddingLTRB(0, 0, 0, 16), FormBuilderTextField( - //autofocus: true, name: EditContactForm.formFieldNickname, - initialValue: widget.contact.nickname, + initialValue: _currentValueNickname, + onChanged: (x) { + setState(() { + _currentValueNickname = x ?? ''; + }); + }, decoration: InputDecoration( labelText: translate('contact_form.form_nickname')), maxLength: 64, @@ -136,14 +194,16 @@ class _EditContactFormState extends State { ), FormBuilderCheckbox( name: EditContactForm.formFieldShowAvailability, - initialValue: widget.contact.showAvailability, + initialValue: _savedValue.showAvailability, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('contact_form.form_show_availability'), style: textTheme.labelMedium), ), FormBuilderTextField( name: EditContactForm.formFieldNotes, - initialValue: widget.contact.notes, + initialValue: _savedValue.notes, minLines: 1, maxLines: 8, maxLength: 1024, @@ -152,24 +212,38 @@ class _EditContactFormState extends State { textInputAction: TextInputAction.newline, ), ElevatedButton( - onPressed: widget.onSubmit == null - ? null - : () async { - if (widget.formKey.currentState?.saveAndValidate() ?? - false) { - await widget.onSubmit!(widget.formKey); - } - }, + onPressed: _isModified ? _doSubmit : null, child: Row(mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), - Text((widget.onSubmit == null) - ? translate('contact_form.save') - : translate('contact_form.save')) - .paddingLTRB(0, 0, 4, 0) + Text(widget.submitText).paddingLTRB(0, 0, 4, 0) ]), - ).paddingSymmetric(vertical: 4).alignAtCenterRight(), + ).paddingSymmetric(vertical: 4).alignAtCenter(), ], ), ); } + + void _doSubmit() { + final onSubmit = widget.onSubmit; + if (_formKey.currentState?.saveAndValidate() ?? false) { + singleFuture((this, _kDoSubmitEditContact), () async { + final updatedContactSpec = _makeContactSpec(); + final saved = await onSubmit(updatedContactSpec); + if (saved) { + setState(() { + _savedValue = updatedContactSpec; + }); + _onChanged(); + } + }); + } + } + + @override + Widget build(BuildContext context) => _editContactForm(context); + + /////////////////////////////////////////////////////////////////////////// + late ContactSpec _savedValue; + late String _currentValueNickname; + bool _isModified = false; } diff --git a/lib/layout/home/drawer_menu/drawer_menu.dart b/lib/layout/home/drawer_menu/drawer_menu.dart index 0821bbb..b56d437 100644 --- a/lib/layout/home/drawer_menu/drawer_menu.dart +++ b/lib/layout/home/drawer_menu/drawer_menu.dart @@ -9,12 +9,13 @@ import 'package:go_router/go_router.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; -import '../../../proto/proto.dart' as proto; import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; import 'menu_item_widget.dart'; +const _scaleKind = ScaleKind.secondary; + class DrawerMenu extends StatefulWidget { const DrawerMenu({super.key}); @@ -40,7 +41,7 @@ class _DrawerMenuState extends State { } void _doEditClick(TypedKey superIdentityRecordKey, - proto.Account existingAccount, OwnedDHTRecordPointer accountRecord) { + AccountSpec existingAccount, OwnedDHTRecordPointer accountRecord) { singleFuture(this, () async { await GoRouterHelper(context).push('/edit_account', extra: [superIdentityRecordKey, existingAccount, accountRecord]); @@ -58,45 +59,6 @@ class _DrawerMenuState extends State { borderRadius: BorderRadius.circular(borderRadius))), child: child); - Widget _makeAvatarWidget({ - required String name, - required double size, - required Color borderColor, - required Color foregroundColor, - required Color backgroundColor, - required ScaleConfig scaleConfig, - required TextStyle textStyle, - ImageProvider? imageProvider, - }) { - final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join(); - late final String shortname; - if (abbrev.length >= 3) { - shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1]; - } else { - shortname = abbrev; - } - - return Container( - height: size, - width: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: scaleConfig.preferBorders - ? Border.all( - color: borderColor, - width: 2 * (size ~/ 32 + 1), - strokeAlign: BorderSide.strokeAlignOutside) - : null, - color: Colors.blue, - ), - child: AvatarImage( - //size: 32, - backgroundImage: imageProvider, - backgroundColor: backgroundColor, - foregroundColor: foregroundColor, - child: Text(shortname, style: textStyle))); - } - Widget _makeAccountWidget( {required String name, required bool selected, @@ -173,6 +135,7 @@ class _DrawerMenuState extends State { footerButtonIconColor: border, footerButtonIconHoverColor: hoverBackground, footerButtonIconFocusColor: activeBackground, + minHeight: 48, )); } @@ -184,6 +147,7 @@ class _DrawerMenuState extends State { final theme = Theme.of(context); final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(_scaleKind); final loggedInAccounts = []; final loggedOutAccounts = []; @@ -197,9 +161,6 @@ class _DrawerMenuState extends State { final avAccountRecordState = perAccountState?.avAccountRecordState; if (perAccountState != null && avAccountRecordState != null) { // Account is logged in - final scale = scaleConfig.useVisualIndicators - ? theme.extension()!.primaryScale - : theme.extension()!.tertiaryScale; final loggedInAccount = avAccountRecordState.when( data: (value) => _makeAccountWidget( name: value.profile.name, @@ -213,7 +174,7 @@ class _DrawerMenuState extends State { footerCallback: () { _doEditClick( superIdentityRecordKey, - value, + AccountSpec.fromProto(value), perAccountState.accountInfo.userLogin!.accountRecordInfo .accountRecord); }), @@ -311,13 +272,14 @@ class _DrawerMenuState extends State { Widget _getBottomButtons() { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(_scaleKind); final settingsButton = _getButton( icon: const Icon(Icons.settings), tooltip: translate('menu.settings_tooltip'), - scale: scale.tertiaryScale, + scale: scale, scaleConfig: scaleConfig, onPressed: () async { await GoRouterHelper(context).push('/settings'); @@ -326,7 +288,7 @@ class _DrawerMenuState extends State { final addButton = _getButton( icon: const Icon(Icons.add), tooltip: translate('menu.add_account_tooltip'), - scale: scale.tertiaryScale, + scale: scale, scaleConfig: scaleConfig, onPressed: () async { await GoRouterHelper(context).push('/new_account'); @@ -340,8 +302,9 @@ class _DrawerMenuState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final scale = theme.extension()!; + final scaleScheme = theme.extension()!; final scaleConfig = theme.extension()!; + final scale = scaleScheme.scale(_scaleKind); //final textTheme = theme.textTheme; final localAccounts = context.watch().state; final perAccountCollectionBlocMapState = @@ -351,8 +314,8 @@ class _DrawerMenuState extends State { begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - scale.tertiaryScale.border, - scale.tertiaryScale.subtleBorder, + scale.border, + scale.subtleBorder, ]); return DecoratedBox( @@ -360,34 +323,35 @@ class _DrawerMenuState extends State { shadows: [ if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) BoxShadow( - color: scale.tertiaryScale.primary.darken(80), + color: scale.primary.darken(60), spreadRadius: 2, ) else if (scaleConfig.useVisualIndicators && scaleConfig.preferBorders) BoxShadow( - color: scale.tertiaryScale.border, + color: scale.border, spreadRadius: 2, ) else BoxShadow( - color: scale.tertiaryScale.primary.darken(40), - blurRadius: 6, + color: scale.appBackground.darken(60).withAlpha(0x3F), + blurRadius: 16, + spreadRadius: 2, offset: const Offset( 0, - 4, + 2, ), ), ], gradient: scaleConfig.useVisualIndicators ? null : gradient, color: scaleConfig.useVisualIndicators ? (scaleConfig.preferBorders - ? scale.tertiaryScale.appBackground - : scale.tertiaryScale.subtleBorder) + ? scale.appBackground + : scale.subtleBorder) : null, shape: RoundedRectangleBorder( side: scaleConfig.preferBorders - ? BorderSide(color: scale.tertiaryScale.primary, width: 2) + ? BorderSide(color: scale.primary, width: 2) : BorderSide.none, borderRadius: BorderRadius.only( topRight: Radius.circular(16 * scaleConfig.borderRadiusScale), @@ -399,31 +363,31 @@ class _DrawerMenuState extends State { child: ColorFiltered( colorFilter: ColorFilter.mode( theme.brightness == Brightness.light - ? scale.tertiaryScale.primary - : scale.tertiaryScale.border, + ? scale.primary + : scale.border, scaleConfig.preferBorders ? BlendMode.modulate : BlendMode.dst), child: Row(children: [ - SvgPicture.asset( - height: 48, - 'assets/images/icon.svg', - colorFilter: scaleConfig.useVisualIndicators - ? grayColorFilter - : null) - .paddingLTRB(0, 0, 16, 0), + // SvgPicture.asset( + // height: 48, + // 'assets/images/icon.svg', + // colorFilter: scaleConfig.useVisualIndicators + // ? grayColorFilter + // : null) + // .paddingLTRB(0, 0, 16, 0), SvgPicture.asset( height: 48, 'assets/images/title.svg', colorFilter: scaleConfig.useVisualIndicators ? grayColorFilter - : null), + : dodgeFilter), ]))), Text(translate('menu.accounts'), style: theme.textTheme.titleMedium!.copyWith( color: scaleConfig.preferBorders - ? scale.tertiaryScale.border - : scale.tertiaryScale.borderText)) + ? scale.border + : scale.borderText)) .paddingLTRB(0, 16, 0, 16), ListView( shrinkWrap: true, @@ -438,16 +402,16 @@ class _DrawerMenuState extends State { Text('${translate('menu.version')} $packageInfoVersion', style: theme.textTheme.labelMedium!.copyWith( color: scaleConfig.preferBorders - ? scale.tertiaryScale.hoverBorder - : scale.tertiaryScale.subtleBackground)), + ? scale.hoverBorder + : scale.subtleBackground)), const Spacer(), SignalStrengthMeterWidget( color: scaleConfig.preferBorders - ? scale.tertiaryScale.hoverBorder - : scale.tertiaryScale.subtleBackground, + ? scale.hoverBorder + : scale.subtleBackground, inactiveColor: scaleConfig.preferBorders - ? scale.tertiaryScale.border - : scale.tertiaryScale.elementBackground, + ? scale.border + : scale.elementBackground, ), ]) ]).paddingAll(16), diff --git a/lib/layout/home/drawer_menu/menu_item_widget.dart b/lib/layout/home/drawer_menu/menu_item_widget.dart index 8529411..a786010 100644 --- a/lib/layout/home/drawer_menu/menu_item_widget.dart +++ b/lib/layout/home/drawer_menu/menu_item_widget.dart @@ -22,39 +22,42 @@ class MenuItemWidget extends StatelessWidget { this.footerButtonIconHoverColor, this.footerButtonIconFocusColor, this.footerCallback, + this.minHeight = 0, super.key, }); @override Widget build(BuildContext context) => TextButton( - onPressed: callback, - style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( - backgroundColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return backgroundHoverColor; - } - if (states.contains(WidgetState.focused)) { - return backgroundFocusColor; - } - return backgroundColor; - }), - side: WidgetStateBorderSide.resolveWith((states) { - if (states.contains(WidgetState.hovered)) { - return borderColor != null - ? BorderSide(width: 2, color: borderHoverColor!) - : null; - } - if (states.contains(WidgetState.focused)) { - return borderColor != null - ? BorderSide(width: 2, color: borderFocusColor!) - : null; - } + onPressed: callback, + style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { + return backgroundHoverColor; + } + if (states.contains(WidgetState.focused)) { + return backgroundFocusColor; + } + return backgroundColor; + }), + side: WidgetStateBorderSide.resolveWith((states) { + if (states.contains(WidgetState.hovered)) { return borderColor != null - ? BorderSide(width: 2, color: borderColor!) + ? BorderSide(width: 2, color: borderHoverColor!) : null; - }), - shape: WidgetStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(borderRadius ?? 0)))), + } + if (states.contains(WidgetState.focused)) { + return borderColor != null + ? BorderSide(width: 2, color: borderFocusColor!) + : null; + } + return borderColor != null + ? BorderSide(width: 2, color: borderColor!) + : null; + }), + shape: WidgetStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(borderRadius ?? 0)))), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: minHeight), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -81,7 +84,7 @@ class MenuItemWidget extends StatelessWidget { onPressed: footerCallback), ], ).paddingAll(2), - ); + )); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -106,7 +109,8 @@ class MenuItemWidget extends StatelessWidget { ..add(ColorProperty('borderColor', borderColor)) ..add(DoubleProperty('borderRadius', borderRadius)) ..add(ColorProperty('borderHoverColor', borderHoverColor)) - ..add(ColorProperty('borderFocusColor', borderFocusColor)); + ..add(ColorProperty('borderFocusColor', borderFocusColor)) + ..add(DoubleProperty('minHeight', minHeight)); } //////////////////////////////////////////////////////////////////////////// @@ -129,4 +133,5 @@ class MenuItemWidget extends StatelessWidget { final Color? footerButtonIconColor; final Color? footerButtonIconHoverColor; final Color? footerButtonIconFocusColor; + final double minHeight; } diff --git a/lib/layout/home/home_screen.dart b/lib/layout/home/home_screen.dart index f226717..3e8e98b 100644 --- a/lib/layout/home/home_screen.dart +++ b/lib/layout/home/home_screen.dart @@ -96,7 +96,7 @@ class HomeScreenState extends State ), Row(mainAxisSize: MainAxisSize.min, children: [ StatefulBuilder( - builder: (context, setState) => Checkbox.adaptive( + builder: (context, setState) => Checkbox( value: displayBetaWarning, onChanged: (value) { setState(() { @@ -213,7 +213,6 @@ class HomeScreenState extends State style: theme.textTheme.bodySmall!, child: ZoomDrawer( controller: _zoomDrawerController, - //menuBackgroundColor: Colors.transparent, menuScreen: Builder(builder: (context) { final zoomDrawer = ZoomDrawer.of(context); zoomDrawer!.stateNotifier.addListener(() { @@ -228,7 +227,7 @@ class HomeScreenState extends State child: Builder(builder: _buildAccountPageView)), borderRadius: 0, angle: 0, - mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), + //mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F), openCurve: Curves.fastEaseInToSlowEaseOut, // duration: const Duration(milliseconds: 250), // reverseDuration: const Duration(milliseconds: 250), diff --git a/lib/notifications/views/notifications_preferences.dart b/lib/notifications/views/notifications_preferences.dart index 5967522..95d4a1e 100644 --- a/lib/notifications/views/notifications_preferences.dart +++ b/lib/notifications/views/notifications_preferences.dart @@ -130,6 +130,8 @@ Widget buildSettingsPageNotificationPreferences( FormBuilderCheckbox( name: formFieldDisplayBetaWarning, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('settings_page.display_beta_warning'), style: textTheme.labelMedium), initialValue: notificationsPreference.displayBetaWarning, @@ -146,6 +148,8 @@ Widget buildSettingsPageNotificationPreferences( FormBuilderCheckbox( name: formFieldEnableBadge, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('settings_page.enable_badge'), style: textTheme.labelMedium), initialValue: notificationsPreference.enableBadge, @@ -161,6 +165,8 @@ Widget buildSettingsPageNotificationPreferences( FormBuilderCheckbox( name: formFieldEnableNotifications, side: BorderSide(color: scale.primaryScale.border, width: 2), + checkColor: scale.primaryScale.borderText, + activeColor: scale.primaryScale.border, title: Text(translate('settings_page.enable_notifications'), style: textTheme.labelMedium), initialValue: notificationsPreference.enableNotifications, diff --git a/lib/proto/veilidchat.pb.dart b/lib/proto/veilidchat.pb.dart index 3947baf..245f9f3 100644 --- a/lib/proto/veilidchat.pb.dart +++ b/lib/proto/veilidchat.pb.dart @@ -3024,6 +3024,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { $fixnum.Int64? expiration, $core.List<$core.int>? invitation, $core.String? message, + $core.String? recipient, }) { final $result = create(); if (contactRequestInbox != null) { @@ -3047,6 +3048,9 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { if (message != null) { $result.message = message; } + if (recipient != null) { + $result.recipient = recipient; + } return $result; } ContactInvitationRecord._() : super(); @@ -3061,6 +3065,7 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { ..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..a<$core.List<$core.int>>(6, _omitFieldNames ? '' : 'invitation', $pb.PbFieldType.OY) ..aOS(7, _omitFieldNames ? '' : 'message') + ..aOS(8, _omitFieldNames ? '' : 'recipient') ..hasRequiredFields = false ; @@ -3162,6 +3167,16 @@ class ContactInvitationRecord extends $pb.GeneratedMessage { $core.bool hasMessage() => $_has(6); @$pb.TagNumber(7) void clearMessage() => clearField(7); + + /// The recipient sent along with the invitation + @$pb.TagNumber(8) + $core.String get recipient => $_getSZ(7); + @$pb.TagNumber(8) + set recipient($core.String v) { $_setString(7, v); } + @$pb.TagNumber(8) + $core.bool hasRecipient() => $_has(7); + @$pb.TagNumber(8) + void clearRecipient() => clearField(8); } diff --git a/lib/proto/veilidchat.pbjson.dart b/lib/proto/veilidchat.pbjson.dart index e102d40..81bf741 100644 --- a/lib/proto/veilidchat.pbjson.dart +++ b/lib/proto/veilidchat.pbjson.dart @@ -628,6 +628,7 @@ const ContactInvitationRecord$json = { {'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'}, {'1': 'invitation', '3': 6, '4': 1, '5': 12, '10': 'invitation'}, {'1': 'message', '3': 7, '4': 1, '5': 9, '10': 'message'}, + {'1': 'recipient', '3': 8, '4': 1, '5': 9, '10': 'recipient'}, ], }; @@ -639,5 +640,6 @@ final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64D 'NlY3JldBgDIAEoCzIRLnZlaWxpZC5DcnlwdG9LZXlSDHdyaXRlclNlY3JldBJTCh1sb2NhbF9j' 'b252ZXJzYXRpb25fcmVjb3JkX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIabG9jYWxDb2' '52ZXJzYXRpb25SZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBFIKZXhwaXJhdGlvbhIeCgpp' - 'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2U='); + 'bnZpdGF0aW9uGAYgASgMUgppbnZpdGF0aW9uEhgKB21lc3NhZ2UYByABKAlSB21lc3NhZ2USHA' + 'oJcmVjaXBpZW50GAggASgJUglyZWNpcGllbnQ='); diff --git a/lib/proto/veilidchat.proto b/lib/proto/veilidchat.proto index 0d4ca0a..e669959 100644 --- a/lib/proto/veilidchat.proto +++ b/lib/proto/veilidchat.proto @@ -478,4 +478,6 @@ message ContactInvitationRecord { bytes invitation = 6; // The message sent along with the invitation string message = 7; + // The recipient sent along with the invitation + string recipient = 8; } \ No newline at end of file diff --git a/lib/router/cubits/router_cubit.dart b/lib/router/cubits/router_cubit.dart index 974319a..48bf95f 100644 --- a/lib/router/cubits/router_cubit.dart +++ b/lib/router/cubits/router_cubit.dart @@ -11,7 +11,6 @@ import 'package:veilid_support/veilid_support.dart'; import '../../../account_manager/account_manager.dart'; import '../../layout/layout.dart'; -import '../../proto/proto.dart' as proto; import '../../settings/settings.dart'; import '../../tools/tools.dart'; import '../../veilid_processor/views/developer.dart'; @@ -43,10 +42,8 @@ class RouterCubit extends Cubit { case AccountRepositoryChange.localAccounts: emit(state.copyWith( hasAnyAccount: accountRepository.getLocalAccounts().isNotEmpty)); - break; case AccountRepositoryChange.userLogins: case AccountRepositoryChange.activeLocalAccount: - break; } }); } @@ -72,7 +69,7 @@ class RouterCubit extends Cubit { final extra = state.extra! as List; return EditAccountPage( superIdentityRecordKey: extra[0]! as TypedKey, - existingAccount: extra[1]! as proto.Account, + initialValue: extra[1]! as AccountSpec, accountRecord: extra[2]! as OwnedDHTRecordPointer, ); }, diff --git a/lib/settings/settings_page.dart b/lib/settings/settings_page.dart index 94606aa..05ba514 100644 --- a/lib/settings/settings_page.dart +++ b/lib/settings/settings_page.dart @@ -45,6 +45,7 @@ class SettingsPageState extends State { child: FormBuilder( key: _formKey, child: ListView( + padding: const EdgeInsets.all(8), children: [ buildSettingsPageColorPreferences( context: context, @@ -56,6 +57,6 @@ class SettingsPageState extends State { context: context, onChanged: () => setState(() {})), ].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(), ), - ).paddingSymmetric(horizontal: 24, vertical: 16), + ).paddingSymmetric(horizontal: 8, vertical: 8), ))); } diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart index a7039e5..cd0b9ce 100644 --- a/lib/theme/models/chat_theme.dart +++ b/lib/theme/models/chat_theme.dart @@ -14,7 +14,7 @@ ChatTheme makeChatTheme( secondaryColor: scaleConfig.preferBorders ? scale.secondaryScale.calloutText : scale.secondaryScale.calloutBackground, - backgroundColor: scale.grayScale.appBackground, + backgroundColor: scale.grayScale.appBackground.withAlpha(192), messageBorderRadius: scaleConfig.borderRadiusScale * 16, bubbleBorderSide: scaleConfig.preferBorders ? BorderSide( diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart index b71ebea..861b052 100644 --- a/lib/theme/models/contrast_generator.dart +++ b/lib/theme/models/contrast_generator.dart @@ -1,9 +1,6 @@ import 'package:flutter/material.dart'; import 'radix_generator.dart'; -import 'scale_theme/scale_color.dart'; -import 'scale_theme/scale_input_decorator_theme.dart'; -import 'scale_theme/scale_scheme.dart'; import 'scale_theme/scale_theme.dart'; ScaleColor _contrastScaleColor( @@ -29,6 +26,7 @@ ScaleColor _contrastScaleColor( primaryText: front, borderText: back, dialogBorder: front, + dialogBorderText: back, calloutBackground: front, calloutText: back, ); @@ -246,7 +244,7 @@ ThemeData contrastGenerator({ TextTheme? customTextTheme, }) { final textTheme = customTextTheme ?? makeRadixTextTheme(brightness); - final scaleScheme = _contrastScaleScheme( + final scheme = _contrastScaleScheme( brightness: brightness, primaryFront: primaryFront, primaryBack: primaryBack, @@ -259,55 +257,51 @@ ThemeData contrastGenerator({ errorFront: errorFront, errorBack: errorBack, ); - final colorScheme = scaleScheme.toColorScheme( - brightness, - ); - final scaleTheme = ScaleTheme( - textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); - final baseThemeData = ThemeData.from( - colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); + final scaleTheme = + ScaleTheme(textTheme: textTheme, scheme: scheme, config: scaleConfig); + + final baseThemeData = scaleTheme.toThemeData(brightness); + + final elevatedButtonTheme = ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primaryScale.elementBackground, + foregroundColor: scheme.primaryScale.appText, + disabledBackgroundColor: + scheme.grayScale.elementBackground.withAlpha(0x7F), + disabledForegroundColor: scheme.grayScale.appText.withAlpha(0x7F), + shape: RoundedRectangleBorder( + side: BorderSide(color: scheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale))) + .copyWith(side: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); + } else if (states.contains(WidgetState.pressed)) { + return BorderSide( + color: scheme.primaryScale.border, + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.hovered)) { + return BorderSide(color: scheme.primaryScale.hoverBorder); + } else if (states.contains(WidgetState.focused)) { + return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); + } + return BorderSide(color: scheme.primaryScale.border); + }))); + final themeData = baseThemeData.copyWith( - appBarTheme: baseThemeData.appBarTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.border, - foregroundColor: scaleScheme.primaryScale.borderText), - bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( - elevation: 0, - modalElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16 * scaleConfig.borderRadiusScale), - topRight: - Radius.circular(16 * scaleConfig.borderRadiusScale)))), - canvasColor: scaleScheme.primaryScale.subtleBackground, - chipTheme: baseThemeData.chipTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.elementBackground, - selectedColor: scaleScheme.primaryScale.activeElementBackground, - surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, - checkmarkColor: scaleScheme.primaryScale.border, - side: BorderSide(color: scaleScheme.primaryScale.border)), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: scaleScheme.primaryScale.elementBackground, - foregroundColor: scaleScheme.primaryScale.appText, - disabledBackgroundColor: scaleScheme.grayScale.elementBackground, - disabledForegroundColor: scaleScheme.grayScale.appText, - shape: RoundedRectangleBorder( - side: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), - ), + // chipTheme: baseThemeData.chipTheme.copyWith( + // backgroundColor: scaleScheme.primaryScale.elementBackground, + // selectedColor: scaleScheme.primaryScale.activeElementBackground, + // surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, + // checkmarkColor: scaleScheme.primaryScale.border, + // side: BorderSide(color: scaleScheme.primaryScale.border)), + elevatedButtonTheme: elevatedButtonTheme, textSelectionTheme: TextSelectionThemeData( - cursorColor: scaleScheme.primaryScale.appText, - selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F), - selectionHandleColor: scaleScheme.primaryScale.appText), - inputDecorationTheme: - ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), - extensions: >[ - scaleScheme, - scaleConfig, - scaleTheme - ]); + cursorColor: scheme.primaryScale.appText, + selectionColor: scheme.primaryScale.appText.withAlpha(0x7F), + selectionHandleColor: scheme.primaryScale.appText), + extensions: >[scheme, scaleConfig, scaleTheme]); return themeData; } diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 3dac7bc..a5c5f87 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -5,9 +5,6 @@ import 'package:flutter/material.dart'; import 'package:radix_colors/radix_colors.dart'; import '../../tools/tools.dart'; -import 'scale_theme/scale_color.dart'; -import 'scale_theme/scale_input_decorator_theme.dart'; -import 'scale_theme/scale_scheme.dart'; import 'scale_theme/scale_theme.dart'; enum RadixThemeColor { @@ -291,6 +288,7 @@ extension ToScaleColor on RadixColor { primaryText: scaleExtra.foregroundText, borderText: step12, dialogBorder: step9, + dialogBorderText: scaleExtra.foregroundText, calloutBackground: step9, calloutText: scaleExtra.foregroundText, ); @@ -609,7 +607,6 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final textTheme = makeRadixTextTheme(brightness); final radix = _radixScheme(brightness, themeColor); final scaleScheme = radix.toScale(); - final colorScheme = scaleScheme.toColorScheme(brightness); final scaleConfig = ScaleConfig( useVisualIndicators: false, preferBorders: false, @@ -619,68 +616,7 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { final scaleTheme = ScaleTheme( textTheme: textTheme, scheme: scaleScheme, config: scaleConfig); - final baseThemeData = ThemeData.from( - colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); - - final themeData = baseThemeData.copyWith( - scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( - thumbColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return scaleScheme.primaryScale.border; - } else if (states.contains(WidgetState.hovered)) { - return scaleScheme.primaryScale.hoverBorder; - } - return scaleScheme.primaryScale.subtleBorder; - }), trackColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return scaleScheme.primaryScale.activeElementBackground; - } else if (states.contains(WidgetState.hovered)) { - return scaleScheme.primaryScale.hoverElementBackground; - } - return scaleScheme.primaryScale.elementBackground; - }), trackBorderColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return scaleScheme.primaryScale.subtleBorder; - } else if (states.contains(WidgetState.hovered)) { - return scaleScheme.primaryScale.subtleBorder; - } - return scaleScheme.primaryScale.subtleBorder; - })), - appBarTheme: baseThemeData.appBarTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.border, - foregroundColor: scaleScheme.primaryScale.borderText), - bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( - elevation: 0, - modalElevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16)))), - canvasColor: scaleScheme.primaryScale.subtleBackground, - chipTheme: baseThemeData.chipTheme.copyWith( - backgroundColor: scaleScheme.primaryScale.elementBackground, - selectedColor: scaleScheme.primaryScale.activeElementBackground, - surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, - checkmarkColor: scaleScheme.primaryScale.primary, - side: BorderSide(color: scaleScheme.primaryScale.border)), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: scaleScheme.primaryScale.elementBackground, - foregroundColor: scaleScheme.primaryScale.primary, - disabledBackgroundColor: scaleScheme.grayScale.elementBackground, - disabledForegroundColor: scaleScheme.grayScale.primary, - shape: RoundedRectangleBorder( - side: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))), - ), - inputDecorationTheme: - ScaleInputDecoratorTheme(scaleScheme, scaleConfig, textTheme), - extensions: >[ - scaleScheme, - scaleConfig, - scaleTheme - ]); + final themeData = scaleTheme.toThemeData(brightness); return themeData; } diff --git a/lib/theme/models/scale_theme/scale_color.dart b/lib/theme/models/scale_theme/scale_color.dart index 244f6a3..e50a01b 100644 --- a/lib/theme/models/scale_theme/scale_color.dart +++ b/lib/theme/models/scale_theme/scale_color.dart @@ -17,6 +17,7 @@ class ScaleColor { required this.primaryText, required this.borderText, required this.dialogBorder, + required this.dialogBorderText, required this.calloutBackground, required this.calloutText, }); @@ -36,6 +37,7 @@ class ScaleColor { Color primaryText; Color borderText; Color dialogBorder; + Color dialogBorderText; Color calloutBackground; Color calloutText; @@ -55,6 +57,7 @@ class ScaleColor { Color? foregroundText, Color? borderText, Color? dialogBorder, + Color? dialogBorderText, Color? calloutBackground, Color? calloutText, }) => @@ -76,6 +79,7 @@ class ScaleColor { primaryText: foregroundText ?? this.primaryText, borderText: borderText ?? this.borderText, dialogBorder: dialogBorder ?? this.dialogBorder, + dialogBorderText: dialogBorderText ?? this.dialogBorderText, calloutBackground: calloutBackground ?? this.calloutBackground, calloutText: calloutText ?? this.calloutText); @@ -112,6 +116,9 @@ class ScaleColor { const Color(0x00000000), dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ?? const Color(0x00000000), + dialogBorderText: + Color.lerp(a.dialogBorderText, b.dialogBorderText, t) ?? + const Color(0x00000000), calloutBackground: Color.lerp(a.calloutBackground, b.calloutBackground, t) ?? const Color(0x00000000), diff --git a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart index 94764a5..692ec85 100644 --- a/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart +++ b/lib/theme/models/scale_theme/scale_custom_dropdown_theme.dart @@ -1,7 +1,6 @@ import 'package:animated_custom_dropdown/custom_dropdown.dart'; import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; class ScaleCustomDropdownTheme { diff --git a/lib/theme/models/scale_theme/scale_input_decorator_theme.dart b/lib/theme/models/scale_theme/scale_input_decorator_theme.dart index 3af1f15..1fb26a4 100644 --- a/lib/theme/models/scale_theme/scale_input_decorator_theme.dart +++ b/lib/theme/models/scale_theme/scale_input_decorator_theme.dart @@ -1,36 +1,61 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; class ScaleInputDecoratorTheme extends InputDecorationTheme { ScaleInputDecoratorTheme( this._scaleScheme, ScaleConfig scaleConfig, this._textTheme) - : super( - border: OutlineInputBorder( - borderSide: BorderSide(color: _scaleScheme.primaryScale.border), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), - contentPadding: const EdgeInsets.all(8), - labelStyle: TextStyle( - color: _scaleScheme.primaryScale.subtleText.withAlpha(127)), - floatingLabelStyle: - TextStyle(color: _scaleScheme.primaryScale.subtleText), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: _scaleScheme.primaryScale.hoverBorder, width: 2), - borderRadius: - BorderRadius.circular(8 * scaleConfig.borderRadiusScale))); + : hintAlpha = scaleConfig.preferBorders ? 127 : 255, + super( + contentPadding: const EdgeInsets.all(8), + labelStyle: TextStyle(color: _scaleScheme.primaryScale.subtleText), + floatingLabelStyle: + TextStyle(color: _scaleScheme.primaryScale.subtleText), + border: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + errorBorder: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.errorScale.border), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: _scaleScheme.primaryScale.hoverBorder, width: 2), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + hoverColor: + _scaleScheme.primaryScale.hoverElementBackground.withAlpha(0x7F), + filled: true, + focusedErrorBorder: OutlineInputBorder( + borderSide: + BorderSide(color: _scaleScheme.errorScale.border, width: 2), + borderRadius: + BorderRadius.circular(8 * scaleConfig.borderRadiusScale)), + ); final ScaleScheme _scaleScheme; final TextTheme _textTheme; + final int hintAlpha; + final int disabledAlpha = 127; @override TextStyle? get hintStyle => WidgetStateTextStyle.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return TextStyle(color: _scaleScheme.grayScale.border); } - return TextStyle(color: _scaleScheme.primaryScale.border); + return TextStyle( + color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha)); }); @override @@ -46,7 +71,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return BorderSide( - color: _scaleScheme.grayScale.border.withAlpha(127)); + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); } if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.hovered)) { @@ -71,7 +96,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { BorderSide? get outlineBorder => WidgetStateBorderSide.resolveWith((states) { if (states.contains(WidgetState.disabled)) { return BorderSide( - color: _scaleScheme.grayScale.border.withAlpha(127)); + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); } if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.hovered)) { @@ -97,7 +122,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { @override Color? get prefixIconColor => WidgetStateColor.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return _scaleScheme.primaryScale.primary.withAlpha(127); + return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha); } if (states.contains(WidgetState.error)) { return _scaleScheme.errorScale.primary; @@ -108,7 +133,7 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { @override Color? get suffixIconColor => WidgetStateColor.resolveWith((states) { if (states.contains(WidgetState.disabled)) { - return _scaleScheme.primaryScale.primary.withAlpha(127); + return _scaleScheme.primaryScale.primary.withAlpha(disabledAlpha); } if (states.contains(WidgetState.error)) { return _scaleScheme.errorScale.primary; @@ -121,7 +146,39 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { final textStyle = _textTheme.bodyLarge ?? const TextStyle(); if (states.contains(WidgetState.disabled)) { return textStyle.copyWith( - color: _scaleScheme.grayScale.border.withAlpha(127)); + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); + } + if (states.contains(WidgetState.error)) { + if (states.contains(WidgetState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(WidgetState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + return textStyle.copyWith( + color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(WidgetState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + if (states.contains(WidgetState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + return textStyle.copyWith( + color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha)); + }); + + @override + TextStyle? get floatingLabelStyle => + WidgetStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodyLarge ?? const TextStyle(); + if (states.contains(WidgetState.disabled)) { + return textStyle.copyWith( + color: _scaleScheme.grayScale.border.withAlpha(disabledAlpha)); } if (states.contains(WidgetState.error)) { if (states.contains(WidgetState.hovered)) { @@ -146,18 +203,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { return textStyle.copyWith(color: _scaleScheme.primaryScale.border); }); - @override - TextStyle? get floatingLabelStyle => labelStyle; - @override TextStyle? get helperStyle => WidgetStateTextStyle.resolveWith((states) { final textStyle = _textTheme.bodySmall ?? const TextStyle(); if (states.contains(WidgetState.disabled)) { - return textStyle.copyWith( - color: _scaleScheme.grayScale.border.withAlpha(127)); + return textStyle.copyWith(color: _scaleScheme.grayScale.border); } return textStyle.copyWith( - color: _scaleScheme.secondaryScale.border.withAlpha(127)); + color: _scaleScheme.primaryScale.border.withAlpha(hintAlpha)); }); @override @@ -165,6 +218,14 @@ class ScaleInputDecoratorTheme extends InputDecorationTheme { final textStyle = _textTheme.bodySmall ?? const TextStyle(); return textStyle.copyWith(color: _scaleScheme.errorScale.primary); }); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(IntProperty('disabledAlpha', disabledAlpha)) + ..add(IntProperty('hintAlpha', hintAlpha)); + } } extension ScaleInputDecoratorThemeExt on ScaleTheme { diff --git a/lib/theme/models/scale_theme/scale_scheme.dart b/lib/theme/models/scale_theme/scale_scheme.dart index ac266bc..8c4a6b8 100644 --- a/lib/theme/models/scale_theme/scale_scheme.dart +++ b/lib/theme/models/scale_theme/scale_scheme.dart @@ -76,8 +76,8 @@ class ScaleScheme extends ThemeExtension { ColorScheme toColorScheme(Brightness brightness) => ColorScheme( brightness: brightness, - primary: primaryScale.primary, // reviewed - onPrimary: primaryScale.primaryText, // reviewed + primary: primaryScale.primary, + onPrimary: primaryScale.primaryText, // primaryContainer: primaryScale.hoverElementBackground, // onPrimaryContainer: primaryScale.subtleText, secondary: secondaryScale.primary, @@ -92,15 +92,12 @@ class ScaleScheme extends ThemeExtension { onError: errorScale.primaryText, // errorContainer: errorScale.hoverElementBackground, // onErrorContainer: errorScale.subtleText, - background: grayScale.appBackground, // reviewed - onBackground: grayScale.appText, // reviewed - surface: primaryScale.appBackground, // reviewed - onSurface: primaryScale.appText, // reviewed - surfaceVariant: secondaryScale.appBackground, + surface: primaryScale.appBackground, + onSurface: primaryScale.appText, onSurfaceVariant: secondaryScale.appText, outline: primaryScale.border, outlineVariant: secondaryScale.border, - shadow: primaryScale.primary.darken(80), + shadow: primaryScale.appBackground.darken(60), //scrim: primaryScale.background, // inverseSurface: primaryScale.subtleText, // onInverseSurface: primaryScale.subtleBackground, diff --git a/lib/theme/models/scale_theme/scale_theme.dart b/lib/theme/models/scale_theme/scale_theme.dart index d539c86..4bfc438 100644 --- a/lib/theme/models/scale_theme/scale_theme.dart +++ b/lib/theme/models/scale_theme/scale_theme.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; export 'scale_color.dart'; @@ -41,4 +42,86 @@ class ScaleTheme extends ThemeExtension { scheme: scheme.lerp(other.scheme, t), config: config.lerp(other.config, t)); } + + ThemeData toThemeData(Brightness brightness) { + final colorScheme = scheme.toColorScheme(brightness); + + final baseThemeData = ThemeData.from( + colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); + + final elevatedButtonTheme = ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: scheme.primaryScale.elementBackground, + foregroundColor: scheme.primaryScale.appText, + disabledBackgroundColor: + scheme.grayScale.elementBackground.withAlpha(0x7F), + disabledForegroundColor: + scheme.grayScale.primary.withAlpha(0x7F), + shape: RoundedRectangleBorder( + side: BorderSide(color: scheme.primaryScale.border), + borderRadius: + BorderRadius.circular(8 * config.borderRadiusScale))) + .copyWith(side: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.disabled)) { + return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); + } else if (states.contains(WidgetState.pressed)) { + return BorderSide( + color: scheme.primaryScale.border, + strokeAlign: BorderSide.strokeAlignOutside); + } else if (states.contains(WidgetState.hovered)) { + return BorderSide(color: scheme.primaryScale.hoverBorder); + } else if (states.contains(WidgetState.focused)) { + return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2); + } + return BorderSide(color: scheme.primaryScale.border); + }))); + + final themeData = baseThemeData.copyWith( + scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.border; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.hoverBorder; + } + return scheme.primaryScale.subtleBorder; + }), trackColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.activeElementBackground; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.hoverElementBackground; + } + return scheme.primaryScale.elementBackground; + }), trackBorderColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.pressed)) { + return scheme.primaryScale.subtleBorder; + } else if (states.contains(WidgetState.hovered)) { + return scheme.primaryScale.subtleBorder; + } + return scheme.primaryScale.subtleBorder; + })), + appBarTheme: baseThemeData.appBarTheme.copyWith( + backgroundColor: scheme.primaryScale.border, + foregroundColor: scheme.primaryScale.borderText), + bottomSheetTheme: baseThemeData.bottomSheetTheme.copyWith( + elevation: 0, + modalElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16 * config.borderRadiusScale), + topRight: Radius.circular(16 * config.borderRadiusScale)))), + canvasColor: scheme.primaryScale.subtleBackground, + chipTheme: baseThemeData.chipTheme.copyWith( + backgroundColor: scheme.primaryScale.elementBackground, + selectedColor: scheme.primaryScale.activeElementBackground, + surfaceTintColor: scheme.primaryScale.hoverElementBackground, + checkmarkColor: scheme.primaryScale.primary, + side: BorderSide(color: scheme.primaryScale.border)), + elevatedButtonTheme: elevatedButtonTheme, + inputDecorationTheme: + ScaleInputDecoratorTheme(scheme, config, textTheme), + extensions: >[scheme, config, this]); + + return themeData; + } } diff --git a/lib/theme/models/scale_theme/scale_tile_theme.dart b/lib/theme/models/scale_theme/scale_tile_theme.dart index e7339d1..da2c3cd 100644 --- a/lib/theme/models/scale_theme/scale_tile_theme.dart +++ b/lib/theme/models/scale_theme/scale_tile_theme.dart @@ -40,7 +40,10 @@ extension ScaleTileThemeExt on ScaleTheme { final shapeBorder = RoundedRectangleBorder( side: config.useVisualIndicators - ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) + ? BorderSide( + width: 2, + color: borderColor, + ) : BorderSide.none, borderRadius: BorderRadius.circular(8 * config.borderRadiusScale)); diff --git a/lib/theme/models/scale_theme/scale_toast_theme.dart b/lib/theme/models/scale_theme/scale_toast_theme.dart index de310d4..61f119d 100644 --- a/lib/theme/models/scale_theme/scale_toast_theme.dart +++ b/lib/theme/models/scale_theme/scale_toast_theme.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'scale_scheme.dart'; import 'scale_theme.dart'; enum ScaleToastKind { @@ -35,14 +34,6 @@ extension ScaleToastThemeExt on ScaleTheme { ScaleToastTheme toastTheme(ScaleToastKind kind) { final toastScaleColor = scheme.scale(ScaleKind.tertiary); - Icon icon; - switch (kind) { - case ScaleToastKind.info: - icon = const Icon(Icons.info, size: 32); - case ScaleToastKind.error: - icon = const Icon(Icons.dangerous, size: 32); - } - final primaryColor = toastScaleColor.calloutText; final borderColor = toastScaleColor.border; final backgroundColor = config.useVisualIndicators @@ -54,6 +45,13 @@ extension ScaleToastThemeExt on ScaleTheme { final titleColor = config.useVisualIndicators ? toastScaleColor.calloutBackground : toastScaleColor.calloutText; + Icon icon; + switch (kind) { + case ScaleToastKind.info: + icon = Icon(Icons.info, size: 32, color: primaryColor); + case ScaleToastKind.error: + icon = Icon(Icons.dangerous, size: 32, color: primaryColor); + } return ScaleToastTheme( primaryColor: primaryColor, diff --git a/lib/theme/views/avatar_widget.dart b/lib/theme/views/avatar_widget.dart index 7a1f610..42bea11 100644 --- a/lib/theme/views/avatar_widget.dart +++ b/lib/theme/views/avatar_widget.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; import '../theme.dart'; class AvatarWidget extends StatelessWidget { - AvatarWidget({ + const AvatarWidget({ required String name, required double size, required Color borderColor, @@ -38,15 +38,11 @@ class AvatarWidget extends StatelessWidget { height: _size, width: _size, decoration: BoxDecoration( - shape: BoxShape.circle, - border: _scaleConfig.useVisualIndicators - ? Border.all( - color: _borderColor, - width: 1 * (_size ~/ 32 + 1), - strokeAlign: BorderSide.strokeAlignOutside) - : null, - color: _borderColor, - ), + shape: BoxShape.circle, + border: Border.all( + color: _borderColor, + width: 1 * (_size ~/ 32 + 1), + strokeAlign: BorderSide.strokeAlignOutside)), child: AvatarImage( //size: 32, backgroundImage: _imageProvider, @@ -55,14 +51,15 @@ class AvatarWidget extends StatelessWidget { ? _foregroundColor : _backgroundColor, child: Text( - shortname, + shortname.isNotEmpty ? shortname : '?', + softWrap: false, style: _textStyle.copyWith( color: _scaleConfig.useVisualIndicators && !_scaleConfig.preferBorders ? _backgroundColor : _foregroundColor, ), - ))); + ).fit().paddingAll(_size / 16))); } //////////////////////////////////////////////////////////////////////////// diff --git a/lib/theme/views/slider_tile.dart b/lib/theme/views/slider_tile.dart index 9b6957a..d293fa6 100644 --- a/lib/theme/views/slider_tile.dart +++ b/lib/theme/views/slider_tile.dart @@ -125,16 +125,21 @@ class SliderTile extends StatelessWidget { child: ListTile( onTap: onTap, dense: true, - visualDensity: const VisualDensity(vertical: -4), + visualDensity: + const VisualDensity(horizontal: -4, vertical: -4), title: Text( title, overflow: TextOverflow.fade, softWrap: false, ), subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, + minTileHeight: 48, iconColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor, - leading: FittedBox(child: leading), - trailing: FittedBox(child: trailing)))))); + leading: + leading != null ? FittedBox(child: leading) : null, + trailing: trailing != null + ? FittedBox(child: trailing) + : null))))); } } diff --git a/lib/theme/views/styled_alert.dart b/lib/theme/views/styled_alert.dart index 39fa6f2..82218e8 100644 --- a/lib/theme/views/styled_alert.dart +++ b/lib/theme/views/styled_alert.dart @@ -94,16 +94,11 @@ Future showErrorModal( {required BuildContext context, required String title, required String text}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - await Alert( context: context, style: _alertStyle(context), useRootNavigator: false, type: AlertType.error, - //style: AlertStyle(), title: title, desc: text, buttons: [ @@ -122,10 +117,6 @@ Future showErrorModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); } @@ -144,16 +135,11 @@ Future showWarningModal( {required BuildContext context, required String title, required String text}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - await Alert( context: context, style: _alertStyle(context), useRootNavigator: false, type: AlertType.warning, - //style: AlertStyle(), title: title, desc: text, buttons: [ @@ -172,10 +158,6 @@ Future showWarningModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); } @@ -183,16 +165,11 @@ Future showWarningWidgetModal( {required BuildContext context, required String title, required Widget child}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - await Alert( context: context, style: _alertStyle(context), useRootNavigator: false, type: AlertType.warning, - //style: AlertStyle(), title: title, content: child, buttons: [ @@ -211,10 +188,6 @@ Future showWarningWidgetModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); } @@ -222,10 +195,6 @@ Future showConfirmModal( {required BuildContext context, required String title, required String text}) async { - // final theme = Theme.of(context); - // final scale = theme.extension()!; - // final scaleConfig = theme.extension()!; - var confirm = false; await Alert( @@ -266,10 +235,6 @@ Future showConfirmModal( ), ) ], - - //backgroundColor: Colors.black, - //titleColor: Colors.white, - //textColor: Colors.white, ).show(); return confirm; diff --git a/lib/theme/views/styled_dialog.dart b/lib/theme/views/styled_dialog.dart index 4e4bd50..75a0f6b 100644 --- a/lib/theme/views/styled_dialog.dart +++ b/lib/theme/views/styled_dialog.dart @@ -21,7 +21,7 @@ class StyledDialog extends StatelessWidget { Radius.circular(16 * scaleConfig.borderRadiusScale)), ), contentPadding: const EdgeInsets.all(4), - backgroundColor: scale.primaryScale.dialogBorder, + backgroundColor: scale.primaryScale.border, title: Text( title, style: textTheme.titleMedium! diff --git a/lib/theme/views/widget_helpers.dart b/lib/theme/views/widget_helpers.dart index 1b9e80f..f66af4b 100644 --- a/lib/theme/views/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -114,14 +114,13 @@ extension LabelExt on Widget { {ScaleColor? scale}) { final theme = Theme.of(context); final scaleScheme = theme.extension()!; - // final scaleConfig = theme.extension()!; scale = scale ?? scaleScheme.primaryScale; - return Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [ + return Wrap(crossAxisAlignment: WrapCrossAlignment.end, children: [ Text( '$label:', - style: theme.textTheme.titleLarge!.copyWith(color: scale.border), - ).paddingLTRB(0, 0, 8, 8), + style: theme.textTheme.bodyLarge!.copyWith(color: scale.hoverBorder), + ).paddingLTRB(0, 0, 8, 0), this ]); } @@ -431,6 +430,31 @@ Widget styledTitleContainer({ ])); } +Widget styledCard({ + required BuildContext context, + required Widget child, + Color? borderColor, + Color? backgroundColor, + Color? titleColor, +}) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final scaleConfig = theme.extension()!; + + return DecoratedBox( + decoration: ShapeDecoration( + color: backgroundColor ?? scale.primaryScale.elementBackground, + shape: RoundedRectangleBorder( + side: (scaleConfig.useVisualIndicators || scaleConfig.preferBorders) + ? BorderSide( + color: borderColor ?? scale.primaryScale.border, width: 2) + : BorderSide.none, + borderRadius: + BorderRadius.circular(12 * scaleConfig.borderRadiusScale), + )), + child: child.paddingAll(4)); +} + Widget styledBottomSheet({ required BuildContext context, required String title, @@ -500,6 +524,12 @@ const grayColorFilter = ColorFilter.matrix([ 0, ]); +const dodgeFilter = + ColorFilter.mode(Color.fromARGB(96, 255, 255, 255), BlendMode.srcIn); + +const overlayFilter = + ColorFilter.mode(Color.fromARGB(127, 255, 255, 255), BlendMode.dstIn); + Container clipBorder({ required bool clipEnabled, required bool borderEnabled, @@ -510,16 +540,17 @@ Container clipBorder({ // ignore: avoid_unnecessary_containers, use_decorated_box Container( decoration: ShapeDecoration( - color: borderColor, shape: RoundedRectangleBorder( - borderRadius: clipEnabled - ? BorderRadius.circular(borderRadius) - : BorderRadius.zero, - )), + side: borderEnabled && clipEnabled + ? BorderSide(color: borderColor, width: 2) + : BorderSide.none, + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius) + : BorderRadius.zero, + )), child: ClipRRect( - clipBehavior: Clip.hardEdge, - borderRadius: clipEnabled - ? BorderRadius.circular(borderRadius) - : BorderRadius.zero, - child: child) - .paddingAll(clipEnabled && borderEnabled ? 2 : 0)); + clipBehavior: Clip.antiAliasWithSaveLayer, + borderRadius: clipEnabled + ? BorderRadius.circular(borderRadius - 2) + : BorderRadius.zero, + child: child)); diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index f561f07..1899c34 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -216,7 +216,7 @@ class _DeveloperPageState extends State { onPressed: () async { final confirm = await showConfirmModal( context: context, - title: translate('toast.confirm'), + title: translate('confirmation.confirm'), text: translate('developer.are_you_sure_clear'), ); if (confirm && context.mounted) { @@ -224,7 +224,7 @@ class _DeveloperPageState extends State { } }), SizedBox.fromSize( - size: const Size(120, 48), + size: const Size(140, 48), child: CustomDropdown( items: _logLevelDropdownItems, initialItem: _logLevelDropdownItems diff --git a/pubspec.lock b/pubspec.lock index b98424d..349a58b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -96,6 +96,14 @@ packages: relative: true source: path version: "0.1.7" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" + source: hosted + version: "3.0.0" awesome_extensions: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1e0a526..94f8952 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: ansicolor: ^2.0.3 archive: ^4.0.4 async_tools: ^0.1.7 + auto_size_text: ^3.0.0 awesome_extensions: ^2.0.21 badges: ^3.1.2 basic_utils: ^5.8.2 @@ -158,14 +159,16 @@ flutter: - assets/i18n/en.json # Launcher icon - assets/launcher/icon.png - # Images - - assets/images/splash.svg + # Vector Images + - assets/images/grid.svg - assets/images/icon.svg + - assets/images/splash.svg - assets/images/title.svg - assets/images/vlogo.svg + # Raster Images - assets/images/ellet.png - - assets/images/toilet.png - assets/images/handshake.png + - assets/images/toilet.png # Printing - assets/js/pdf/3.2.146/pdf.min.js # Sounds diff --git a/build.bat b/update_generated_files.bat similarity index 100% rename from build.bat rename to update_generated_files.bat diff --git a/build.sh b/update_generated_files.sh similarity index 100% rename from build.sh rename to update_generated_files.sh