unify handling of themes

accessible theming/high contrast support
This commit is contained in:
Christien Rioux 2024-04-12 20:55:05 -04:00
parent 23ec185324
commit 4f02435964
41 changed files with 958 additions and 622 deletions

View File

@ -6,7 +6,6 @@
"settings_tooltip": "Settings" "settings_tooltip": "Settings"
}, },
"pager": { "pager": {
"account": "Account",
"chats": "Chats", "chats": "Chats",
"contacts": "Contacts" "contacts": "Contacts"
}, },
@ -67,6 +66,9 @@
"add_chat_sheet": { "add_chat_sheet": {
"new_chat": "New Chat" "new_chat": "New Chat"
}, },
"chat": {
"say_something": "Say Something"
},
"create_invitation_dialog": { "create_invitation_dialog": {
"title": "Create Contact Invitation", "title": "Create Contact Invitation",
"connect_with_me": "Connect with me on VeilidChat!", "connect_with_me": "Connect with me on VeilidChat!",

View File

@ -7,6 +7,7 @@ import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../layout/default_app_bar.dart'; import '../../../layout/default_app_bar.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart'; import '../../../tools/tools.dart';
import '../../../veilid_processor/veilid_processor.dart'; import '../../../veilid_processor/veilid_processor.dart';
import '../../account_manager.dart'; import '../../account_manager.dart';

View File

@ -31,11 +31,14 @@ class ProfileWidget extends StatelessWidget {
child: Column(children: [ child: Column(children: [
Text( Text(
_profile.name, _profile.name,
style: textTheme.headlineSmall, style: textTheme.headlineSmall!
.copyWith(color: scale.primaryScale.borderText),
textAlign: TextAlign.left, textAlign: TextAlign.left,
).paddingAll(4), ).paddingAll(4),
if (_profile.pronouns.isNotEmpty) 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), .paddingLTRB(4, 0, 4, 4),
]), ]),
); );

View File

@ -13,7 +13,6 @@ import '../../chat_list/chat_list.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart';
import '../chat.dart'; import '../chat.dart';
class ChatComponent extends StatelessWidget { class ChatComponent extends StatelessWidget {
@ -173,11 +172,13 @@ class ChatComponent extends StatelessWidget {
16, 0, 16, 0), 16, 0, 16, 0),
child: Text(_remoteUser.firstName!, child: Text(_remoteUser.firstName!,
textAlign: TextAlign.start, textAlign: TextAlign.start,
style: textTheme.titleMedium), style: textTheme.titleMedium!.copyWith(
color: scale.primaryScale.borderText)),
)), )),
const Spacer(), const Spacer(),
IconButton( IconButton(
icon: const Icon(Icons.close), icon: Icon(Icons.close,
color: scale.primaryScale.borderText),
onPressed: () async { onPressed: () async {
context.read<ActiveChatCubit>().setActiveChat(null); context.read<ActiveChatCubit>().setActiveChat(null);
}).paddingLTRB(16, 0, 16, 0) }).paddingLTRB(16, 0, 16, 0)

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/theme.dart';
class EmptyChatWidget extends StatelessWidget { class EmptyChatWidget extends StatelessWidget {
const EmptyChatWidget({super.key}); const EmptyChatWidget({super.key});
@ -7,8 +10,11 @@ class EmptyChatWidget extends StatelessWidget {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build( Widget build(
BuildContext context, BuildContext context,
) => ) {
Container( final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -19,16 +25,17 @@ class EmptyChatWidget extends StatelessWidget {
children: [ children: [
Icon( Icon(
Icons.chat, Icons.chat,
color: Theme.of(context).disabledColor, color: scale.primaryScale.subtleBorder,
size: 48, size: 48,
), ),
Text( Text(
'Say Something', translate('chat.say_something'),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).disabledColor, color: scale.primaryScale.subtleBorder,
), ),
), ),
], ],
), ),
); );
} }
}

View File

@ -4,12 +4,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart';
Widget newChatBottomSheetBuilder( Widget newChatBottomSheetBuilder(
BuildContext sheetContext, BuildContext context) { BuildContext sheetContext, BuildContext context) {
final theme = Theme.of(sheetContext); //final theme = Theme.of(sheetContext);
final scale = theme.extension<ScaleScheme>()!; //final scale = theme.extension<ScaleScheme>()!;
return KeyboardListener( return KeyboardListener(
focusNode: FocusNode(), focusNode: FocusNode(),
@ -23,49 +22,10 @@ Widget newChatBottomSheetBuilder(
title: translate('add_chat_sheet.new_chat'), title: translate('add_chat_sheet.new_chat'),
child: SizedBox( child: SizedBox(
height: 160, height: 160,
child: Row( child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
Text( Text(
'Group and custom chat functionality is not available yet') '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)))); ]).paddingAll(16))));
} }

View File

@ -1,8 +1,6 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../chat/cubits/active_chat_cubit.dart'; import '../../chat/cubits/active_chat_cubit.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
@ -25,85 +23,35 @@ class ChatSingleContactItemWidget extends StatelessWidget {
Widget build( Widget build(
BuildContext context, BuildContext context,
) { ) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final activeChatCubit = context.watch<ActiveChatCubit>(); final activeChatCubit = context.watch<ActiveChatCubit>();
final remoteConversationRecordKey = final remoteConversationRecordKey =
_contact.remoteConversationRecordKey.toVeilid(); _contact.remoteConversationRecordKey.toVeilid();
final selected = activeChatCubit.state == remoteConversationRecordKey; final selected = activeChatCubit.state == remoteConversationRecordKey;
return Container( return SliderTile(
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), key: ObjectKey(_contact),
endActionPane: ActionPane( disabled: _disabled,
motion: const DrawerMotion(), selected: selected,
children: [ tileScale: ScaleKind.secondary,
SlidableAction( title: _contact.editedProfile.name,
onPressed: _disabled subtitle: _contact.editedProfile.pronouns,
? null icon: Icons.chat,
: (context) async { onTap: () {
final chatListCubit = context.read<ChatListCubit>();
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 { singleFuture(activeChatCubit, () async {
activeChatCubit activeChatCubit.setActiveChat(remoteConversationRecordKey);
.setActiveChat(remoteConversationRecordKey);
}); });
}, },
title: Text(_contact.editedProfile.name), endActions: [
SliderTileAction(
/// xxx show last message here icon: Icons.delete,
subtitle: (_contact.editedProfile.pronouns.isNotEmpty) label: translate('button.delete'),
? Text(_contact.editedProfile.pronouns) actionScale: ScaleKind.tertiary,
: null, onPressed: (context) async {
iconColor: selected final chatListCubit = context.read<ChatListCubit>();
? scale.primaryScale.appText await chatListCubit.deleteChat(
: scale.primaryScale.subtleText, remoteConversationRecordKey: remoteConversationRecordKey);
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<proto.Contact>('contact', _contact));
} }
} }

View File

@ -7,7 +7,7 @@ import 'package:searchable_listview/searchable_listview.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../theme/theme.dart';
import '../chat_list.dart'; import '../chat_list.dart';
class ChatSingleContactListWidget extends StatelessWidget { class ChatSingleContactListWidget extends StatelessWidget {
@ -42,7 +42,8 @@ class ChatSingleContactListWidget extends StatelessWidget {
} }
return ChatSingleContactItemWidget( return ChatSingleContactItemWidget(
contact: contact, contact: contact,
disabled: contactListV.busy); disabled: contactListV.busy)
.paddingLTRB(0, 4, 0, 0);
}, },
filter: (value) { filter: (value) {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();

View File

@ -10,6 +10,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../contact_invitation.dart'; import '../contact_invitation.dart';

View File

@ -1,7 +1,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart'; import '../../theme/theme.dart';
@ -28,80 +27,26 @@ class ContactInvitationItemWidget extends StatelessWidget {
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); // final remoteConversationKey =
//final textTheme = theme.textTheme; // contact.remoteConversationRecordKey.toVeilid();
final scale = theme.extension<ScaleScheme>()!;
return Container( const selected =
clipBehavior: Clip.antiAlias, false; // xxx: eventually when we have selectable invitations:
decoration: ShapeDecoration( // activeContactCubit.state == remoteConversationRecordKey;
color: scale.tertiaryScale.subtleBorder,
shape: RoundedRectangleBorder( final tileDisabled =
borderRadius: BorderRadius.circular(8), disabled || context.watch<ContactInvitationListCubit>().isBusy;
)),
child: Slidable( return SliderTile(
// Specify a key if the Slidable is dismissible.
key: ObjectKey(contactInvitationRecord), key: ObjectKey(contactInvitationRecord),
endActionPane: ActionPane( disabled: tileDisabled,
// A motion is a widget used to control how the pane animates. selected: selected,
motion: const DrawerMotion(), tileScale: ScaleKind.primary,
title: contactInvitationRecord.message.isEmpty
// A pane can dismiss the Slidable. ? translate('contact_list.invitation')
//dismissible: DismissiblePane(onDismissed: () {}), : contactInvitationRecord.message,
icon: Icons.person_add,
// All actions are defined in the children parameter. onTap: () async {
children: [
// A SlidableAction can have an icon and/or a label.
SlidableAction(
onPressed: disabled
? null
: (context) async {
final contactInvitationListCubit =
context.read<ContactInvitationListCubit>();
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) { if (!context.mounted) {
return; return;
} }
@ -109,18 +54,24 @@ class ContactInvitationItemWidget extends StatelessWidget {
context: context, context: context,
message: contactInvitationRecord.message, message: contactInvitationRecord.message,
create: (context) => InvitationGeneratorCubit.value( create: (context) => InvitationGeneratorCubit.value(
Uint8List.fromList( Uint8List.fromList(contactInvitationRecord.invitation)));
contactInvitationRecord.invitation)));
}, },
title: Text( endActions: [
contactInvitationRecord.message.isEmpty SliderTileAction(
? translate('contact_list.invitation') icon: Icons.delete,
: contactInvitationRecord.message, label: translate('button.delete'),
softWrap: true, actionScale: ScaleKind.tertiary,
), onPressed: (context) async {
iconColor: scale.tertiaryScale.background, final contactInvitationListCubit =
textColor: scale.tertiaryScale.appText, context.read<ContactInvitationListCubit>();
//Text(Timestamp.fromInt64(contactInvitationRecord.expiration) / ), await contactInvitationListCubit.deleteInvitation(
leading: const Icon(Icons.person_add)))); accepted: false,
contactRequestInboxRecordKey: contactInvitationRecord
.contactRequestInbox.recordKey
.toVeilid());
},
)
],
);
} }
} }

View File

@ -10,6 +10,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../contact_invitation.dart'; import '../contact_invitation.dart';

View File

@ -9,6 +9,7 @@ import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
import '../../contacts/contacts.dart'; import '../../contacts/contacts.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../contact_invitation.dart'; import '../contact_invitation.dart';

View File

@ -4,7 +4,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'create_invitation_dialog.dart'; import 'create_invitation_dialog.dart';
import 'paste_invitation_dialog.dart'; import 'paste_invitation_dialog.dart';
import 'scan_invitation_dialog.dart'; import 'scan_invitation_dialog.dart';
@ -37,7 +36,7 @@ Widget newContactBottomSheetBuilder(
}, },
iconSize: 64, iconSize: 64,
icon: const Icon(Icons.contact_page), icon: const Icon(Icons.contact_page),
color: scale.primaryScale.background), color: scale.primaryScale.hoverBorder),
Text( Text(
translate('add_contact_sheet.create_invite'), translate('add_contact_sheet.create_invite'),
) )
@ -50,7 +49,7 @@ Widget newContactBottomSheetBuilder(
}, },
iconSize: 64, iconSize: 64,
icon: const Icon(Icons.qr_code_scanner), icon: const Icon(Icons.qr_code_scanner),
color: scale.primaryScale.background), color: scale.primaryScale.hoverBorder),
Text( Text(
translate('add_contact_sheet.scan_invite'), translate('add_contact_sheet.scan_invite'),
) )
@ -63,7 +62,7 @@ Widget newContactBottomSheetBuilder(
}, },
iconSize: 64, iconSize: 64,
icon: const Icon(Icons.paste), icon: const Icon(Icons.paste),
color: scale.primaryScale.background), color: scale.primaryScale.hoverBorder),
Text( Text(
translate('add_contact_sheet.paste_invite'), translate('add_contact_sheet.paste_invite'),
) )

View File

@ -211,7 +211,7 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
scale.grayScale.subtleBackground); scale.grayScale.subtleBackground);
case TorchState.on: case TorchState.on:
return Icon(Icons.flash_on, return Icon(Icons.flash_on,
color: scale.primaryScale.background); color: scale.primaryScale.primary);
} }
}, },
), ),
@ -258,8 +258,8 @@ class ScanInvitationDialogState extends State<ScanInvitationDialog> {
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: IconButton( child: IconButton(
color: Colors.white, color: Colors.white,
icon: Icon(Icons.close, icon:
color: scale.grayScale.background), Icon(Icons.close, color: scale.grayScale.primary),
iconSize: 32, iconSize: 32,
onPressed: () => { onPressed: () => {
SchedulerBinding.instance SchedulerBinding.instance

View File

@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../chat_list/chat_list.dart'; import '../../chat_list/chat_list.dart';
import '../../layout/layout.dart'; import '../../layout/layout.dart';
@ -17,106 +16,65 @@ class ContactItemWidget extends StatelessWidget {
final proto.Contact contact; final proto.Contact contact;
final bool disabled; final bool disabled;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<proto.Contact>('contact', contact))
..add(DiagnosticsProperty<bool>('disabled', disabled));
}
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build( Widget build(
BuildContext context, BuildContext context,
) { ) {
final theme = Theme.of(context);
//final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!;
final remoteConversationKey = final remoteConversationKey =
contact.remoteConversationRecordKey.toVeilid(); contact.remoteConversationRecordKey.toVeilid();
const selected = false; // xxx: eventually when we have selectable contacts: const selected = false; // xxx: eventually when we have selectable contacts:
// activeContactCubit.state == remoteConversationRecordKey; // activeContactCubit.state == remoteConversationRecordKey;
return Container( final tileDisabled = disabled || context.watch<ContactListCubit>().isBusy;
margin: const EdgeInsets.fromLTRB(0, 4, 0, 0),
clipBehavior: Clip.antiAlias, return SliderTile(
decoration: ShapeDecoration(
color: selected
// ignore: dead_code
? scale.primaryScale.activeElementBackground
: scale.primaryScale.hoverElementBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
)),
child: Slidable(
key: ObjectKey(contact), key: ObjectKey(contact),
endActionPane: ActionPane( disabled: tileDisabled,
motion: const DrawerMotion(), selected: selected,
children: [ tileScale: ScaleKind.primary,
SlidableAction( title: contact.editedProfile.name,
onPressed: disabled || context.watch<ChatListCubit>().isBusy subtitle: contact.editedProfile.pronouns,
? null icon: Icons.person,
: (context) async { onTap: () async {
final contactListCubit =
context.read<ContactListCubit>();
final chatListCubit = context.read<ChatListCubit>();
// Remove any chats for this contact
await chatListCubit.deleteChat(
remoteConversationRecordKey:
remoteConversationKey);
// 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',
// ),
],
),
// The child of the Slidable is what the user sees when the
// component is not dragged.
child: ListTile(
onTap: disabled || context.watch<ChatListCubit>().isBusy
? null
: () async {
// Start a chat // Start a chat
final chatListCubit = context.read<ChatListCubit>(); final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact( await chatListCubit.getOrCreateChatSingleContact(
remoteConversationRecordKey: remoteConversationKey); remoteConversationRecordKey: remoteConversationKey);
// Click over to chats // Click over to chats
if (context.mounted) { if (context.mounted) {
await MainPager.of(context) await MainPager.of(context)
?.pageController ?.pageController
.animateToPage(1, .animateToPage(1, duration: 250.ms, curve: Curves.easeInOut);
duration: 250.ms, curve: Curves.easeInOut);
} }
}, },
title: Text(contact.editedProfile.name), endActions: [
subtitle: (contact.editedProfile.pronouns.isNotEmpty) SliderTileAction(
? Text(contact.editedProfile.pronouns) icon: Icons.delete,
: null, label: translate('button.delete'),
iconColor: selected actionScale: ScaleKind.tertiary,
// ignore: dead_code onPressed: (context) async {
? scale.primaryScale.appText final contactListCubit = context.read<ContactListCubit>();
: scale.primaryScale.subtleText, final chatListCubit = context.read<ChatListCubit>();
textColor: selected
// ignore: dead_code
? scale.primaryScale.appText
: scale.primaryScale.subtleText,
selectedColor: scale.primaryScale.appText,
leading: const Icon(Icons.person))));
}
@override // Remove any chats for this contact
void debugFillProperties(DiagnosticPropertiesBuilder properties) { await chatListCubit.deleteChat(
super.debugFillProperties(properties); remoteConversationRecordKey: remoteConversationKey);
properties.add(DiagnosticsProperty<proto.Contact>('contact', contact));
// Delete the contact itself
await contactListCubit.deleteContact(contact: contact);
})
],
);
} }
} }

View File

@ -6,7 +6,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:searchable_listview/searchable_listview.dart'; import 'package:searchable_listview/searchable_listview.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../theme/theme.dart';
import 'contact_item_widget.dart'; import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart'; import 'empty_contact_list_widget.dart';
@ -25,7 +25,11 @@ class ContactListWidget extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) => SizedBox.expand( Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
return SizedBox.expand(
child: styledTitleContainer( child: styledTitleContainer(
context: context, context: context,
title: translate('contact_list.title'), title: translate('contact_list.title'),
@ -35,7 +39,8 @@ class ContactListWidget extends StatelessWidget {
: SearchableList<proto.Contact>( : SearchableList<proto.Contact>(
initialList: contactList.toList(), initialList: contactList.toList(),
builder: (l, i, c) => builder: (l, i, c) =>
ContactItemWidget(contact: c, disabled: disabled), ContactItemWidget(contact: c, disabled: disabled)
.paddingLTRB(0, 4, 0, 0),
filter: (value) { filter: (value) {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();
return contactList return contactList
@ -49,9 +54,11 @@ class ContactListWidget extends StatelessWidget {
.toList(); .toList();
}, },
spaceBetweenSearchAndList: 4, spaceBetweenSearchAndList: 4,
defaultSuffixIconColor: scale.primaryScale.border,
inputDecoration: InputDecoration( inputDecoration: InputDecoration(
labelText: translate('contact_list.search'), labelText: translate('contact_list.search'),
), ),
).paddingAll(8), ).paddingAll(8),
))).paddingLTRB(8, 0, 8, 8); ))).paddingLTRB(8, 0, 8, 8);
} }
}

View File

@ -37,11 +37,11 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
Row(children: [ Row(children: [
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
color: scale.secondaryScale.appText, color: scale.secondaryScale.borderText,
constraints: const BoxConstraints.expand(height: 64, width: 64), constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: backgroundColor: MaterialStateProperty.all(
MaterialStateProperty.all(scale.secondaryScale.border), scale.primaryScale.hoverBorder),
shape: MaterialStateProperty.all( shape: MaterialStateProperty.all(
const RoundedRectangleBorder( const RoundedRectangleBorder(
borderRadius: borderRadius:

View File

@ -11,7 +11,7 @@ import '../../../chat_list/chat_list.dart';
import '../../../contact_invitation/contact_invitation.dart'; import '../../../contact_invitation/contact_invitation.dart';
import '../../../contacts/contacts.dart'; import '../../../contacts/contacts.dart';
import '../../../router/router.dart'; import '../../../router/router.dart';
import '../../../tools/tools.dart'; import '../../../theme/theme.dart';
class HomeAccountReadyShell extends StatefulWidget { class HomeAccountReadyShell extends StatefulWidget {
factory HomeAccountReadyShell( factory HomeAccountReadyShell(

View File

@ -61,8 +61,9 @@ class AccountPageState extends State<AccountPage> {
translate('account_page.contact_invitations'), translate('account_page.contact_invitations'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.titleMedium! style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.subtleText), .copyWith(color: scale.primaryScale.borderText),
), ),
iconColor: scale.primaryScale.borderText,
initiallyExpanded: true, initiallyExpanded: true,
children: [ children: [
ContactInvitationListWidget( ContactInvitationListWidget(

View File

@ -10,7 +10,6 @@ import 'package:stylish_bottom_bar/stylish_bottom_bar.dart';
import '../../../../chat/chat.dart'; import '../../../../chat/chat.dart';
import '../../../../contact_invitation/contact_invitation.dart'; import '../../../../contact_invitation/contact_invitation.dart';
import '../../../../theme/theme.dart'; import '../../../../theme/theme.dart';
import '../../../../tools/tools.dart';
import 'account_page.dart'; import 'account_page.dart';
import 'bottom_sheet_action_button.dart'; import 'bottom_sheet_action_button.dart';
import 'chats_page.dart'; import 'chats_page.dart';
@ -41,7 +40,7 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
Icons.add_comment_sharp, Icons.add_comment_sharp,
]; ];
final _bottomLabelList = <String>[ final _bottomLabelList = <String>[
translate('pager.account'), translate('pager.contacts'),
translate('pager.chats'), translate('pager.chats'),
]; ];
@ -82,12 +81,11 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return BottomBarItem( return BottomBarItem(
title: Text(_bottomLabelList[index]), title: Text(_bottomLabelList[index]),
icon: Icon(_selectedIconList[index], color: scale.primaryScale.appText), icon:
Icon(_selectedIconList[index], color: scale.primaryScale.borderText),
selectedIcon: selectedIcon:
Icon(_selectedIconList[index], color: scale.primaryScale.appText), Icon(_selectedIconList[index], color: scale.primaryScale.borderText),
backgroundColor: scale.primaryScale.appText, backgroundColor: scale.primaryScale.borderText,
//unSelectedColor: theme.colorScheme.primaryContainer,
//selectedColor: theme.colorScheme.primary,
//badge: const Text('9+'), //badge: const Text('9+'),
//showBadge: true, //showBadge: true,
); );
@ -169,21 +167,10 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
// ), // ),
bottomNavigationBar: StylishBottomBar( bottomNavigationBar: StylishBottomBar(
backgroundColor: scale.primaryScale.hoverBorder, backgroundColor: scale.primaryScale.hoverBorder,
// gradient: LinearGradient(
// begin: Alignment.topCenter,
// end: Alignment.bottomCenter,
// colors: <Color>[
// theme.colorScheme.primary,
// theme.colorScheme.primaryContainer,
// ]),
//borderRadius: BorderRadius.all(Radius.circular(16)),
option: AnimatedBarOptions( option: AnimatedBarOptions(
// iconSize: 32,
//barAnimation: BarAnimation.fade,
iconStyle: IconStyle.animated,
inkEffect: true, inkEffect: true,
inkColor: scale.primaryScale.hoverBackground, inkColor: scale.primaryScale.hoverPrimary,
//opacity: 0.3, opacity: 0.3,
), ),
items: _buildBottomBarItems(), items: _buildBottomBarItems(),
hasNotch: true, hasNotch: true,
@ -198,11 +185,11 @@ class MainPagerState extends State<MainPager> with TickerProviderStateMixin {
floatingActionButton: BottomSheetActionButton( floatingActionButton: BottomSheetActionButton(
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(14))), borderRadius: BorderRadius.all(Radius.circular(14))),
foregroundColor: scale.secondaryScale.appText, foregroundColor: scale.secondaryScale.borderText,
backgroundColor: scale.secondaryScale.hoverBorder, backgroundColor: scale.secondaryScale.hoverBorder,
builder: (context) => Icon( builder: (context) => Icon(
_fabIconList[_currentPage], _fabIconList[_currentPage],
color: scale.secondaryScale.appText, color: scale.secondaryScale.borderText,
), ),
bottomSheetBuilder: (sheetContext) => bottomSheetBuilder: (sheetContext) =>
_bottomSheetBuilder(sheetContext, context)), _bottomSheetBuilder(sheetContext, context)),

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../tools/tools.dart'; import '../../theme/theme.dart';
class HomeNoActive extends StatefulWidget { class HomeNoActive extends StatefulWidget {
const HomeNoActive({super.key}); const HomeNoActive({super.key});

View File

@ -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,
));

View File

@ -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: <ThemeExtension<dynamic>>[
scaleScheme,
scaleConfig,
]);
}

View File

@ -1,4 +1,6 @@
export 'chat_theme.dart';
export 'radix_generator.dart'; export 'radix_generator.dart';
export 'scale_color.dart'; export 'scale_color.dart';
export 'scale_scheme.dart'; export 'scale_scheme.dart';
export 'slider_tile.dart';
export 'theme_preference.dart'; export 'theme_preference.dart';

View File

@ -1,16 +1,16 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:radix_colors/radix_colors.dart'; import 'package:radix_colors/radix_colors.dart';
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import 'scale_color.dart'; import 'scale_color.dart';
import 'scale_input_decorator_theme.dart';
import 'scale_scheme.dart'; import 'scale_scheme.dart';
enum RadixThemeColor { enum RadixThemeColor {
scarlet, // tomato + red + violet scarlet, // red + violet + tomato
babydoll, // crimson + purple + pink babydoll, // crimson + pink + purple
vapor, // pink + cyan + plum vapor, // pink + cyan + plum
gold, // yellow + amber + orange gold, // yellow + amber + orange
garden, // grass + orange + brown garden, // grass + orange + brown
@ -19,7 +19,7 @@ enum RadixThemeColor {
lapis, // blue + indigo + mint lapis, // blue + indigo + mint
eggplant, // violet + purple + indigo eggplant, // violet + purple + indigo
lime, // lime + yellow + orange lime, // lime + yellow + orange
grim, // mauve + slate + sage grim, // grey + purple + brown
} }
enum _RadixBaseColor { enum _RadixBaseColor {
@ -282,11 +282,15 @@ extension ToScaleColor on RadixColor {
subtleBorder: step6, subtleBorder: step6,
border: step7, border: step7,
hoverBorder: step8, hoverBorder: step8,
background: step9, primary: step9,
hoverBackground: step10, hoverPrimary: step10,
subtleText: step11, subtleText: step11,
appText: step12, 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) { RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) {
late RadixScheme radixScheme; late RadixScheme radixScheme;
switch (themeColor) { switch (themeColor) {
// tomato + red + violet // red + violet + tomato
case RadixThemeColor.scarlet: case RadixThemeColor.scarlet:
radixScheme = RadixScheme( radixScheme = RadixScheme(
primaryScale: primaryScale: _radixColorSteps(brightness, false, _RadixBaseColor.red),
_radixColorSteps(brightness, false, _RadixBaseColor.tomato),
primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryExtra: RadixScaleExtra(foregroundText: Colors.white),
primaryAlphaScale: primaryAlphaScale:
_radixColorSteps(brightness, true, _RadixBaseColor.tomato), _radixColorSteps(brightness, true, _RadixBaseColor.red),
primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white),
secondaryScale: secondaryScale:
_radixColorSteps(brightness, false, _RadixBaseColor.red), _radixColorSteps(brightness, false, _RadixBaseColor.violet),
secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white),
tertiaryScale: tertiaryScale:
_radixColorSteps(brightness, false, _RadixBaseColor.violet), _radixColorSteps(brightness, false, _RadixBaseColor.tomato),
tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white),
grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.tomato), grayScale: _radixGraySteps(brightness, false, _RadixBaseColor.red),
grayExtra: RadixScaleExtra(foregroundText: Colors.white), grayExtra: RadixScaleExtra(foregroundText: Colors.white),
errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.yellow), errorScale: _radixColorSteps(brightness, false, _RadixBaseColor.yellow),
errorExtra: RadixScaleExtra(foregroundText: Colors.black), errorExtra: RadixScaleExtra(foregroundText: Colors.black),
); );
// crimson + purple + pink // crimson + pink + purple
case RadixThemeColor.babydoll: case RadixThemeColor.babydoll:
radixScheme = RadixScheme( radixScheme = RadixScheme(
primaryScale: primaryScale:
@ -369,10 +372,10 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) {
_radixColorSteps(brightness, true, _RadixBaseColor.crimson), _radixColorSteps(brightness, true, _RadixBaseColor.crimson),
primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white),
secondaryScale: secondaryScale:
_radixColorSteps(brightness, false, _RadixBaseColor.purple), _radixColorSteps(brightness, false, _RadixBaseColor.pink),
secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white),
tertiaryScale: tertiaryScale:
_radixColorSteps(brightness, false, _RadixBaseColor.pink), _radixColorSteps(brightness, false, _RadixBaseColor.purple),
tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white),
grayScale: grayScale:
_radixGraySteps(brightness, false, _RadixBaseColor.crimson), _radixGraySteps(brightness, false, _RadixBaseColor.crimson),
@ -546,13 +549,13 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) {
_radixGraySteps(brightness, false, _RadixBaseColor.tomato), _radixGraySteps(brightness, false, _RadixBaseColor.tomato),
primaryExtra: RadixScaleExtra(foregroundText: Colors.white), primaryExtra: RadixScaleExtra(foregroundText: Colors.white),
primaryAlphaScale: primaryAlphaScale:
_radixColorSteps(brightness, true, _RadixBaseColor.tomato), _radixGraySteps(brightness, true, _RadixBaseColor.tomato),
primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white), primaryAlphaExtra: RadixScaleExtra(foregroundText: Colors.white),
secondaryScale: secondaryScale:
_radixColorSteps(brightness, false, _RadixBaseColor.indigo), _radixColorSteps(brightness, false, _RadixBaseColor.purple),
secondaryExtra: RadixScaleExtra(foregroundText: Colors.white), secondaryExtra: RadixScaleExtra(foregroundText: Colors.white),
tertiaryScale: tertiaryScale:
_radixColorSteps(brightness, false, _RadixBaseColor.teal), _radixColorSteps(brightness, false, _RadixBaseColor.brown),
tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white), tertiaryExtra: RadixScaleExtra(foregroundText: Colors.white),
grayScale: brightness == Brightness.dark grayScale: brightness == Brightness.dark
? RadixColors.dark.gray ? RadixColors.dark.gray
@ -565,87 +568,7 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) {
return radixScheme; return radixScheme;
} }
ColorScheme _scaleToColorScheme(Brightness brightness, ScaleScheme scale) => TextTheme makeRadixTextTheme(Brightness brightness) {
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) {
late final TextTheme textTheme; late final TextTheme textTheme;
if (Platform.isIOS) { if (Platform.isIOS) {
textTheme = (brightness == Brightness.light) textTheme = (brightness == Brightness.light)
@ -677,10 +600,11 @@ TextTheme _makeTextTheme(Brightness brightness) {
} }
ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
final textTheme = _makeTextTheme(brightness); final textTheme = makeRadixTextTheme(brightness);
final radix = _radixScheme(brightness, themeColor); final radix = _radixScheme(brightness, themeColor);
final scaleScheme = radix.toScale(); final scaleScheme = radix.toScale();
final colorScheme = _scaleToColorScheme(brightness, scaleScheme); final colorScheme = scaleScheme.toColorScheme(brightness);
final scaleConfig = ScaleConfig(useVisualIndicators: false);
final themeData = ThemeData.from( final themeData = ThemeData.from(
colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true); colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true);
@ -697,34 +621,18 @@ ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) {
backgroundColor: scaleScheme.primaryScale.elementBackground, backgroundColor: scaleScheme.primaryScale.elementBackground,
selectedColor: scaleScheme.primaryScale.activeElementBackground, selectedColor: scaleScheme.primaryScale.activeElementBackground,
surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground, surfaceTintColor: scaleScheme.primaryScale.hoverElementBackground,
checkmarkColor: scaleScheme.primaryScale.background, checkmarkColor: scaleScheme.primaryScale.primary,
side: BorderSide(color: scaleScheme.primaryScale.border)), side: BorderSide(color: scaleScheme.primaryScale.border)),
elevatedButtonTheme: ElevatedButtonThemeData( elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: scaleScheme.primaryScale.elementBackground, backgroundColor: scaleScheme.primaryScale.elementBackground,
foregroundColor: scaleScheme.primaryScale.appText, foregroundColor: scaleScheme.primaryScale.primary,
disabledBackgroundColor: scaleScheme.grayScale.elementBackground, disabledBackgroundColor: scaleScheme.grayScale.elementBackground,
disabledForegroundColor: scaleScheme.grayScale.appText, disabledForegroundColor: scaleScheme.grayScale.primary,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide(color: scaleScheme.primaryScale.border), side: BorderSide(color: scaleScheme.primaryScale.border),
borderRadius: BorderRadius.circular(8))), borderRadius: BorderRadius.circular(8))),
), ),
focusColor: scaleScheme.primaryScale.activeElementBackground, inputDecorationTheme: ScaleInputDecoratorTheme(scaleScheme, textTheme),
hoverColor: scaleScheme.primaryScale.hoverElementBackground, extensions: <ThemeExtension<dynamic>>[scaleScheme, scaleConfig]);
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: <ThemeExtension<dynamic>>[
scaleScheme,
]);
} }

View File

@ -10,11 +10,15 @@ class ScaleColor {
required this.subtleBorder, required this.subtleBorder,
required this.border, required this.border,
required this.hoverBorder, required this.hoverBorder,
required this.background, required this.primary,
required this.hoverBackground, required this.hoverPrimary,
required this.subtleText, required this.subtleText,
required this.appText, required this.appText,
required this.foregroundText, required this.primaryText,
required this.borderText,
required this.dialogBorder,
required this.calloutBackground,
required this.calloutText,
}); });
Color appBackground; Color appBackground;
@ -25,11 +29,15 @@ class ScaleColor {
Color subtleBorder; Color subtleBorder;
Color border; Color border;
Color hoverBorder; Color hoverBorder;
Color background; Color primary;
Color hoverBackground; Color hoverPrimary;
Color subtleText; Color subtleText;
Color appText; Color appText;
Color foregroundText; Color primaryText;
Color borderText;
Color dialogBorder;
Color calloutBackground;
Color calloutText;
ScaleColor copyWith({ ScaleColor copyWith({
Color? appBackground, Color? appBackground,
@ -45,6 +53,10 @@ class ScaleColor {
Color? subtleText, Color? subtleText,
Color? appText, Color? appText,
Color? foregroundText, Color? foregroundText,
Color? borderText,
Color? dialogBorder,
Color? calloutBackground,
Color? calloutText,
}) => }) =>
ScaleColor( ScaleColor(
appBackground: appBackground ?? this.appBackground, appBackground: appBackground ?? this.appBackground,
@ -57,12 +69,15 @@ class ScaleColor {
subtleBorder: subtleBorder ?? this.subtleBorder, subtleBorder: subtleBorder ?? this.subtleBorder,
border: border ?? this.border, border: border ?? this.border,
hoverBorder: hoverBorder ?? this.hoverBorder, hoverBorder: hoverBorder ?? this.hoverBorder,
background: background ?? this.background, primary: background ?? this.primary,
hoverBackground: hoverBackground ?? this.hoverBackground, hoverPrimary: hoverBackground ?? this.hoverPrimary,
subtleText: subtleText ?? this.subtleText, subtleText: subtleText ?? this.subtleText,
appText: appText ?? this.appText, appText: appText ?? this.appText,
foregroundText: foregroundText ?? this.foregroundText, 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 // ignore: prefer_constructors_over_static_methods
static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor( 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), border: Color.lerp(a.border, b.border, t) ?? const Color(0x00000000),
hoverBorder: Color.lerp(a.hoverBorder, b.hoverBorder, t) ?? hoverBorder: Color.lerp(a.hoverBorder, b.hoverBorder, t) ??
const Color(0x00000000), const Color(0x00000000),
background: Color.lerp(a.background, b.background, t) ?? primary: Color.lerp(a.primary, b.primary, t) ?? const Color(0x00000000),
const Color(0x00000000), hoverPrimary: Color.lerp(a.hoverPrimary, b.hoverPrimary, t) ??
hoverBackground: Color.lerp(a.hoverBackground, b.hoverBackground, t) ??
const Color(0x00000000), const Color(0x00000000),
subtleText: Color.lerp(a.subtleText, b.subtleText, t) ?? subtleText: Color.lerp(a.subtleText, b.subtleText, t) ??
const Color(0x00000000), const Color(0x00000000),
appText: Color.lerp(a.appText, b.appText, 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), const Color(0x00000000),
); );
} }

View File

@ -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);
});
}

View File

@ -2,14 +2,17 @@ import 'package:flutter/material.dart';
import 'scale_color.dart'; import 'scale_color.dart';
enum ScaleKind { primary, primaryAlpha, secondary, tertiary, gray, error }
class ScaleScheme extends ThemeExtension<ScaleScheme> { class ScaleScheme extends ThemeExtension<ScaleScheme> {
ScaleScheme( ScaleScheme({
{required this.primaryScale, required this.primaryScale,
required this.primaryAlphaScale, required this.primaryAlphaScale,
required this.secondaryScale, required this.secondaryScale,
required this.tertiaryScale, required this.tertiaryScale,
required this.grayScale, required this.grayScale,
required this.errorScale}); required this.errorScale,
});
final ScaleColor primaryScale; final ScaleColor primaryScale;
final ScaleColor primaryAlphaScale; final ScaleColor primaryAlphaScale;
@ -18,6 +21,23 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
final ScaleColor grayScale; final ScaleColor grayScale;
final ScaleColor errorScale; 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 @override
ScaleScheme copyWith( ScaleScheme copyWith(
{ScaleColor? primaryScale, {ScaleColor? primaryScale,
@ -50,4 +70,65 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
errorScale: ScaleColor.lerp(errorScale, other.errorScale, t), 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> {
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,
);
}
} }

View File

@ -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<SliderTileAction> endActions;
final List<SliderTileAction> startActions;
final GestureTapCallback? onTap;
final IconData? icon;
final String title;
final String subtitle;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<bool>('disabled', disabled))
..add(DiagnosticsProperty<bool>('selected', selected))
..add(DiagnosticsProperty<ScaleKind>('tileScale', tileScale))
..add(IterableProperty<SliderTileAction>('endActions', endActions))
..add(IterableProperty<SliderTileAction>('startActions', startActions))
..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap))
..add(DiagnosticsProperty<IconData?>('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<ScaleScheme>()!;
final tileColor = scale.scale(!disabled ? tileScale : ScaleKind.gray);
final scalecfg = theme.extension<ScaleConfig>()!;
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)))));
}
}

View File

@ -2,7 +2,8 @@ import 'package:change_case/change_case.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.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'; import 'radix_generator.dart';
part 'theme_preference.freezed.dart'; part 'theme_preference.freezed.dart';
@ -83,7 +84,7 @@ extension ThemePreferencesExt on ThemePreferences {
// Special cases // Special cases
case ColorPreference.contrast: case ColorPreference.contrast:
// xxx do contrastGenerator // xxx do contrastGenerator
themeData = radixGenerator(brightness, RadixThemeColor.grim); themeData = contrastGenerator(brightness);
// Generate from Radix // Generate from Radix
case ColorPreference.scarlet: case ColorPreference.scarlet:
themeData = radixGenerator(brightness, RadixThemeColor.scarlet); themeData = radixGenerator(brightness, RadixThemeColor.scarlet);

View File

@ -2,7 +2,7 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme/theme.dart'; import '../theme.dart';
class StyledDialog extends StatelessWidget { class StyledDialog extends StatelessWidget {
const StyledDialog({required this.title, required this.child, super.key}); const StyledDialog({required this.title, required this.child, super.key});
@ -19,10 +19,11 @@ class StyledDialog extends StatelessWidget {
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),
contentPadding: const EdgeInsets.all(4), contentPadding: const EdgeInsets.all(4),
backgroundColor: scale.primaryScale.border, backgroundColor: scale.primaryScale.dialogBorder,
title: Text( title: Text(
title, title,
style: textTheme.titleMedium, style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.borderText),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
titlePadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), titlePadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),

View File

@ -1,2 +1,5 @@
export 'brightness_preferences.dart'; export 'brightness_preferences.dart';
export 'color_preferences.dart'; export 'color_preferences.dart';
export 'scanner_error_widget.dart';
export 'styled_dialog.dart';
export 'widget_helpers.dart';

View File

@ -9,7 +9,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import 'package:motion_toast/motion_toast.dart'; import 'package:motion_toast/motion_toast.dart';
import 'package:quickalert/quickalert.dart'; import 'package:quickalert/quickalert.dart';
import '../theme/theme.dart'; import '../theme.dart';
extension BorderExt on Widget { extension BorderExt on Widget {
DecoratedBox debugBorder() => DecoratedBox( DecoratedBox debugBorder() => DecoratedBox(
@ -35,19 +35,22 @@ Widget buildProgressIndicator() => Builder(builder: (context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return SpinKitFoldingCube( return SpinKitFoldingCube(
color: scale.tertiaryScale.background, color: scale.tertiaryScale.primary,
size: 80, size: 80,
); );
}); });
Widget waitingPage({String? text}) => Builder( Widget waitingPage({String? text}) => Builder(builder: (context) {
builder: (context) => ColoredBox( final theme = Theme.of(context);
color: Theme.of(context).scaffoldBackgroundColor, final scale = theme.extension<ScaleScheme>()!;
return ColoredBox(
color: scale.tertiaryScale.primaryText,
child: Center( child: Center(
child: Column(children: [ child: Column(children: [
buildProgressIndicator().expanded(), buildProgressIndicator().expanded(),
if (text != null) Text(text) if (text != null) Text(text)
])))); ])));
});
Widget debugPage(String text) => Builder( Widget debugPage(String text) => Builder(
builder: (context) => ColoredBox( builder: (context) => ColoredBox(
@ -132,12 +135,30 @@ void showInfoToast(BuildContext context, String message) {
).show(context); ).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({ Widget styledTitleContainer({
required BuildContext context, required BuildContext context,
required String title, required String title,
required Widget child, required Widget child,
Color? borderColor, Color? borderColor,
Color? backgroundColor, Color? backgroundColor,
Color? titleColor,
}) { }) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
@ -153,7 +174,7 @@ Widget styledTitleContainer({
Text( Text(
title, title,
style: textTheme.titleMedium! style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.subtleText), .copyWith(color: titleColor ?? scale.primaryScale.borderText),
).paddingLTRB(8, 8, 8, 4), ).paddingLTRB(8, 8, 8, 4),
DecoratedBox( DecoratedBox(
decoration: ShapeDecoration( decoration: ShapeDecoration(
@ -174,6 +195,7 @@ Widget styledBottomSheet({
required Widget child, required Widget child,
Color? borderColor, Color? borderColor,
Color? backgroundColor, Color? backgroundColor,
Color? titleColor,
}) { }) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
@ -181,7 +203,7 @@ Widget styledBottomSheet({
return DecoratedBox( return DecoratedBox(
decoration: ShapeDecoration( decoration: ShapeDecoration(
color: borderColor ?? scale.primaryScale.border, color: borderColor ?? scale.primaryScale.dialogBorder,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
@ -190,7 +212,7 @@ Widget styledBottomSheet({
Text( Text(
title, title,
style: textTheme.titleMedium! style: textTheme.titleMedium!
.copyWith(color: scale.primaryScale.subtleText), .copyWith(color: titleColor ?? scale.primaryScale.borderText),
).paddingLTRB(8, 8, 8, 4), ).paddingLTRB(8, 8, 8, 4),
DecoratedBox( DecoratedBox(
decoration: ShapeDecoration( decoration: ShapeDecoration(

View File

@ -52,27 +52,23 @@ class _EnterPasswordDialogState extends State<EnterPasswordDialog> {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
return Dialog( return StyledDialog(
backgroundColor: scale.grayScale.subtleBackground, title: widget.matchPass == null
? translate('enter_password_dialog.enter_password')
: translate('enter_password_dialog.reenter_password'),
child: Form( child: Form(
key: formKey, key: formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(
widget.matchPass == null
? translate('enter_password_dialog.enter_password')
: translate('enter_password_dialog.reenter_password'),
style: theme.textTheme.titleLarge,
).paddingAll(16),
TextField( TextField(
controller: passwordController, controller: passwordController,
focusNode: focusNode, focusNode: focusNode,
autofocus: true, autofocus: true,
enableSuggestions: false, enableSuggestions: false,
obscureText: obscureText: !_passwordVisible,
!_passwordVisible, //This will obscure text dynamically obscuringCharacter: '*',
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.singleLineFormatter FilteringTextInputFormatter.singleLineFormatter
], ],
@ -87,7 +83,7 @@ class _EnterPasswordDialogState extends State<EnterPasswordDialog> {
? null ? null
: Icon(Icons.check_circle, : Icon(Icons.check_circle,
color: passwordController.text == widget.matchPass color: passwordController.text == widget.matchPass
? scale.primaryScale.background ? scale.primaryScale.primary
: scale.grayScale.subtleBackground), : scale.grayScale.subtleBackground),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(

View File

@ -67,20 +67,16 @@ class _EnterPinDialogState extends State<EnterPinDialog> {
); );
/// Optionally you can use form to validate the Pinput /// Optionally you can use form to validate the Pinput
return Dialog( return StyledDialog(
backgroundColor: scale.grayScale.subtleBackground, title: !widget.reenter
? translate('enter_pin_dialog.enter_pin')
: translate('enter_pin_dialog.reenter_pin'),
child: Form( child: Form(
key: formKey, key: formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text(
!widget.reenter
? translate('enter_pin_dialog.enter_pin')
: translate('enter_pin_dialog.reenter_pin'),
style: theme.textTheme.titleLarge,
).paddingAll(16),
Directionality( Directionality(
// Specify direction if desired // Specify direction if desired
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,

View File

@ -5,10 +5,7 @@ export 'loggy.dart';
export 'phono_byte.dart'; export 'phono_byte.dart';
export 'pop_control.dart'; export 'pop_control.dart';
export 'responsive.dart'; export 'responsive.dart';
export 'scanner_error_widget.dart';
export 'shared_preferences.dart'; export 'shared_preferences.dart';
export 'state_logger.dart'; export 'state_logger.dart';
export 'stream_listenable.dart'; export 'stream_listenable.dart';
export 'styled_dialog.dart';
export 'widget_helpers.dart';
export 'window_control.dart'; export 'window_control.dart';

View File

@ -140,17 +140,18 @@ class _DeveloperPageState extends State<DeveloperPage> {
// }); // });
return Scaffold( return Scaffold(
backgroundColor: scale.primaryScale.primary,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: Text(translate('developer.title')), title: Text(translate('developer.title')),
leading: IconButton( leading: IconButton(
icon: Icon(Icons.arrow_back, color: scale.primaryScale.appText), icon: Icon(Icons.arrow_back, color: scale.primaryScale.primaryText),
onPressed: () => GoRouterHelper(context).pop(), onPressed: () => GoRouterHelper(context).pop(),
), ),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),
color: scale.primaryScale.appText, color: scale.primaryScale.primaryText,
disabledColor: scale.grayScale.subtleText, disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F),
onPressed: _terminalController.selection == null onPressed: _terminalController.selection == null
? null ? null
: () async { : () async {
@ -158,17 +159,22 @@ class _DeveloperPageState extends State<DeveloperPage> {
}), }),
IconButton( IconButton(
icon: const Icon(Icons.clear_all), icon: const Icon(Icons.clear_all),
color: scale.primaryScale.appText, color: scale.primaryScale.primaryText,
disabledColor: scale.grayScale.subtleText, disabledColor: scale.primaryScale.primaryText.withAlpha(0x3F),
onPressed: () async { onPressed: () async {
await QuickAlert.show( await QuickAlert.show(
context: context, context: context,
type: QuickAlertType.confirm, type: QuickAlertType.confirm,
title: translate('developer.are_you_sure_clear'), title: translate('developer.are_you_sure_clear'),
textColor: scale.primaryScale.appText, titleColor: scale.primaryScale.appText,
confirmBtnColor: scale.primaryScale.elementBackground, textColor: scale.primaryScale.subtleText,
backgroundColor: scale.primaryScale.subtleBackground, confirmBtnColor: scale.primaryScale.primary,
headerBackgroundColor: scale.primaryScale.background, cancelBtnTextStyle: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 18,
color: scale.primaryScale.appText),
backgroundColor: scale.primaryScale.appBackground,
headerBackgroundColor: scale.primaryScale.primary,
confirmBtnText: translate('button.ok'), confirmBtnText: translate('button.ok'),
cancelBtnText: translate('button.cancel'), cancelBtnText: translate('button.cancel'),
onConfirmBtnTap: () async { onConfirmBtnTap: () async {
@ -194,13 +200,23 @@ class _DeveloperPageState extends State<DeveloperPage> {
width: 64, width: 64,
height: 40, height: 40,
render: ResultRender.icon, render: ResultRender.icon,
icon: SizedBox(
width: 10,
height: 10,
child: CustomPaint(
painter: DropdownArrowPainter(
color: scale.primaryScale.primaryText))),
textStyle: textTheme.labelMedium! textStyle: textTheme.labelMedium!
.copyWith(color: scale.primaryScale.appText), .copyWith(color: scale.primaryScale.primaryText),
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
openBoxDecoration: BoxDecoration( openBoxDecoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground), color: scale.primaryScale.border,
boxDecoration: borderRadius: BorderRadius.circular(8),
BoxDecoration(color: scale.primaryScale.elementBackground), ),
boxDecoration: BoxDecoration(
color: scale.primaryScale.hoverBorder,
borderRadius: BorderRadius.circular(8),
),
), ),
dropdownOptions: DropdownOptions( dropdownOptions: DropdownOptions(
width: 160, width: 160,
@ -224,7 +240,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)), selectedPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)),
dropdownList: _logLevelDropdownItems, dropdownList: _logLevelDropdownItems,
) ).paddingLTRB(0, 0, 8, 0)
], ],
), ),
body: SafeArea( body: SafeArea(
@ -245,13 +261,19 @@ class _DeveloperPageState extends State<DeveloperPage> {
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2), contentPadding: const EdgeInsets.fromLTRB(8, 2, 8, 2),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scale.primaryScale.border)), ),
fillColor: scale.primaryScale.subtleBackground, fillColor: scale.primaryScale.subtleBackground,
hintText: translate('developer.command'), hintText: translate('developer.command'),
suffixIcon: IconButton( 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 onPressed: _debugCommandController.text.isEmpty
? null ? null
: () async { : () async {

View File

@ -33,32 +33,32 @@ class SignalStrengthMeterWidget extends StatelessWidget {
switch (connectionState.attachment.state) { switch (connectionState.attachment.state) {
case AttachmentState.detached: case AttachmentState.detached:
iconWidget = Icon(Icons.signal_cellular_nodata, iconWidget = Icon(Icons.signal_cellular_nodata,
size: iconSize, color: scale.grayScale.appText); size: iconSize, color: scale.primaryScale.primaryText);
return; return;
case AttachmentState.detaching: case AttachmentState.detaching:
iconWidget = Icon(Icons.signal_cellular_off, iconWidget = Icon(Icons.signal_cellular_off,
size: iconSize, color: scale.grayScale.appText); size: iconSize, color: scale.primaryScale.primaryText);
return; return;
case AttachmentState.attaching: case AttachmentState.attaching:
value = 0; value = 0;
color = scale.primaryScale.appText; color = scale.primaryScale.primaryText;
case AttachmentState.attachedWeak: case AttachmentState.attachedWeak:
value = 1; value = 1;
color = scale.primaryScale.appText; color = scale.primaryScale.primaryText;
case AttachmentState.attachedStrong: case AttachmentState.attachedStrong:
value = 2; value = 2;
color = scale.primaryScale.appText; color = scale.primaryScale.primaryText;
case AttachmentState.attachedGood: case AttachmentState.attachedGood:
value = 3; value = 3;
color = scale.primaryScale.appText; color = scale.primaryScale.primaryText;
case AttachmentState.fullyAttached: case AttachmentState.fullyAttached:
value = 4; value = 4;
color = scale.primaryScale.appText; color = scale.primaryScale.primaryText;
case AttachmentState.overAttached: case AttachmentState.overAttached:
value = 4; value = 4;
color = scale.secondaryScale.subtleText; color = scale.primaryScale.primaryText;
} }
inactiveColor = scale.grayScale.subtleText; inactiveColor = scale.primaryScale.primaryText;
iconWidget = SignalStrengthIndicator.bars( iconWidget = SignalStrengthIndicator.bars(
value: value, value: value,
@ -66,7 +66,7 @@ class SignalStrengthMeterWidget extends StatelessWidget {
inactiveColor: inactiveColor, inactiveColor: inactiveColor,
size: iconSize, size: iconSize,
barCount: 4, barCount: 4,
spacing: 1); spacing: 2);
}, },
loading: () => {iconWidget = const Icon(Icons.warning)}, loading: () => {iconWidget = const Icon(Icons.warning)},
error: (e, st) => { error: (e, st) => {

View File

@ -69,7 +69,7 @@ class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
// Because this is async, we could get an update while we're // Because this is async, we could get an update while we're
// still processing the last one. Only called after init future has run // still processing the last one. Only called after init future has run
// so we dont have to wait for that here. // so we dont have to wait for that here.
_sspUpdate.busyUpdate<T, AsyncValue<IList<T>>>( _sspUpdate.busyUpdate<T, DHTShortArrayState<T>>(
busy, (emit) async => _refreshInner(emit)); busy, (emit) async => _refreshInner(emit));
} }

View File

@ -470,6 +470,8 @@ class _DHTShortArrayHead {
_subscription = null; _subscription = null;
} }
// Called when the shortarray changes online and we find out from a watch
// but not when we make a change locally
Future<void> _onHeadValueChanged( Future<void> _onHeadValueChanged(
DHTRecord record, Uint8List? data, List<ValueSubkeyRange> subkeys) async { DHTRecord record, Uint8List? data, List<ValueSubkeyRange> subkeys) async {
// If head record subkey zero changes, then the layout // If head record subkey zero changes, then the layout