From 4f0243596496377398eca18b291a20b8fdf483be Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Fri, 12 Apr 2024 20:55:05 -0400 Subject: [PATCH] unify handling of themes accessible theming/high contrast support --- assets/i18n/en.json | 4 +- .../new_account_page/new_account_page.dart | 1 + lib/account_manager/views/profile_widget.dart | 7 +- lib/chat/views/chat_component.dart | 7 +- lib/chat/views/empty_chat_widget.dart | 55 +++--- lib/chat/views/new_chat_bottom_sheet.dart | 46 +---- .../chat_single_contact_item_widget.dart | 102 +++-------- .../chat_single_contact_list_widget.dart | 7 +- .../views/contact_invitation_display.dart | 1 + .../views/contact_invitation_item_widget.dart | 135 +++++--------- .../views/create_invitation_dialog.dart | 1 + .../views/invitation_dialog.dart | 1 + .../views/new_contact_bottom_sheet.dart | 7 +- .../views/scan_invitation_dialog.dart | 6 +- lib/contacts/views/contact_item_widget.dart | 132 +++++--------- lib/contacts/views/contact_list_widget.dart | 67 +++---- .../home_account_ready_main.dart | 6 +- .../home_account_ready_shell.dart | 2 +- .../main_pager/account_page.dart | 3 +- .../main_pager/main_pager.dart | 31 +--- lib/layout/home/home_no_active.dart | 2 +- lib/theme/models/chat_theme.dart | 54 ++++++ lib/theme/models/contrast_generator.dart | 83 +++++++++ lib/theme/models/models.dart | 2 + lib/theme/models/radix_generator.dart | 156 ++++------------- lib/theme/models/scale_color.dart | 75 +++++--- .../models/scale_input_decorator_theme.dart | 165 ++++++++++++++++++ lib/theme/models/scale_scheme.dart | 95 +++++++++- lib/theme/models/slider_tile.dart | 151 ++++++++++++++++ lib/theme/models/theme_preference.dart | 5 +- .../views}/scanner_error_widget.dart | 0 lib/{tools => theme/views}/styled_dialog.dart | 7 +- lib/theme/views/views.dart | 3 + .../views}/widget_helpers.dart | 48 +++-- lib/tools/enter_password.dart | 18 +- lib/tools/enter_pin.dart | 12 +- lib/tools/tools.dart | 3 - lib/veilid_processor/views/developer.dart | 56 ++++-- .../views/signal_strength_meter.dart | 20 +-- .../dht_short_array_cubit.dart | 2 +- .../dht_short_array/dht_short_array_head.dart | 2 + 41 files changed, 958 insertions(+), 622 deletions(-) create mode 100644 lib/theme/models/chat_theme.dart create mode 100644 lib/theme/models/contrast_generator.dart create mode 100644 lib/theme/models/scale_input_decorator_theme.dart create mode 100644 lib/theme/models/slider_tile.dart rename lib/{tools => theme/views}/scanner_error_widget.dart (100%) rename lib/{tools => theme/views}/styled_dialog.dart (90%) rename lib/{tools => theme/views}/widget_helpers.dart (85%) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index ed96192..2a1e4ac 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -6,7 +6,6 @@ "settings_tooltip": "Settings" }, "pager": { - "account": "Account", "chats": "Chats", "contacts": "Contacts" }, @@ -67,6 +66,9 @@ "add_chat_sheet": { "new_chat": "New Chat" }, + "chat": { + "say_something": "Say Something" + }, "create_invitation_dialog": { "title": "Create Contact Invitation", "connect_with_me": "Connect with me on VeilidChat!", diff --git a/lib/account_manager/views/new_account_page/new_account_page.dart b/lib/account_manager/views/new_account_page/new_account_page.dart index 53ba671..38664ba 100644 --- a/lib/account_manager/views/new_account_page/new_account_page.dart +++ b/lib/account_manager/views/new_account_page/new_account_page.dart @@ -7,6 +7,7 @@ import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:go_router/go_router.dart'; import '../../../layout/default_app_bar.dart'; +import '../../../theme/theme.dart'; import '../../../tools/tools.dart'; import '../../../veilid_processor/veilid_processor.dart'; import '../../account_manager.dart'; diff --git a/lib/account_manager/views/profile_widget.dart b/lib/account_manager/views/profile_widget.dart index 5d85014..ecb7c3d 100644 --- a/lib/account_manager/views/profile_widget.dart +++ b/lib/account_manager/views/profile_widget.dart @@ -31,11 +31,14 @@ class ProfileWidget extends StatelessWidget { child: Column(children: [ Text( _profile.name, - style: textTheme.headlineSmall, + style: textTheme.headlineSmall! + .copyWith(color: scale.primaryScale.borderText), textAlign: TextAlign.left, ).paddingAll(4), if (_profile.pronouns.isNotEmpty) - Text(_profile.pronouns, style: textTheme.bodyMedium) + Text(_profile.pronouns, + style: textTheme.bodyMedium! + .copyWith(color: scale.primaryScale.borderText)) .paddingLTRB(4, 0, 4, 4), ]), ); diff --git a/lib/chat/views/chat_component.dart b/lib/chat/views/chat_component.dart index 32210a0..35a2987 100644 --- a/lib/chat/views/chat_component.dart +++ b/lib/chat/views/chat_component.dart @@ -13,7 +13,6 @@ import '../../chat_list/chat_list.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import '../chat.dart'; class ChatComponent extends StatelessWidget { @@ -173,11 +172,13 @@ class ChatComponent extends StatelessWidget { 16, 0, 16, 0), child: Text(_remoteUser.firstName!, textAlign: TextAlign.start, - style: textTheme.titleMedium), + style: textTheme.titleMedium!.copyWith( + color: scale.primaryScale.borderText)), )), const Spacer(), IconButton( - icon: const Icon(Icons.close), + icon: Icon(Icons.close, + color: scale.primaryScale.borderText), onPressed: () async { context.read().setActiveChat(null); }).paddingLTRB(16, 0, 16, 0) diff --git a/lib/chat/views/empty_chat_widget.dart b/lib/chat/views/empty_chat_widget.dart index a9072cd..c975722 100644 --- a/lib/chat/views/empty_chat_widget.dart +++ b/lib/chat/views/empty_chat_widget.dart @@ -1,4 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_translate/flutter_translate.dart'; + +import '../../theme/theme.dart'; class EmptyChatWidget extends StatelessWidget { const EmptyChatWidget({super.key}); @@ -7,28 +10,32 @@ class EmptyChatWidget extends StatelessWidget { // ignore: prefer_expression_function_bodies Widget build( BuildContext context, - ) => - Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.chat, - color: Theme.of(context).disabledColor, - size: 48, - ), - Text( - 'Say Something', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).disabledColor, - ), - ), - ], - ), - ); + ) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.chat, + color: scale.primaryScale.subtleBorder, + size: 48, + ), + Text( + translate('chat.say_something'), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: scale.primaryScale.subtleBorder, + ), + ), + ], + ), + ); + } } diff --git a/lib/chat/views/new_chat_bottom_sheet.dart b/lib/chat/views/new_chat_bottom_sheet.dart index e900b6c..646a3ec 100644 --- a/lib/chat/views/new_chat_bottom_sheet.dart +++ b/lib/chat/views/new_chat_bottom_sheet.dart @@ -4,12 +4,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; Widget newChatBottomSheetBuilder( BuildContext sheetContext, BuildContext context) { - final theme = Theme.of(sheetContext); - final scale = theme.extension()!; + //final theme = Theme.of(sheetContext); + //final scale = theme.extension()!; return KeyboardListener( focusNode: FocusNode(), @@ -23,49 +22,10 @@ Widget newChatBottomSheetBuilder( title: translate('add_chat_sheet.new_chat'), child: SizedBox( height: 160, - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( 'Group and custom chat functionality is not available yet') - // Column(children: [ - // IconButton( - // onPressed: () async { - // Navigator.pop(sheetContext); - // await CreateInvitationDialog.show(context); - // }, - // iconSize: 64, - // icon: const Icon(Icons.contact_page), - // color: scale.primaryScale.background), - // Text( - // translate('accounts_menu.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.background), - // Text( - // translate('accounts_menu.scan_invite'), - // ) - // ]), - // Column(children: [ - // IconButton( - // onPressed: () async { - // Navigator.pop(sheetContext); - // await PasteInvitationDialog.show(context); - // }, - // iconSize: 64, - // icon: const Icon(Icons.paste), - // color: scale.primaryScale.background), - // Text( - // translate('accounts_menu.paste_invite'), - // ) - // ]) ]).paddingAll(16)))); } 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 241791c..75501b4 100644 --- a/lib/chat_list/views/chat_single_contact_item_widget.dart +++ b/lib/chat_list/views/chat_single_contact_item_widget.dart @@ -1,8 +1,6 @@ import 'package:async_tools/async_tools.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../chat/cubits/active_chat_cubit.dart'; import '../../proto/proto.dart' as proto; @@ -25,85 +23,35 @@ class ChatSingleContactItemWidget extends StatelessWidget { Widget build( BuildContext context, ) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final activeChatCubit = context.watch(); final remoteConversationRecordKey = _contact.remoteConversationRecordKey.toVeilid(); final selected = activeChatCubit.state == remoteConversationRecordKey; - return Container( - margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: selected - ? scale.primaryScale.activeElementBackground - : scale.primaryScale.hoverElementBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - )), - child: Slidable( - key: ObjectKey(_contact), - endActionPane: ActionPane( - motion: const DrawerMotion(), - children: [ - SlidableAction( - onPressed: _disabled - ? null - : (context) async { - final chatListCubit = context.read(); - await chatListCubit.deleteChat( - remoteConversationRecordKey: - remoteConversationRecordKey); - }, - backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.foregroundText, - icon: Icons.delete, - label: translate('button.delete'), - padding: const EdgeInsets.all(2)), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: scale.secondaryScale.background, - // foregroundColor: scale.secondaryScale.text, - // icon: Icons.edit, - // label: 'Edit', - // ), - ], - ), - - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - onTap: _disabled - ? null - : () { - singleFuture(activeChatCubit, () async { - activeChatCubit - .setActiveChat(remoteConversationRecordKey); - }); - }, - title: Text(_contact.editedProfile.name), - - /// xxx show last message here - subtitle: (_contact.editedProfile.pronouns.isNotEmpty) - ? Text(_contact.editedProfile.pronouns) - : null, - iconColor: selected - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - textColor: selected - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - selectedColor: scale.primaryScale.appText, - //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), - leading: const Icon(Icons.chat)))); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', _contact)); + return SliderTile( + key: ObjectKey(_contact), + disabled: _disabled, + selected: selected, + tileScale: ScaleKind.secondary, + title: _contact.editedProfile.name, + subtitle: _contact.editedProfile.pronouns, + icon: Icons.chat, + onTap: () { + singleFuture(activeChatCubit, () async { + activeChatCubit.setActiveChat(remoteConversationRecordKey); + }); + }, + endActions: [ + SliderTileAction( + icon: Icons.delete, + label: translate('button.delete'), + actionScale: ScaleKind.tertiary, + onPressed: (context) async { + final chatListCubit = context.read(); + await chatListCubit.deleteChat( + remoteConversationRecordKey: remoteConversationRecordKey); + }) + ], + ); } } diff --git a/lib/chat_list/views/chat_single_contact_list_widget.dart b/lib/chat_list/views/chat_single_contact_list_widget.dart index 0e212dd..c1f54d8 100644 --- a/lib/chat_list/views/chat_single_contact_list_widget.dart +++ b/lib/chat_list/views/chat_single_contact_list_widget.dart @@ -7,7 +7,7 @@ import 'package:searchable_listview/searchable_listview.dart'; import '../../contacts/contacts.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; +import '../../theme/theme.dart'; import '../chat_list.dart'; class ChatSingleContactListWidget extends StatelessWidget { @@ -41,8 +41,9 @@ class ChatSingleContactListWidget extends StatelessWidget { return const Text('...'); } return ChatSingleContactItemWidget( - contact: contact, - disabled: contactListV.busy); + contact: contact, + disabled: contactListV.busy) + .paddingLTRB(0, 4, 0, 0); }, filter: (value) { final lowerValue = value.toLowerCase(); diff --git a/lib/contact_invitation/views/contact_invitation_display.dart b/lib/contact_invitation/views/contact_invitation_display.dart index 020d510..374a309 100644 --- a/lib/contact_invitation/views/contact_invitation_display.dart +++ b/lib/contact_invitation/views/contact_invitation_display.dart @@ -10,6 +10,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:veilid_support/veilid_support.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; diff --git a/lib/contact_invitation/views/contact_invitation_item_widget.dart b/lib/contact_invitation/views/contact_invitation_item_widget.dart index fd00c0a..fcf021f 100644 --- a/lib/contact_invitation/views/contact_invitation_item_widget.dart +++ b/lib/contact_invitation/views/contact_invitation_item_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../proto/proto.dart' as proto; import '../../theme/theme.dart'; @@ -28,99 +27,51 @@ class ContactInvitationItemWidget extends StatelessWidget { @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; + // final remoteConversationKey = + // contact.remoteConversationRecordKey.toVeilid(); - return Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: scale.tertiaryScale.subtleBorder, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - )), - child: Slidable( - // Specify a key if the Slidable is dismissible. - key: ObjectKey(contactInvitationRecord), - endActionPane: ActionPane( - // A motion is a widget used to control how the pane animates. - motion: const DrawerMotion(), + const selected = + false; // xxx: eventually when we have selectable invitations: + // activeContactCubit.state == remoteConversationRecordKey; - // A pane can dismiss the Slidable. - //dismissible: DismissiblePane(onDismissed: () {}), + final tileDisabled = + disabled || context.watch().isBusy; - // All actions are defined in the children parameter. - children: [ - // A SlidableAction can have an icon and/or a label. - SlidableAction( - onPressed: disabled - ? null - : (context) async { - final contactInvitationListCubit = - context.read(); - await contactInvitationListCubit.deleteInvitation( - accepted: false, - contactRequestInboxRecordKey: - contactInvitationRecord - .contactRequestInbox.recordKey - .toVeilid()); - }, - backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.appText, - icon: Icons.delete, - label: translate('button.delete'), - padding: const EdgeInsets.all(2)), - ], - ), - - // startActionPane: ActionPane( - // motion: const DrawerMotion(), - // children: [ - // SlidableAction( - // // An action can be bigger than the others. - // flex: 2, - // onPressed: (context) => (), - // backgroundColor: Color(0xFF7BC043), - // foregroundColor: Colors.white, - // icon: Icons.archive, - // label: 'Archive', - // ), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: Color(0xFF0392CF), - // foregroundColor: Colors.white, - // icon: Icons.save, - // label: 'Save', - // ), - // ], - // ), - - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - //title: Text(translate('contact_list.invitation')), - onTap: disabled - ? null - : () async { - if (!context.mounted) { - return; - } - await ContactInvitationDisplayDialog.show( - context: context, - message: contactInvitationRecord.message, - create: (context) => InvitationGeneratorCubit.value( - Uint8List.fromList( - contactInvitationRecord.invitation))); - }, - title: Text( - contactInvitationRecord.message.isEmpty - ? translate('contact_list.invitation') - : contactInvitationRecord.message, - softWrap: true, - ), - iconColor: scale.tertiaryScale.background, - textColor: scale.tertiaryScale.appText, - //Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), - leading: const Icon(Icons.person_add)))); + return SliderTile( + key: ObjectKey(contactInvitationRecord), + disabled: tileDisabled, + selected: selected, + tileScale: ScaleKind.primary, + title: contactInvitationRecord.message.isEmpty + ? translate('contact_list.invitation') + : contactInvitationRecord.message, + icon: Icons.person_add, + onTap: () async { + if (!context.mounted) { + return; + } + await ContactInvitationDisplayDialog.show( + context: context, + message: contactInvitationRecord.message, + create: (context) => InvitationGeneratorCubit.value( + Uint8List.fromList(contactInvitationRecord.invitation))); + }, + endActions: [ + SliderTileAction( + icon: Icons.delete, + label: translate('button.delete'), + actionScale: ScaleKind.tertiary, + onPressed: (context) async { + final contactInvitationListCubit = + context.read(); + await contactInvitationListCubit.deleteInvitation( + accepted: false, + contactRequestInboxRecordKey: contactInvitationRecord + .contactRequestInbox.recordKey + .toVeilid()); + }, + ) + ], + ); } } diff --git a/lib/contact_invitation/views/create_invitation_dialog.dart b/lib/contact_invitation/views/create_invitation_dialog.dart index 8365744..ace71d5 100644 --- a/lib/contact_invitation/views/create_invitation_dialog.dart +++ b/lib/contact_invitation/views/create_invitation_dialog.dart @@ -10,6 +10,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; diff --git a/lib/contact_invitation/views/invitation_dialog.dart b/lib/contact_invitation/views/invitation_dialog.dart index 4d304ab..60d8784 100644 --- a/lib/contact_invitation/views/invitation_dialog.dart +++ b/lib/contact_invitation/views/invitation_dialog.dart @@ -9,6 +9,7 @@ import 'package:veilid_support/veilid_support.dart'; import '../../account_manager/account_manager.dart'; import '../../contacts/contacts.dart'; +import '../../theme/theme.dart'; import '../../tools/tools.dart'; import '../contact_invitation.dart'; diff --git a/lib/contact_invitation/views/new_contact_bottom_sheet.dart b/lib/contact_invitation/views/new_contact_bottom_sheet.dart index 32bd725..a79a07f 100644 --- a/lib/contact_invitation/views/new_contact_bottom_sheet.dart +++ b/lib/contact_invitation/views/new_contact_bottom_sheet.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../theme/theme.dart'; -import '../../tools/tools.dart'; import 'create_invitation_dialog.dart'; import 'paste_invitation_dialog.dart'; import 'scan_invitation_dialog.dart'; @@ -37,7 +36,7 @@ Widget newContactBottomSheetBuilder( }, iconSize: 64, icon: const Icon(Icons.contact_page), - color: scale.primaryScale.background), + color: scale.primaryScale.hoverBorder), Text( translate('add_contact_sheet.create_invite'), ) @@ -50,7 +49,7 @@ Widget newContactBottomSheetBuilder( }, iconSize: 64, icon: const Icon(Icons.qr_code_scanner), - color: scale.primaryScale.background), + color: scale.primaryScale.hoverBorder), Text( translate('add_contact_sheet.scan_invite'), ) @@ -63,7 +62,7 @@ Widget newContactBottomSheetBuilder( }, iconSize: 64, icon: const Icon(Icons.paste), - color: scale.primaryScale.background), + color: scale.primaryScale.hoverBorder), Text( translate('add_contact_sheet.paste_invite'), ) diff --git a/lib/contact_invitation/views/scan_invitation_dialog.dart b/lib/contact_invitation/views/scan_invitation_dialog.dart index 03f6101..44bb32e 100644 --- a/lib/contact_invitation/views/scan_invitation_dialog.dart +++ b/lib/contact_invitation/views/scan_invitation_dialog.dart @@ -211,7 +211,7 @@ class ScanInvitationDialogState extends State { scale.grayScale.subtleBackground); case TorchState.on: return Icon(Icons.flash_on, - color: scale.primaryScale.background); + color: scale.primaryScale.primary); } }, ), @@ -258,8 +258,8 @@ class ScanInvitationDialogState extends State { alignment: Alignment.topRight, child: IconButton( color: Colors.white, - icon: Icon(Icons.close, - color: scale.grayScale.background), + icon: + Icon(Icons.close, color: scale.grayScale.primary), iconSize: 32, onPressed: () => { SchedulerBinding.instance diff --git a/lib/contacts/views/contact_item_widget.dart b/lib/contacts/views/contact_item_widget.dart index 9016212..dfe9e6e 100644 --- a/lib/contacts/views/contact_item_widget.dart +++ b/lib/contacts/views/contact_item_widget.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_translate/flutter_translate.dart'; import '../../chat_list/chat_list.dart'; import '../../layout/layout.dart'; @@ -17,106 +16,65 @@ class ContactItemWidget extends StatelessWidget { final proto.Contact contact; final bool disabled; + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('contact', contact)) + ..add(DiagnosticsProperty('disabled', disabled)); + } + @override // ignore: prefer_expression_function_bodies Widget build( BuildContext context, ) { - final theme = Theme.of(context); - //final textTheme = theme.textTheme; - final scale = theme.extension()!; - final remoteConversationKey = contact.remoteConversationRecordKey.toVeilid(); const selected = false; // xxx: eventually when we have selectable contacts: // activeContactCubit.state == remoteConversationRecordKey; - return Container( - margin: const EdgeInsets.fromLTRB(0, 4, 0, 0), - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: selected - // ignore: dead_code - ? scale.primaryScale.activeElementBackground - : scale.primaryScale.hoverElementBackground, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - )), - child: Slidable( - key: ObjectKey(contact), - endActionPane: ActionPane( - motion: const DrawerMotion(), - children: [ - SlidableAction( - onPressed: disabled || context.watch().isBusy - ? null - : (context) async { - final contactListCubit = - context.read(); - final chatListCubit = context.read(); + final tileDisabled = disabled || context.watch().isBusy; - // Remove any chats for this contact - await chatListCubit.deleteChat( - remoteConversationRecordKey: - remoteConversationKey); + return SliderTile( + key: ObjectKey(contact), + disabled: tileDisabled, + selected: selected, + tileScale: ScaleKind.primary, + title: contact.editedProfile.name, + subtitle: contact.editedProfile.pronouns, + icon: Icons.person, + onTap: () async { + // Start a chat + final chatListCubit = context.read(); - // Delete the contact itself - await contactListCubit.deleteContact( - contact: contact); - }, - backgroundColor: scale.tertiaryScale.background, - foregroundColor: scale.tertiaryScale.appText, - icon: Icons.delete, - label: translate('button.delete'), - padding: const EdgeInsets.all(2)), - // SlidableAction( - // onPressed: (context) => (), - // backgroundColor: scale.secondaryScale.background, - // foregroundColor: scale.secondaryScale.text, - // icon: Icons.edit, - // label: 'Edit', - // ), - ], - ), + await chatListCubit.getOrCreateChatSingleContact( + remoteConversationRecordKey: remoteConversationKey); + // Click over to chats + if (context.mounted) { + await MainPager.of(context) + ?.pageController + .animateToPage(1, duration: 250.ms, curve: Curves.easeInOut); + } + }, + endActions: [ + SliderTileAction( + icon: Icons.delete, + label: translate('button.delete'), + actionScale: ScaleKind.tertiary, + onPressed: (context) async { + final contactListCubit = context.read(); + final chatListCubit = context.read(); - // The child of the Slidable is what the user sees when the - // component is not dragged. - child: ListTile( - onTap: disabled || context.watch().isBusy - ? null - : () async { - // Start a chat - final chatListCubit = context.read(); - await chatListCubit.getOrCreateChatSingleContact( - remoteConversationRecordKey: remoteConversationKey); - // Click over to chats - if (context.mounted) { - await MainPager.of(context) - ?.pageController - .animateToPage(1, - duration: 250.ms, curve: Curves.easeInOut); - } - }, - title: Text(contact.editedProfile.name), - subtitle: (contact.editedProfile.pronouns.isNotEmpty) - ? Text(contact.editedProfile.pronouns) - : null, - iconColor: selected - // ignore: dead_code - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - textColor: selected - // ignore: dead_code - ? scale.primaryScale.appText - : scale.primaryScale.subtleText, - selectedColor: scale.primaryScale.appText, - leading: const Icon(Icons.person)))); - } + // Remove any chats for this contact + await chatListCubit.deleteChat( + remoteConversationRecordKey: remoteConversationKey); - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('contact', contact)); + // Delete the contact itself + await contactListCubit.deleteContact(contact: contact); + }) + ], + ); } } diff --git a/lib/contacts/views/contact_list_widget.dart b/lib/contacts/views/contact_list_widget.dart index 7fc0a08..4c83a92 100644 --- a/lib/contacts/views/contact_list_widget.dart +++ b/lib/contacts/views/contact_list_widget.dart @@ -6,7 +6,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:searchable_listview/searchable_listview.dart'; import '../../proto/proto.dart' as proto; -import '../../tools/tools.dart'; +import '../../theme/theme.dart'; import 'contact_item_widget.dart'; import 'empty_contact_list_widget.dart'; @@ -25,33 +25,40 @@ class ContactListWidget extends StatelessWidget { } @override - Widget build(BuildContext context) => SizedBox.expand( - child: styledTitleContainer( - context: context, - title: translate('contact_list.title'), - child: SizedBox.expand( - child: (contactList.isEmpty) - ? const EmptyContactListWidget() - : SearchableList( - initialList: contactList.toList(), - builder: (l, i, c) => - ContactItemWidget(contact: c, disabled: disabled), - filter: (value) { - final lowerValue = value.toLowerCase(); - return contactList - .where((element) => - element.editedProfile.name - .toLowerCase() - .contains(lowerValue) || - element.editedProfile.pronouns - .toLowerCase() - .contains(lowerValue)) - .toList(); - }, - spaceBetweenSearchAndList: 4, - inputDecoration: InputDecoration( - labelText: translate('contact_list.search'), - ), - ).paddingAll(8), - ))).paddingLTRB(8, 0, 8, 8); + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return SizedBox.expand( + child: styledTitleContainer( + context: context, + title: translate('contact_list.title'), + child: SizedBox.expand( + child: (contactList.isEmpty) + ? const EmptyContactListWidget() + : SearchableList( + initialList: contactList.toList(), + builder: (l, i, c) => + ContactItemWidget(contact: c, disabled: disabled) + .paddingLTRB(0, 4, 0, 0), + filter: (value) { + final lowerValue = value.toLowerCase(); + return contactList + .where((element) => + element.editedProfile.name + .toLowerCase() + .contains(lowerValue) || + element.editedProfile.pronouns + .toLowerCase() + .contains(lowerValue)) + .toList(); + }, + spaceBetweenSearchAndList: 4, + defaultSuffixIconColor: scale.primaryScale.border, + inputDecoration: InputDecoration( + labelText: translate('contact_list.search'), + ), + ).paddingAll(8), + ))).paddingLTRB(8, 0, 8, 8); + } } diff --git a/lib/layout/home/home_account_ready/home_account_ready_main.dart b/lib/layout/home/home_account_ready/home_account_ready_main.dart index fd5790f..e6ed99e 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_main.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_main.dart @@ -37,11 +37,11 @@ class _HomeAccountReadyMainState extends State { Row(children: [ IconButton( icon: const Icon(Icons.settings), - color: scale.secondaryScale.appText, + color: scale.secondaryScale.borderText, constraints: const BoxConstraints.expand(height: 64, width: 64), style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(scale.secondaryScale.border), + backgroundColor: MaterialStateProperty.all( + scale.primaryScale.hoverBorder), shape: MaterialStateProperty.all( const RoundedRectangleBorder( borderRadius: diff --git a/lib/layout/home/home_account_ready/home_account_ready_shell.dart b/lib/layout/home/home_account_ready/home_account_ready_shell.dart index c508568..4ff44d2 100644 --- a/lib/layout/home/home_account_ready/home_account_ready_shell.dart +++ b/lib/layout/home/home_account_ready/home_account_ready_shell.dart @@ -11,7 +11,7 @@ import '../../../chat_list/chat_list.dart'; import '../../../contact_invitation/contact_invitation.dart'; import '../../../contacts/contacts.dart'; import '../../../router/router.dart'; -import '../../../tools/tools.dart'; +import '../../../theme/theme.dart'; class HomeAccountReadyShell extends StatefulWidget { factory HomeAccountReadyShell( diff --git a/lib/layout/home/home_account_ready/main_pager/account_page.dart b/lib/layout/home/home_account_ready/main_pager/account_page.dart index 3d0cfad..304d534 100644 --- a/lib/layout/home/home_account_ready/main_pager/account_page.dart +++ b/lib/layout/home/home_account_ready/main_pager/account_page.dart @@ -61,8 +61,9 @@ class AccountPageState extends State { translate('account_page.contact_invitations'), textAlign: TextAlign.center, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.subtleText), + .copyWith(color: scale.primaryScale.borderText), ), + iconColor: scale.primaryScale.borderText, initiallyExpanded: true, children: [ ContactInvitationListWidget( diff --git a/lib/layout/home/home_account_ready/main_pager/main_pager.dart b/lib/layout/home/home_account_ready/main_pager/main_pager.dart index 8c03eda..cdd6ac5 100644 --- a/lib/layout/home/home_account_ready/main_pager/main_pager.dart +++ b/lib/layout/home/home_account_ready/main_pager/main_pager.dart @@ -10,7 +10,6 @@ import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import '../../../../chat/chat.dart'; import '../../../../contact_invitation/contact_invitation.dart'; import '../../../../theme/theme.dart'; -import '../../../../tools/tools.dart'; import 'account_page.dart'; import 'bottom_sheet_action_button.dart'; import 'chats_page.dart'; @@ -41,7 +40,7 @@ class MainPagerState extends State with TickerProviderStateMixin { Icons.add_comment_sharp, ]; final _bottomLabelList = [ - translate('pager.account'), + translate('pager.contacts'), translate('pager.chats'), ]; @@ -82,12 +81,11 @@ class MainPagerState extends State with TickerProviderStateMixin { final scale = theme.extension()!; return BottomBarItem( title: Text(_bottomLabelList[index]), - icon: Icon(_selectedIconList[index], color: scale.primaryScale.appText), + icon: + Icon(_selectedIconList[index], color: scale.primaryScale.borderText), selectedIcon: - Icon(_selectedIconList[index], color: scale.primaryScale.appText), - backgroundColor: scale.primaryScale.appText, - //unSelectedColor: theme.colorScheme.primaryContainer, - //selectedColor: theme.colorScheme.primary, + Icon(_selectedIconList[index], color: scale.primaryScale.borderText), + backgroundColor: scale.primaryScale.borderText, //badge: const Text('9+'), //showBadge: true, ); @@ -169,21 +167,10 @@ class MainPagerState extends State with TickerProviderStateMixin { // ), bottomNavigationBar: StylishBottomBar( backgroundColor: scale.primaryScale.hoverBorder, - // gradient: LinearGradient( - // begin: Alignment.topCenter, - // end: Alignment.bottomCenter, - // colors: [ - // theme.colorScheme.primary, - // theme.colorScheme.primaryContainer, - // ]), - //borderRadius: BorderRadius.all(Radius.circular(16)), option: AnimatedBarOptions( - // iconSize: 32, - //barAnimation: BarAnimation.fade, - iconStyle: IconStyle.animated, inkEffect: true, - inkColor: scale.primaryScale.hoverBackground, - //opacity: 0.3, + inkColor: scale.primaryScale.hoverPrimary, + opacity: 0.3, ), items: _buildBottomBarItems(), hasNotch: true, @@ -198,11 +185,11 @@ class MainPagerState extends State with TickerProviderStateMixin { floatingActionButton: BottomSheetActionButton( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(14))), - foregroundColor: scale.secondaryScale.appText, + foregroundColor: scale.secondaryScale.borderText, backgroundColor: scale.secondaryScale.hoverBorder, builder: (context) => Icon( _fabIconList[_currentPage], - color: scale.secondaryScale.appText, + color: scale.secondaryScale.borderText, ), bottomSheetBuilder: (sheetContext) => _bottomSheetBuilder(sheetContext, context)), diff --git a/lib/layout/home/home_no_active.dart b/lib/layout/home/home_no_active.dart index e61fe0e..b2671dc 100644 --- a/lib/layout/home/home_no_active.dart +++ b/lib/layout/home/home_no_active.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../tools/tools.dart'; +import '../../theme/theme.dart'; class HomeNoActive extends StatefulWidget { const HomeNoActive({super.key}); diff --git a/lib/theme/models/chat_theme.dart b/lib/theme/models/chat_theme.dart new file mode 100644 index 0000000..b6ef7ba --- /dev/null +++ b/lib/theme/models/chat_theme.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; + +import 'scale_scheme.dart'; + +ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => + DefaultChatTheme( + primaryColor: scale.primaryScale.calloutBackground, + secondaryColor: scale.secondaryScale.calloutBackground, + backgroundColor: scale.grayScale.appBackground, + sendButtonIcon: Image.asset( + 'assets/icon-send.png', + color: scale.primaryScale.borderText, + package: 'flutter_chat_ui', + ), + inputBackgroundColor: Colors.blue, + inputBorderRadius: BorderRadius.zero, + inputTextDecoration: InputDecoration( + filled: true, + fillColor: scale.primaryScale.elementBackground, + isDense: true, + contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8))), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8))), + ), + inputContainerDecoration: + BoxDecoration(color: scale.primaryScale.border), + inputPadding: const EdgeInsets.all(9), + inputTextColor: scale.primaryScale.appText, + attachmentButtonIcon: const Icon(Icons.attach_file), + sentMessageBodyTextStyle: TextStyle( + color: scale.primaryScale.calloutText, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + sentEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + ), + receivedMessageBodyTextStyle: TextStyle( + color: scale.secondaryScale.calloutText, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + receivedEmojiMessageTextStyle: const TextStyle( + color: Colors.white, + fontSize: 64, + )); diff --git a/lib/theme/models/contrast_generator.dart b/lib/theme/models/contrast_generator.dart new file mode 100644 index 0000000..52b32c9 --- /dev/null +++ b/lib/theme/models/contrast_generator.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import 'radix_generator.dart'; +import 'scale_color.dart'; +import 'scale_input_decorator_theme.dart'; +import 'scale_scheme.dart'; + +ScaleScheme _contrastScale(Brightness brightness) { + final back = brightness == Brightness.light ? Colors.white : Colors.black; + final front = brightness == Brightness.light ? Colors.black : Colors.white; + + final primaryScale = ScaleColor( + appBackground: back, + subtleBackground: back, + elementBackground: back, + hoverElementBackground: back, + activeElementBackground: back, + subtleBorder: front, + border: front, + hoverBorder: front, + primary: back, + hoverPrimary: back, + subtleText: front, + appText: front, + primaryText: front, + borderText: back, + dialogBorder: front, + calloutBackground: front, + calloutText: back, + ); + + return ScaleScheme( + primaryScale: primaryScale, + primaryAlphaScale: primaryScale, + secondaryScale: primaryScale, + tertiaryScale: primaryScale, + grayScale: primaryScale, + errorScale: primaryScale); +} + +ThemeData contrastGenerator(Brightness brightness) { + final textTheme = makeRadixTextTheme(brightness); + final scaleScheme = _contrastScale(brightness); + final colorScheme = scaleScheme.toColorScheme(brightness); + final scaleConfig = ScaleConfig(useVisualIndicators: true); + + final themeData = ThemeData.from( + colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); + return themeData.copyWith( + bottomSheetTheme: themeData.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: themeData.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))), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: scaleScheme.primaryScale.appText, + selectionColor: scaleScheme.primaryScale.appText.withAlpha(0x7F), + selectionHandleColor: scaleScheme.primaryScale.appText), + inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme), + extensions: >[ + scaleScheme, + scaleConfig, + ]); +} diff --git a/lib/theme/models/models.dart b/lib/theme/models/models.dart index c22e8ab..e0ba490 100644 --- a/lib/theme/models/models.dart +++ b/lib/theme/models/models.dart @@ -1,4 +1,6 @@ +export 'chat_theme.dart'; export 'radix_generator.dart'; export 'scale_color.dart'; export 'scale_scheme.dart'; +export 'slider_tile.dart'; export 'theme_preference.dart'; diff --git a/lib/theme/models/radix_generator.dart b/lib/theme/models/radix_generator.dart index 415b628..b1f510a 100644 --- a/lib/theme/models/radix_generator.dart +++ b/lib/theme/models/radix_generator.dart @@ -1,16 +1,16 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:radix_colors/radix_colors.dart'; import '../../tools/tools.dart'; import 'scale_color.dart'; +import 'scale_input_decorator_theme.dart'; import 'scale_scheme.dart'; enum RadixThemeColor { - scarlet, // tomato + red + violet - babydoll, // crimson + purple + pink + scarlet, // red + violet + tomato + babydoll, // crimson + pink + purple vapor, // pink + cyan + plum gold, // yellow + amber + orange garden, // grass + orange + brown @@ -19,7 +19,7 @@ enum RadixThemeColor { lapis, // blue + indigo + mint eggplant, // violet + purple + indigo lime, // lime + yellow + orange - grim, // mauve + slate + sage + grim, // grey + purple + brown } enum _RadixBaseColor { @@ -282,11 +282,15 @@ extension ToScaleColor on RadixColor { subtleBorder: step6, border: step7, hoverBorder: step8, - background: step9, - hoverBackground: step10, + primary: step9, + hoverPrimary: step10, subtleText: step11, appText: step12, - foregroundText: scaleExtra.foregroundText, + primaryText: scaleExtra.foregroundText, + borderText: step12, + dialogBorder: step9, + calloutBackground: step9, + calloutText: scaleExtra.foregroundText, ); } @@ -338,28 +342,27 @@ class RadixScheme { RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { late RadixScheme radixScheme; switch (themeColor) { - // tomato + red + violet + // red + violet + tomato case RadixThemeColor.scarlet: radixScheme = RadixScheme( - primaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.tomato), + primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.red), primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.tomato), + _radixColorSteps(brightness, true, _RadixBaseColor.red), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.red), + _radixColorSteps(brightness, false, _RadixBaseColor.violet), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.violet), + _radixColorSteps(brightness, false, _RadixBaseColor.tomato), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), - grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.tomato), + grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.red), grayExtra: RadixScaleExtra(foregroundText: Colors.white), errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.yellow), errorExtra: RadixScaleExtra(foregroundText: Colors.black), ); - // crimson + purple + pink + // crimson + pink + purple case RadixThemeColor.babydoll: radixScheme = RadixScheme( primaryScale: @@ -369,10 +372,10 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { _radixColorSteps(brightness, true, _RadixBaseColor.crimson), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.purple), + _radixColorSteps(brightness, false, _RadixBaseColor.pink), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.pink), + _radixColorSteps(brightness, false, _RadixBaseColor.purple), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.crimson), @@ -546,13 +549,13 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { _radixGraySteps(brightness, false, _RadixBaseColor.tomato), primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaScale: - _radixColorSteps(brightness, true, _RadixBaseColor.tomato), + _radixGraySteps(brightness, true, _RadixBaseColor.tomato), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.indigo), + _radixColorSteps(brightness, false, _RadixBaseColor.purple), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryScale: - _radixColorSteps(brightness, false, _RadixBaseColor.teal), + _radixColorSteps(brightness, false, _RadixBaseColor.brown), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), grayScale: brightness == Brightness.dark ? RadixColors.dark.gray @@ -565,87 +568,7 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { return radixScheme; } -ColorScheme _scaleToColorScheme(Brightness brightness, ScaleScheme scale) => - ColorScheme( - brightness: brightness, - primary: scale.primaryScale.background, // reviewed - onPrimary: scale.primaryScale.foregroundText, // reviewed - primaryContainer: - Colors.red, // scale.primaryScale.hoverElementBackground, - onPrimaryContainer: Colors.green, //scale.primaryScale.subtleText, - secondary: scale.secondaryScale.background, - onSecondary: scale.secondaryScale.foregroundText, - secondaryContainer: scale.secondaryScale.hoverElementBackground, - onSecondaryContainer: scale.secondaryScale.subtleText, - tertiary: scale.tertiaryScale.background, - onTertiary: scale.tertiaryScale.foregroundText, - tertiaryContainer: scale.tertiaryScale.hoverElementBackground, - onTertiaryContainer: scale.tertiaryScale.subtleText, - error: scale.errorScale.background, - onError: scale.errorScale.foregroundText, - errorContainer: scale.errorScale.hoverElementBackground, - onErrorContainer: scale.errorScale.subtleText, - background: scale.grayScale.appBackground, // reviewed - onBackground: scale.grayScale.appText, // reviewed - surface: scale.primaryScale.background, // reviewed - onSurface: scale.primaryScale.foregroundText, // reviewed - surfaceVariant: scale.primaryScale.elementBackground, - onSurfaceVariant: - scale.primaryScale.foregroundText, // ?? reviewed a little - outline: scale.primaryScale.border, - outlineVariant: scale.primaryScale.subtleBorder, - shadow: const Color(0xFF000000), - scrim: scale.primaryScale.background, - inverseSurface: scale.primaryScale.subtleText, - onInverseSurface: scale.primaryScale.subtleBackground, - inversePrimary: scale.primaryScale.hoverBackground, - surfaceTint: scale.primaryAlphaScale.hoverElementBackground, - ); - -ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) => - DefaultChatTheme( - primaryColor: scale.primaryScale.background, - secondaryColor: scale.secondaryScale.background, - backgroundColor: scale.grayScale.appBackground, - inputBackgroundColor: Colors.blue, - inputBorderRadius: BorderRadius.zero, - inputTextDecoration: InputDecoration( - filled: true, - fillColor: scale.primaryScale.elementBackground, - isDense: true, - contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - border: const OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.all(Radius.circular(8))), - ), - inputContainerDecoration: - BoxDecoration(color: scale.primaryScale.border), - inputPadding: const EdgeInsets.all(9), - inputTextColor: scale.primaryScale.appText, - attachmentButtonIcon: const Icon(Icons.attach_file), - sentMessageBodyTextStyle: TextStyle( - color: scale.primaryScale.foregroundText, - decorationColor: Colors.red, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - sentEmojiMessageTextStyle: const TextStyle( - color: Colors.white, - fontSize: 64, - ), - receivedMessageBodyTextStyle: TextStyle( - color: scale.primaryScale.foregroundText, - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1.5, - ), - receivedEmojiMessageTextStyle: const TextStyle( - color: Colors.white, - fontSize: 64, - )); - -TextTheme _makeTextTheme(Brightness brightness) { +TextTheme makeRadixTextTheme(Brightness brightness) { late final TextTheme textTheme; if (Platform.isIOS) { textTheme = (brightness == Brightness.light) @@ -677,10 +600,11 @@ TextTheme _makeTextTheme(Brightness brightness) { } ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { - final textTheme = _makeTextTheme(brightness); + final textTheme = makeRadixTextTheme(brightness); final radix = _radixScheme(brightness, themeColor); final scaleScheme = radix.toScale(); - final colorScheme = _scaleToColorScheme(brightness, scaleScheme); + final colorScheme = scaleScheme.toColorScheme(brightness); + final scaleConfig = ScaleConfig(useVisualIndicators: false); final themeData = ThemeData.from( colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); @@ -697,34 +621,18 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { backgroundColor: scaleScheme.primaryScale.elementBackground, selectedColor: scaleScheme.primaryScale.activeElementBackground, surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, - checkmarkColor: scaleScheme.primaryScale.background, + checkmarkColor: scaleScheme.primaryScale.primary, side: BorderSide(color: scaleScheme.primaryScale.border)), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: scaleScheme.primaryScale.elementBackground, - foregroundColor: scaleScheme.primaryScale.appText, + foregroundColor: scaleScheme.primaryScale.primary, disabledBackgroundColor: scaleScheme.grayScale.elementBackground, - disabledForegroundColor: scaleScheme.grayScale.appText, + disabledForegroundColor: scaleScheme.grayScale.primary, shape: RoundedRectangleBorder( side: BorderSide(color: scaleScheme.primaryScale.border), borderRadius: BorderRadius.circular(8))), ), - focusColor: scaleScheme.primaryScale.activeElementBackground, - hoverColor: scaleScheme.primaryScale.hoverElementBackground, - inputDecorationTheme: themeData.inputDecorationTheme.copyWith( - border: OutlineInputBorder( - borderSide: BorderSide(color: scaleScheme.primaryScale.border), - borderRadius: BorderRadius.circular(8)), - 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))), - extensions: >[ - scaleScheme, - ]); + inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme), + extensions: >[scaleScheme, scaleConfig]); } diff --git a/lib/theme/models/scale_color.dart b/lib/theme/models/scale_color.dart index 8a14acc..244f6a3 100644 --- a/lib/theme/models/scale_color.dart +++ b/lib/theme/models/scale_color.dart @@ -10,11 +10,15 @@ class ScaleColor { required this.subtleBorder, required this.border, required this.hoverBorder, - required this.background, - required this.hoverBackground, + required this.primary, + required this.hoverPrimary, required this.subtleText, required this.appText, - required this.foregroundText, + required this.primaryText, + required this.borderText, + required this.dialogBorder, + required this.calloutBackground, + required this.calloutText, }); Color appBackground; @@ -25,11 +29,15 @@ class ScaleColor { Color subtleBorder; Color border; Color hoverBorder; - Color background; - Color hoverBackground; + Color primary; + Color hoverPrimary; Color subtleText; Color appText; - Color foregroundText; + Color primaryText; + Color borderText; + Color dialogBorder; + Color calloutBackground; + Color calloutText; ScaleColor copyWith({ Color? appBackground, @@ -45,24 +53,31 @@ class ScaleColor { Color? subtleText, Color? appText, Color? foregroundText, + Color? borderText, + Color? dialogBorder, + Color? calloutBackground, + Color? calloutText, }) => ScaleColor( - appBackground: appBackground ?? this.appBackground, - subtleBackground: subtleBackground ?? this.subtleBackground, - elementBackground: elementBackground ?? this.elementBackground, - hoverElementBackground: - hoverElementBackground ?? this.hoverElementBackground, - activeElementBackground: - activeElementBackground ?? this.activeElementBackground, - subtleBorder: subtleBorder ?? this.subtleBorder, - border: border ?? this.border, - hoverBorder: hoverBorder ?? this.hoverBorder, - background: background ?? this.background, - hoverBackground: hoverBackground ?? this.hoverBackground, - subtleText: subtleText ?? this.subtleText, - appText: appText ?? this.appText, - foregroundText: foregroundText ?? this.foregroundText, - ); + appBackground: appBackground ?? this.appBackground, + subtleBackground: subtleBackground ?? this.subtleBackground, + elementBackground: elementBackground ?? this.elementBackground, + hoverElementBackground: + hoverElementBackground ?? this.hoverElementBackground, + activeElementBackground: + activeElementBackground ?? this.activeElementBackground, + subtleBorder: subtleBorder ?? this.subtleBorder, + border: border ?? this.border, + hoverBorder: hoverBorder ?? this.hoverBorder, + primary: background ?? this.primary, + hoverPrimary: hoverBackground ?? this.hoverPrimary, + subtleText: subtleText ?? this.subtleText, + appText: appText ?? this.appText, + primaryText: foregroundText ?? this.primaryText, + borderText: borderText ?? this.borderText, + dialogBorder: dialogBorder ?? this.dialogBorder, + calloutBackground: calloutBackground ?? this.calloutBackground, + calloutText: calloutText ?? this.calloutText); // ignore: prefer_constructors_over_static_methods static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor( @@ -85,14 +100,22 @@ class ScaleColor { border: Color.lerp(a.border, b.border, t) ?? const Color(0x00000000), hoverBorder: Color.lerp(a.hoverBorder, b.hoverBorder, t) ?? const Color(0x00000000), - background: Color.lerp(a.background, b.background, t) ?? - const Color(0x00000000), - hoverBackground: Color.lerp(a.hoverBackground, b.hoverBackground, t) ?? + primary: Color.lerp(a.primary, b.primary, t) ?? const Color(0x00000000), + hoverPrimary: Color.lerp(a.hoverPrimary, b.hoverPrimary, t) ?? const Color(0x00000000), subtleText: Color.lerp(a.subtleText, b.subtleText, t) ?? const Color(0x00000000), appText: Color.lerp(a.appText, b.appText, t) ?? const Color(0x00000000), - foregroundText: Color.lerp(a.foregroundText, b.foregroundText, t) ?? + primaryText: Color.lerp(a.primaryText, b.primaryText, t) ?? + const Color(0x00000000), + borderText: Color.lerp(a.borderText, b.borderText, t) ?? + const Color(0x00000000), + dialogBorder: Color.lerp(a.dialogBorder, b.dialogBorder, t) ?? + const Color(0x00000000), + calloutBackground: + Color.lerp(a.calloutBackground, b.calloutBackground, t) ?? + const Color(0x00000000), + calloutText: Color.lerp(a.calloutText, b.calloutText, t) ?? const Color(0x00000000), ); } diff --git a/lib/theme/models/scale_input_decorator_theme.dart b/lib/theme/models/scale_input_decorator_theme.dart new file mode 100644 index 0000000..f6865cd --- /dev/null +++ b/lib/theme/models/scale_input_decorator_theme.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +import 'scale_scheme.dart'; + +class ScaleInputDecoratorTheme extends InputDecorationTheme { + ScaleInputDecoratorTheme(this._scaleScheme, this._textTheme) + : super( + border: OutlineInputBorder( + borderSide: BorderSide(color: _scaleScheme.primaryScale.border), + borderRadius: BorderRadius.circular(8)), + 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))); + + final ScaleScheme _scaleScheme; + final TextTheme _textTheme; + + @override + TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return TextStyle(color: _scaleScheme.grayScale.border); + } + return TextStyle(color: _scaleScheme.primaryScale.border); + }); + + @override + Color? get fillColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _scaleScheme.grayScale.primary.withOpacity(0.04); + } + return _scaleScheme.primaryScale.primary.withOpacity(0.04); + }); + + @override + BorderSide? get activeIndicatorBorder => + MaterialStateBorderSide.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return BorderSide( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _scaleScheme.errorScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.secondaryScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide( + color: _scaleScheme.secondaryScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.secondaryScale.subtleBorder); + }); + + @override + BorderSide? get outlineBorder => + MaterialStateBorderSide.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return BorderSide( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _scaleScheme.errorScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _scaleScheme.primaryScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _scaleScheme.primaryScale.border, width: 2); + } + return BorderSide(color: _scaleScheme.primaryScale.subtleBorder); + }); + + @override + Color? get iconColor => _scaleScheme.primaryScale.primary; + + @override + Color? get prefixIconColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _scaleScheme.primaryScale.primary.withAlpha(0x3F); + } + if (states.contains(MaterialState.error)) { + return _scaleScheme.errorScale.primary; + } + return _scaleScheme.primaryScale.primary; + }); + + @override + Color? get suffixIconColor => MaterialStateColor.resolveWith((states) { + if (states.contains(MaterialState.disabled)) { + return _scaleScheme.primaryScale.primary.withAlpha(0x3F); + } + if (states.contains(MaterialState.error)) { + return _scaleScheme.errorScale.primary; + } + return _scaleScheme.primaryScale.primary; + }); + + @override + TextStyle? get labelStyle => MaterialStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodyLarge ?? const TextStyle(); + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.errorScale.hoverBorder); + } + return textStyle.copyWith( + color: _scaleScheme.errorScale.subtleBorder); + } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith( + color: _scaleScheme.primaryScale.hoverBorder); + } + return textStyle.copyWith(color: _scaleScheme.primaryScale.border); + }); + + @override + TextStyle? get floatingLabelStyle => labelStyle; + + @override + TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodySmall ?? const TextStyle(); + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith( + color: _scaleScheme.grayScale.border.withAlpha(0x7F)); + } + return textStyle.copyWith( + color: _scaleScheme.secondaryScale.border.withAlpha(0x7F)); + }); + + @override + TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((states) { + final textStyle = _textTheme.bodySmall ?? const TextStyle(); + return textStyle.copyWith(color: _scaleScheme.errorScale.primary); + }); +} diff --git a/lib/theme/models/scale_scheme.dart b/lib/theme/models/scale_scheme.dart index 74e51bc..990fe1e 100644 --- a/lib/theme/models/scale_scheme.dart +++ b/lib/theme/models/scale_scheme.dart @@ -2,14 +2,17 @@ import 'package:flutter/material.dart'; import 'scale_color.dart'; +enum ScaleKind { primary, primaryAlpha, secondary, tertiary, gray, error } + class ScaleScheme extends ThemeExtension { - ScaleScheme( - {required this.primaryScale, - required this.primaryAlphaScale, - required this.secondaryScale, - required this.tertiaryScale, - required this.grayScale, - required this.errorScale}); + ScaleScheme({ + required this.primaryScale, + required this.primaryAlphaScale, + required this.secondaryScale, + required this.tertiaryScale, + required this.grayScale, + required this.errorScale, + }); final ScaleColor primaryScale; final ScaleColor primaryAlphaScale; @@ -18,6 +21,23 @@ class ScaleScheme extends ThemeExtension { final ScaleColor grayScale; final ScaleColor errorScale; + ScaleColor scale(ScaleKind kind) { + switch (kind) { + case ScaleKind.primary: + return primaryScale; + case ScaleKind.primaryAlpha: + return primaryAlphaScale; + case ScaleKind.secondary: + return secondaryScale; + case ScaleKind.tertiary: + return tertiaryScale; + case ScaleKind.gray: + return grayScale; + case ScaleKind.error: + return errorScale; + } + } + @override ScaleScheme copyWith( {ScaleColor? primaryScale, @@ -50,4 +70,65 @@ class ScaleScheme extends ThemeExtension { errorScale: ScaleColor.lerp(errorScale, other.errorScale, t), ); } + + ColorScheme toColorScheme(Brightness brightness) => ColorScheme( + brightness: brightness, + primary: primaryScale.primary, // reviewed + onPrimary: primaryScale.primaryText, // reviewed + // primaryContainer: primaryScale.hoverElementBackground, + // onPrimaryContainer: primaryScale.subtleText, + secondary: secondaryScale.primary, + onSecondary: secondaryScale.primaryText, + // secondaryContainer: secondaryScale.hoverElementBackground, + // onSecondaryContainer: secondaryScale.subtleText, + tertiary: tertiaryScale.primary, + onTertiary: tertiaryScale.primaryText, + // tertiaryContainer: tertiaryScale.hoverElementBackground, + // onTertiaryContainer: tertiaryScale.subtleText, + error: errorScale.primary, + onError: errorScale.primaryText, + // errorContainer: errorScale.hoverElementBackground, + // onErrorContainer: errorScale.subtleText, + background: grayScale.appBackground, // reviewed + onBackground: grayScale.appText, // reviewed + surface: primaryScale.primary, // reviewed + onSurface: primaryScale.primaryText, // reviewed + surfaceVariant: secondaryScale.primary, + onSurfaceVariant: secondaryScale.primaryText, // ?? reviewed a little + outline: primaryScale.border, + outlineVariant: secondaryScale.border, + shadow: const Color(0xFF000000), + //scrim: primaryScale.background, + // inverseSurface: primaryScale.subtleText, + // onInverseSurface: primaryScale.subtleBackground, + // inversePrimary: primaryScale.hoverBackground, + // surfaceTint: primaryAlphaScale.hoverElementBackground, + ); +} + +class ScaleConfig extends ThemeExtension { + ScaleConfig({ + required this.useVisualIndicators, + }); + + final bool useVisualIndicators; + + @override + ScaleConfig copyWith({ + bool? useVisualIndicators, + }) => + ScaleConfig( + useVisualIndicators: useVisualIndicators ?? this.useVisualIndicators, + ); + + @override + ScaleConfig lerp(ScaleConfig? other, double t) { + if (other is! ScaleConfig) { + return this; + } + return ScaleConfig( + useVisualIndicators: + t < .5 ? useVisualIndicators : other.useVisualIndicators, + ); + } } diff --git a/lib/theme/models/slider_tile.dart b/lib/theme/models/slider_tile.dart new file mode 100644 index 0000000..251581e --- /dev/null +++ b/lib/theme/models/slider_tile.dart @@ -0,0 +1,151 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; + +import '../theme.dart'; + +class SliderTileAction { + const SliderTileAction({ + required this.actionScale, + required this.onPressed, + this.key, + this.icon, + this.label, + }); + + final Key? key; + final ScaleKind actionScale; + final String? label; + final IconData? icon; + final SlidableActionCallback? onPressed; +} + +class SliderTile extends StatelessWidget { + const SliderTile( + {required this.disabled, + required this.selected, + required this.tileScale, + required this.title, + this.subtitle = '', + this.endActions = const [], + this.startActions = const [], + this.onTap, + this.icon, + super.key}); + + final bool disabled; + final bool selected; + final ScaleKind tileScale; + final List endActions; + final List startActions; + final GestureTapCallback? onTap; + final IconData? icon; + final String title; + final String subtitle; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('disabled', disabled)) + ..add(DiagnosticsProperty('selected', selected)) + ..add(DiagnosticsProperty('tileScale', tileScale)) + ..add(IterableProperty('endActions', endActions)) + ..add(IterableProperty('startActions', startActions)) + ..add(ObjectFlagProperty.has('onTap', onTap)) + ..add(DiagnosticsProperty('icon', icon)) + ..add(StringProperty('title', title)) + ..add(StringProperty('subtitle', subtitle)); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final tileColor = scale.scale(!disabled ? tileScale : ScaleKind.gray); + final scalecfg = theme.extension()!; + + final borderColor = selected ? tileColor.hoverBorder : tileColor.border; + final backgroundColor = scalecfg.useVisualIndicators && !selected + ? tileColor.borderText + : borderColor; + final textColor = scalecfg.useVisualIndicators && !selected + ? borderColor + : tileColor.borderText; + + return Container( + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + color: backgroundColor, + shape: RoundedRectangleBorder( + side: scalecfg.useVisualIndicators + ? BorderSide(width: 2, color: borderColor, strokeAlign: 0) + : BorderSide.none, + borderRadius: BorderRadius.circular(8), + )), + child: Slidable( + // Specify a key if the Slidable is dismissible. + key: key, + endActionPane: endActions.isEmpty + ? null + : ActionPane( + motion: const DrawerMotion(), + children: endActions + .map( + (a) => SlidableAction( + onPressed: disabled ? null : a.onPressed, + backgroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.borderText + : tileColor.border) + : scale.scale(a.actionScale).primary, + foregroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.border + : tileColor.borderText) + : scale.scale(a.actionScale).primaryText, + icon: a.icon, + label: a.label, + padding: const EdgeInsets.all(2)), + ) + .toList()), + startActionPane: startActions.isEmpty + ? null + : ActionPane( + motion: const DrawerMotion(), + children: startActions + .map( + (a) => SlidableAction( + onPressed: disabled ? null : a.onPressed, + backgroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.borderText + : tileColor.border) + : scale.scale(a.actionScale).primary, + foregroundColor: scalecfg.useVisualIndicators + ? (selected + ? tileColor.border + : tileColor.borderText) + : scale.scale(a.actionScale).primaryText, + icon: a.icon, + label: a.label, + padding: const EdgeInsets.all(2)), + ) + .toList()), + child: Padding( + padding: scalecfg.useVisualIndicators + ? EdgeInsets.zero + : const EdgeInsets.fromLTRB(0, 2, 0, 2), + child: ListTile( + onTap: onTap, + title: Text( + title, + softWrap: true, + ), + subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, + iconColor: textColor, + textColor: textColor, + leading: icon == null ? null : Icon(icon))))); + } +} diff --git a/lib/theme/models/theme_preference.dart b/lib/theme/models/theme_preference.dart index 74c90d8..334bbba 100644 --- a/lib/theme/models/theme_preference.dart +++ b/lib/theme/models/theme_preference.dart @@ -2,7 +2,8 @@ import 'package:change_case/change_case.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../tools/tools.dart'; +import '../views/widget_helpers.dart'; +import 'contrast_generator.dart'; import 'radix_generator.dart'; part 'theme_preference.freezed.dart'; @@ -83,7 +84,7 @@ extension ThemePreferencesExt on ThemePreferences { // Special cases case ColorPreference.contrast: // xxx do contrastGenerator - themeData = radixGenerator(brightness, RadixThemeColor.grim); + themeData = contrastGenerator(brightness); // Generate from Radix case ColorPreference.scarlet: themeData = radixGenerator(brightness, RadixThemeColor.scarlet); diff --git a/lib/tools/scanner_error_widget.dart b/lib/theme/views/scanner_error_widget.dart similarity index 100% rename from lib/tools/scanner_error_widget.dart rename to lib/theme/views/scanner_error_widget.dart diff --git a/lib/tools/styled_dialog.dart b/lib/theme/views/styled_dialog.dart similarity index 90% rename from lib/tools/styled_dialog.dart rename to lib/theme/views/styled_dialog.dart index 25c3c0a..0fd079c 100644 --- a/lib/tools/styled_dialog.dart +++ b/lib/theme/views/styled_dialog.dart @@ -2,7 +2,7 @@ import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../theme/theme.dart'; +import '../theme.dart'; class StyledDialog extends StatelessWidget { const StyledDialog({required this.title, required this.child, super.key}); @@ -19,10 +19,11 @@ class StyledDialog extends StatelessWidget { borderRadius: BorderRadius.all(Radius.circular(16)), ), contentPadding: const EdgeInsets.all(4), - backgroundColor: scale.primaryScale.border, + backgroundColor: scale.primaryScale.dialogBorder, title: Text( title, - style: textTheme.titleMedium, + style: textTheme.titleMedium! + .copyWith(color: scale.primaryScale.borderText), textAlign: TextAlign.center, ), titlePadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), diff --git a/lib/theme/views/views.dart b/lib/theme/views/views.dart index 85d06c4..6f6d7ac 100644 --- a/lib/theme/views/views.dart +++ b/lib/theme/views/views.dart @@ -1,2 +1,5 @@ export 'brightness_preferences.dart'; export 'color_preferences.dart'; +export 'scanner_error_widget.dart'; +export 'styled_dialog.dart'; +export 'widget_helpers.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/theme/views/widget_helpers.dart similarity index 85% rename from lib/tools/widget_helpers.dart rename to lib/theme/views/widget_helpers.dart index 3d8be63..7cef561 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/theme/views/widget_helpers.dart @@ -9,7 +9,7 @@ import 'package:flutter_translate/flutter_translate.dart'; import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; -import '../theme/theme.dart'; +import '../theme.dart'; extension BorderExt on Widget { DecoratedBox debugBorder() => DecoratedBox( @@ -35,19 +35,22 @@ Widget buildProgressIndicator() => Builder(builder: (context) { final theme = Theme.of(context); final scale = theme.extension()!; return SpinKitFoldingCube( - color: scale.tertiaryScale.background, + color: scale.tertiaryScale.primary, size: 80, ); }); -Widget waitingPage({String? text}) => Builder( - builder: (context) => ColoredBox( - color: Theme.of(context).scaffoldBackgroundColor, - child: Center( - child: Column(children: [ - buildProgressIndicator().expanded(), - if (text != null) Text(text) - ])))); +Widget waitingPage({String? text}) => Builder(builder: (context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + return ColoredBox( + color: scale.tertiaryScale.primaryText, + child: Center( + child: Column(children: [ + buildProgressIndicator().expanded(), + if (text != null) Text(text) + ]))); + }); Widget debugPage(String text) => Builder( builder: (context) => ColoredBox( @@ -132,12 +135,30 @@ void showInfoToast(BuildContext context, String message) { ).show(context); } +// Widget insetBorder( +// {required BuildContext context, +// required bool enabled, +// required Color color, +// required Widget child}) { +// if (!enabled) { +// return child; +// } + +// return Stack({ +// children: [] { +// DecoratedBox(decoration: BoxDecoration() +// child, +// } +// }) +// } + Widget styledTitleContainer({ required BuildContext context, required String title, required Widget child, Color? borderColor, Color? backgroundColor, + Color? titleColor, }) { final theme = Theme.of(context); final scale = theme.extension()!; @@ -153,7 +174,7 @@ Widget styledTitleContainer({ Text( title, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.subtleText), + .copyWith(color: titleColor ?? scale.primaryScale.borderText), ).paddingLTRB(8, 8, 8, 4), DecoratedBox( decoration: ShapeDecoration( @@ -174,6 +195,7 @@ Widget styledBottomSheet({ required Widget child, Color? borderColor, Color? backgroundColor, + Color? titleColor, }) { final theme = Theme.of(context); final scale = theme.extension()!; @@ -181,7 +203,7 @@ Widget styledBottomSheet({ return DecoratedBox( decoration: ShapeDecoration( - color: borderColor ?? scale.primaryScale.border, + color: borderColor ?? scale.primaryScale.dialogBorder, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), @@ -190,7 +212,7 @@ Widget styledBottomSheet({ Text( title, style: textTheme.titleMedium! - .copyWith(color: scale.primaryScale.subtleText), + .copyWith(color: titleColor ?? scale.primaryScale.borderText), ).paddingLTRB(8, 8, 8, 4), DecoratedBox( decoration: ShapeDecoration( diff --git a/lib/tools/enter_password.dart b/lib/tools/enter_password.dart index ca1bd73..2240278 100644 --- a/lib/tools/enter_password.dart +++ b/lib/tools/enter_password.dart @@ -52,27 +52,23 @@ class _EnterPasswordDialogState extends State { final theme = Theme.of(context); final scale = theme.extension()!; - return Dialog( - backgroundColor: scale.grayScale.subtleBackground, + return StyledDialog( + title: widget.matchPass == null + ? translate('enter_password_dialog.enter_password') + : translate('enter_password_dialog.reenter_password'), child: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - widget.matchPass == null - ? translate('enter_password_dialog.enter_password') - : translate('enter_password_dialog.reenter_password'), - style: theme.textTheme.titleLarge, - ).paddingAll(16), TextField( controller: passwordController, focusNode: focusNode, autofocus: true, enableSuggestions: false, - obscureText: - !_passwordVisible, //This will obscure text dynamically + obscureText: !_passwordVisible, + obscuringCharacter: '*', inputFormatters: [ FilteringTextInputFormatter.singleLineFormatter ], @@ -87,7 +83,7 @@ class _EnterPasswordDialogState extends State { ? null : Icon(Icons.check_circle, color: passwordController.text == widget.matchPass - ? scale.primaryScale.background + ? scale.primaryScale.primary : scale.grayScale.subtleBackground), suffixIcon: IconButton( icon: Icon( diff --git a/lib/tools/enter_pin.dart b/lib/tools/enter_pin.dart index 7166126..d0a21ec 100644 --- a/lib/tools/enter_pin.dart +++ b/lib/tools/enter_pin.dart @@ -67,20 +67,16 @@ class _EnterPinDialogState extends State { ); /// Optionally you can use form to validate the Pinput - return Dialog( - backgroundColor: scale.grayScale.subtleBackground, + return StyledDialog( + title: !widget.reenter + ? translate('enter_pin_dialog.enter_pin') + : translate('enter_pin_dialog.reenter_pin'), child: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - !widget.reenter - ? translate('enter_pin_dialog.enter_pin') - : translate('enter_pin_dialog.reenter_pin'), - style: theme.textTheme.titleLarge, - ).paddingAll(16), Directionality( // Specify direction if desired textDirection: TextDirection.ltr, diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index a5cefcf..c556f98 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -5,10 +5,7 @@ export 'loggy.dart'; export 'phono_byte.dart'; export 'pop_control.dart'; export 'responsive.dart'; -export 'scanner_error_widget.dart'; export 'shared_preferences.dart'; export 'state_logger.dart'; export 'stream_listenable.dart'; -export 'styled_dialog.dart'; -export 'widget_helpers.dart'; export 'window_control.dart'; diff --git a/lib/veilid_processor/views/developer.dart b/lib/veilid_processor/views/developer.dart index 4e70ff9..8ca539a 100644 --- a/lib/veilid_processor/views/developer.dart +++ b/lib/veilid_processor/views/developer.dart @@ -140,17 +140,18 @@ class _DeveloperPageState extends State { // }); return Scaffold( + backgroundColor: scale.primaryScale.primary, appBar: DefaultAppBar( title: Text(translate('developer.title')), leading: IconButton( - icon: Icon(Icons.arrow_back, color: scale.primaryScale.appText), + icon: Icon(Icons.arrow_back, color: scale.primaryScale.primaryText), onPressed: () => GoRouterHelper(context).pop(), ), actions: [ IconButton( icon: const Icon(Icons.copy), - color: scale.primaryScale.appText, - disabledColor: scale.grayScale.subtleText, + color: scale.primaryScale.primaryText, + disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), onPressed: _terminalController.selection == null ? null : () async { @@ -158,17 +159,22 @@ class _DeveloperPageState extends State { }), IconButton( icon: const Icon(Icons.clear_all), - color: scale.primaryScale.appText, - disabledColor: scale.grayScale.subtleText, + color: scale.primaryScale.primaryText, + disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F), onPressed: () async { await QuickAlert.show( context: context, type: QuickAlertType.confirm, title: translate('developer.are_you_sure_clear'), - textColor: scale.primaryScale.appText, - confirmBtnColor: scale.primaryScale.elementBackground, - backgroundColor: scale.primaryScale.subtleBackground, - headerBackgroundColor: scale.primaryScale.background, + titleColor: scale.primaryScale.appText, + textColor: scale.primaryScale.subtleText, + confirmBtnColor: scale.primaryScale.primary, + cancelBtnTextStyle: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 18, + color: scale.primaryScale.appText), + backgroundColor: scale.primaryScale.appBackground, + headerBackgroundColor: scale.primaryScale.primary, confirmBtnText: translate('button.ok'), cancelBtnText: translate('button.cancel'), onConfirmBtnTap: () async { @@ -194,13 +200,23 @@ class _DeveloperPageState extends State { width: 64, height: 40, render: ResultRender.icon, + icon: SizedBox( + width: 10, + height: 10, + child: CustomPaint( + painter: DropdownArrowPainter( + color: scale.primaryScale.primaryText))), textStyle: textTheme.labelMedium! - .copyWith(color: scale.primaryScale.appText), + .copyWith(color: scale.primaryScale.primaryText), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), openBoxDecoration: BoxDecoration( - color: scale.primaryScale.activeElementBackground), - boxDecoration: - BoxDecoration(color: scale.primaryScale.elementBackground), + color: scale.primaryScale.border, + borderRadius: BorderRadius.circular(8), + ), + boxDecoration: BoxDecoration( + color: scale.primaryScale.hoverBorder, + borderRadius: BorderRadius.circular(8), + ), ), dropdownOptions: DropdownOptions( width: 160, @@ -224,7 +240,7 @@ class _DeveloperPageState extends State { padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)), dropdownList: _logLevelDropdownItems, - ) + ).paddingLTRB(0, 0, 8, 0) ], ), body: SafeArea( @@ -245,13 +261,19 @@ class _DeveloperPageState extends State { decoration: InputDecoration( filled: true, contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), - border: OutlineInputBorder( + enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: scale.primaryScale.border)), + borderSide: BorderSide.none), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), fillColor: scale.primaryScale.subtleBackground, hintText: translate('developer.command'), suffixIcon: IconButton( - icon: const Icon(Icons.send), + icon: Icon(Icons.send, + color: _debugCommandController.text.isEmpty + ? scale.primaryScale.primary.withAlpha(0x3F) + : scale.primaryScale.primary), onPressed: _debugCommandController.text.isEmpty ? null : () async { diff --git a/lib/veilid_processor/views/signal_strength_meter.dart b/lib/veilid_processor/views/signal_strength_meter.dart index eaf5022..4691e87 100644 --- a/lib/veilid_processor/views/signal_strength_meter.dart +++ b/lib/veilid_processor/views/signal_strength_meter.dart @@ -33,32 +33,32 @@ class SignalStrengthMeterWidget extends StatelessWidget { switch (connectionState.attachment.state) { case AttachmentState.detached: iconWidget = Icon(Icons.signal_cellular_nodata, - size: iconSize, color: scale.grayScale.appText); + size: iconSize, color: scale.primaryScale.primaryText); return; case AttachmentState.detaching: iconWidget = Icon(Icons.signal_cellular_off, - size: iconSize, color: scale.grayScale.appText); + size: iconSize, color: scale.primaryScale.primaryText); return; case AttachmentState.attaching: value = 0; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.attachedWeak: value = 1; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.attachedStrong: value = 2; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.attachedGood: value = 3; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.fullyAttached: value = 4; - color = scale.primaryScale.appText; + color = scale.primaryScale.primaryText; case AttachmentState.overAttached: value = 4; - color = scale.secondaryScale.subtleText; + color = scale.primaryScale.primaryText; } - inactiveColor = scale.grayScale.subtleText; + inactiveColor = scale.primaryScale.primaryText; iconWidget = SignalStrengthIndicator.bars( value: value, @@ -66,7 +66,7 @@ class SignalStrengthMeterWidget extends StatelessWidget { inactiveColor: inactiveColor, size: iconSize, barCount: 4, - spacing: 1); + spacing: 2); }, loading: () => {iconWidget = const Icon(Icons.warning)}, error: (e, st) => { diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart index cd5ef23..52214ba 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_cubit.dart @@ -69,7 +69,7 @@ class DHTShortArrayCubit extends Cubit> // Because this is async, we could get an update while we're // still processing the last one. Only called after init future has run // so we dont have to wait for that here. - _sspUpdate.busyUpdate>>( + _sspUpdate.busyUpdate>( busy, (emit) async => _refreshInner(emit)); } diff --git a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart index 01390ed..2fd1a60 100644 --- a/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart +++ b/packages/veilid_support/lib/dht_support/src/dht_short_array/dht_short_array_head.dart @@ -470,6 +470,8 @@ class _DHTShortArrayHead { _subscription = null; } + // Called when the shortarray changes online and we find out from a watch + // but not when we make a change locally Future _onHeadValueChanged( DHTRecord record, Uint8List? data, List subkeys) async { // If head record subkey zero changes, then the layout