Merge branch 'debugging' into 'main'

Improved contact invitation menus

See merge request veilid/veilidchat!37
This commit is contained in:
Christien Rioux 2025-03-19 23:30:34 -04:00
commit e4358586aa
30 changed files with 679 additions and 802 deletions

View file

@ -158,9 +158,8 @@
"away": "Away" "away": "Away"
}, },
"add_contact_sheet": { "add_contact_sheet": {
"new_contact": "New Contact", "add_contact": "Add Contact",
"create_invite": "Create\nInvitation", "create_invite": "Create\nInvitation",
"receive_invite": "Receive\nInvitation",
"scan_invite": "Scan\nInvitation", "scan_invite": "Scan\nInvitation",
"paste_invite": "Paste\nInvitation" "paste_invite": "Paste\nInvitation"
}, },

View file

@ -54,8 +54,6 @@ PODS:
- nanopb/encode (= 3.30910.0) - nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0) - nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0) - nanopb/encode (3.30910.0)
- native_device_orientation (0.0.1):
- Flutter
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- pasteboard (0.0.1): - pasteboard (0.0.1):
@ -87,7 +85,6 @@ DEPENDENCIES:
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- native_device_orientation (from `.symlinks/plugins/native_device_orientation/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@ -124,8 +121,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
mobile_scanner: mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios" :path: ".symlinks/plugins/mobile_scanner/ios"
native_device_orientation:
:path: ".symlinks/plugins/native_device_orientation/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
pasteboard: pasteboard:
@ -151,7 +146,7 @@ SPEC CHECKSUMS:
camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318 GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8 GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
@ -163,7 +158,6 @@ SPEC CHECKSUMS:
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
native_device_orientation: e3580675687d5034770da198f6839ebf2122ef94
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564

View file

@ -14,7 +14,6 @@ class ActiveLocalAccountCubit extends Cubit<TypedKey?> {
switch (change) { switch (change) {
case AccountRepositoryChange.activeLocalAccount: case AccountRepositoryChange.activeLocalAccount:
emit(_accountRepository.getActiveLocalAccount()); emit(_accountRepository.getActiveLocalAccount());
break;
// Ignore these // Ignore these
case AccountRepositoryChange.localAccounts: case AccountRepositoryChange.localAccounts:
case AccountRepositoryChange.userLogins: case AccountRepositoryChange.userLogins:

View file

@ -32,6 +32,7 @@ class PerAccountCollectionState with _$PerAccountCollectionState {
} }
extension PerAccountCollectionStateExt on PerAccountCollectionState { extension PerAccountCollectionStateExt on PerAccountCollectionState {
// Returns if the account is ready and logged in
bool get isReady => bool get isReady =>
avAccountRecordState != null && avAccountRecordState != null &&
avAccountRecordState!.isData && avAccountRecordState!.isData &&
@ -45,7 +46,11 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState {
activeConversationsBlocMapCubit != null && activeConversationsBlocMapCubit != null &&
activeSingleContactChatBlocMapCubit != null; activeSingleContactChatBlocMapCubit != null;
Widget provide({required Widget child}) => MultiBlocProvider(providers: [ /// If we have a selected account and it is ready and not locked,
/// this will provide the unlocked account's cubits to the context
Widget provideReady({required Widget child}) {
if (isReady) {
return MultiBlocProvider(providers: [
BlocProvider.value(value: accountInfoCubit!), BlocProvider.value(value: accountInfoCubit!),
BlocProvider.value(value: accountRecordCubit!), BlocProvider.value(value: accountRecordCubit!),
BlocProvider.value(value: contactInvitationListCubit!), BlocProvider.value(value: contactInvitationListCubit!),
@ -56,4 +61,9 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState {
BlocProvider.value(value: activeConversationsBlocMapCubit!), BlocProvider.value(value: activeConversationsBlocMapCubit!),
BlocProvider.value(value: activeSingleContactChatBlocMapCubit!), BlocProvider.value(value: activeSingleContactChatBlocMapCubit!),
], child: child); ], child: child);
} else {
// Otherwise we just provide the child
return child;
}
}
} }

View file

@ -100,19 +100,9 @@ class VeilidChatApp extends StatelessWidget {
onInvoke: (intent) => _attachDetach(context)), onInvoke: (intent) => _attachDetach(context)),
}, child: Focus(autofocus: true, child: builder(context))))); }, child: Focus(autofocus: true, child: builder(context)))));
@override Widget appBuilder(
Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>( BuildContext context, LocalizationDelegate localizationDelegate) =>
initialData: null, ThemeProvider(
create: (context) async => VeilidChatGlobalInit.initialize(),
builder: (context, __) {
final globalInit = context.watch<VeilidChatGlobalInit?>();
if (globalInit == null) {
// Splash screen until we're done with init
return const Splash();
}
// Once init is done, we proceed with the app
final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider(
initTheme: initialThemeData, initTheme: initialThemeData,
builder: (context, theme) => LocalizationProvider( builder: (context, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state, state: LocalizationProvider.of(context).state,
@ -129,8 +119,7 @@ class VeilidChatApp extends StatelessWidget {
create: (context) => create: (context) =>
ConnectionStateCubit(ProcessorRepository.instance)), ConnectionStateCubit(ProcessorRepository.instance)),
BlocProvider<RouterCubit>( BlocProvider<RouterCubit>(
create: (context) => create: (context) => RouterCubit(AccountRepository.instance),
RouterCubit(AccountRepository.instance),
), ),
BlocProvider<LocalAccountsCubit>( BlocProvider<LocalAccountsCubit>(
create: (context) => create: (context) =>
@ -191,14 +180,28 @@ class VeilidChatApp extends StatelessWidget {
FormBuilderLocalizations.delegate, FormBuilderLocalizations.delegate,
localizationDelegate localizationDelegate
], ],
supportedLocales: supportedLocales: localizationDelegate.supportedLocales,
localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale, locale: localizationDelegate.currentLocale,
) )
]); ]);
})), })),
)), )),
); );
@override
Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>(
initialData: null,
create: (context) async => VeilidChatGlobalInit.initialize(),
builder: (context, __) {
final globalInit = context.watch<VeilidChatGlobalInit?>();
if (globalInit == null) {
// Splash screen until we're done with init
return const Splash();
}
// Once init is done, we proceed with the app
final localizationDelegate = LocalizedApp.of(context).delegate;
return SafeArea(child: appBuilder(context, localizationDelegate));
}); });
@override @override

View file

@ -33,11 +33,6 @@ class ChatComponentWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// final theme = Theme.of(context);
// final scale = theme.extension<ScaleScheme>()!;
// final scaleConfig = theme.extension<ScaleConfig>()!;
// final textTheme = theme.textTheme;
// Get the account info // Get the account info
final accountInfo = context.watch<AccountInfoCubit>().state; final accountInfo = context.watch<AccountInfoCubit>().state;
@ -125,7 +120,7 @@ class ChatComponentWidget extends StatelessWidget {
return Column( return Column(
children: [ children: [
Container( Container(
height: 48, height: 40,
decoration: BoxDecoration( decoration: BoxDecoration(
color: scale.border, color: scale.border,
), ),
@ -141,9 +136,10 @@ class ChatComponentWidget extends StatelessWidget {
)), )),
const Spacer(), const Spacer(),
IconButton( IconButton(
iconSize: 24,
icon: Icon(Icons.close, color: scale.borderText), icon: Icon(Icons.close, color: scale.borderText),
onPressed: _onClose) onPressed: _onClose)
.paddingLTRB(16, 0, 16, 0) .paddingLTRB(0, 0, 8, 0)
]), ]),
), ),
DecoratedBox( DecoratedBox(

View file

@ -1,31 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/theme.dart';
Widget newChatBottomSheetBuilder(
BuildContext sheetContext, BuildContext context) {
//final theme = Theme.of(sheetContext);
//final scale = theme.extension<ScaleScheme>()!;
return KeyboardListener(
focusNode: FocusNode(),
onKeyEvent: (ke) {
if (ke.logicalKey == LogicalKeyboardKey.escape) {
Navigator.pop(sheetContext);
}
},
child: styledBottomSheet(
context: context,
title: translate('add_chat_sheet.new_chat'),
child: SizedBox(
height: 160,
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
'Group and custom chat functionality is not available yet')
]).paddingAll(16))));
}

View file

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../theme/models/scale_theme/scale_scheme.dart'; import '../../theme/theme.dart';
class NoConversationWidget extends StatelessWidget { class NoConversationWidget extends StatelessWidget {
const NoConversationWidget({super.key}); const NoConversationWidget({super.key});
@ -37,7 +37,6 @@ class NoConversationWidget extends StatelessWidget {
), ),
), ),
], ],
), ));
);
} }
} }

View file

@ -1,4 +1,3 @@
export 'chat_component_widget.dart'; export 'chat_component_widget.dart';
export 'empty_chat_widget.dart'; export 'empty_chat_widget.dart';
export 'new_chat_bottom_sheet.dart';
export 'no_conversation_widget.dart'; export 'no_conversation_widget.dart';

View file

@ -48,7 +48,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
key: ObjectKey(contactInvitationRecord), key: ObjectKey(contactInvitationRecord),
disabled: tileDisabled, disabled: tileDisabled,
selected: selected, selected: selected,
tileScale: ScaleKind.primary, tileScale: ScaleKind.secondary,
title: title, title: title,
leading: const Icon(Icons.person_add), leading: const Icon(Icons.person_add),
onTap: () async { onTap: () async {

View file

@ -17,19 +17,25 @@ import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart'; import 'empty_contact_list_widget.dart';
enum ContactsBrowserElementKind { enum ContactsBrowserElementKind {
invitation,
contact, contact,
invitation,
} }
class ContactsBrowserElement { class ContactsBrowserElement {
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
: kind = ContactsBrowserElementKind.invitation,
contact = null,
invitation = i;
ContactsBrowserElement.contact(proto.Contact c) ContactsBrowserElement.contact(proto.Contact c)
: kind = ContactsBrowserElementKind.contact, : kind = ContactsBrowserElementKind.contact,
invitation = null, invitation = null,
contact = c; contact = c;
ContactsBrowserElement.invitation(proto.ContactInvitationRecord i)
: kind = ContactsBrowserElementKind.invitation,
contact = null,
invitation = i;
String get sortKey => switch (kind) {
ContactsBrowserElementKind.contact => contact!.displayName,
ContactsBrowserElementKind.invitation =>
invitation!.recipient + invitation!.message
};
final ContactsBrowserElementKind kind; final ContactsBrowserElementKind kind;
final proto.ContactInvitationRecord? invitation; final proto.ContactInvitationRecord? invitation;
@ -66,27 +72,25 @@ class ContactsBrowser extends StatefulWidget {
class _ContactsBrowserState extends State<ContactsBrowser> class _ContactsBrowserState extends State<ContactsBrowser>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
Widget buildInvitationBar(BuildContext context) { Widget buildInvitationButton(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final textTheme = theme.textTheme; final scaleScheme = theme.extension<ScaleScheme>()!;
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
final menuIconColor = scaleConfig.preferBorders final menuIconColor = scaleConfig.preferBorders
? scale.primaryScale.hoverBorder ? scaleScheme.primaryScale.hoverBorder
: scale.primaryScale.hoverBorder; : scaleScheme.primaryScale.hoverBorder;
final menuBackgroundColor = scaleConfig.preferBorders final menuBackgroundColor = scaleConfig.preferBorders
? scale.primaryScale.elementBackground ? scaleScheme.primaryScale.activeElementBackground
: scale.primaryScale.elementBackground; : scaleScheme.primaryScale.activeElementBackground;
final menuBorderColor = scale.primaryScale.hoverBorder; final menuBorderColor = scaleScheme.primaryScale.hoverBorder;
final menuParams = StarMenuParameters( final menuParams = StarMenuParameters(
shape: MenuShape.grid, shape: MenuShape.linear,
checkItemsScreenBoundaries: true,
centerOffset: const Offset(0, 64), centerOffset: const Offset(0, 64),
backgroundParams: // backgroundParams:
BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)), // BackgroundParams(backgroundColor: theme.shadowColor.withAlpha(128)),
boundaryBackground: BoundaryBackground( boundaryBackground: BoundaryBackground(
color: menuBackgroundColor, color: menuBackgroundColor,
decoration: ShapeDecoration( decoration: ShapeDecoration(
@ -99,89 +103,64 @@ class _ContactsBrowserState extends State<ContactsBrowser>
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
8 * scaleConfig.borderRadiusScale))))); 8 * scaleConfig.borderRadiusScale)))));
final receiveInviteMenuItems = [ ElevatedButton makeMenuButton(
Column(mainAxisSize: MainAxisSize.min, children: [ {required IconData iconData,
IconButton( required String text,
onPressed: () async { required void Function()? onPressed}) =>
_receiveInviteMenuController.closeMenu!(); ElevatedButton.icon(
await ScanInvitationDialog.show(context); onPressed: onPressed,
},
iconSize: 32,
icon: Icon( icon: Icon(
Icons.qr_code_scanner, iconData,
size: 32, size: 32,
color: menuIconColor, ).paddingSTEB(0, 8, 0, 8),
), label: Text(
), text,
Text(translate('add_contact_sheet.scan_invite'),
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.labelSmall!.copyWith(color: menuIconColor)) ).paddingSTEB(0, 8, 0, 8));
]).paddingAll(4),
Column(mainAxisSize: MainAxisSize.min, children: [ final inviteMenuItems = [
IconButton( makeMenuButton(
iconData: Icons.paste,
text: translate('add_contact_sheet.paste_invite'),
onPressed: () async { onPressed: () async {
_receiveInviteMenuController.closeMenu!(); _invitationMenuController.closeMenu!();
await PasteInvitationDialog.show(context); await PasteInvitationDialog.show(context);
}, }),
iconSize: 32, makeMenuButton(
icon: Icon( iconData: Icons.qr_code_scanner,
Icons.paste, text: translate('add_contact_sheet.scan_invite'),
size: 32, onPressed: () async {
color: menuIconColor, _invitationMenuController.closeMenu!();
), await ScanInvitationDialog.show(context);
), }).paddingLTRB(0, 0, 0, 8),
Text(translate('add_contact_sheet.paste_invite'), makeMenuButton(
maxLines: 2, iconData: Icons.contact_page,
textAlign: TextAlign.center, text: translate('add_contact_sheet.create_invite'),
style: textTheme.labelSmall!.copyWith(color: menuIconColor)) onPressed: () async {
]).paddingAll(4) _invitationMenuController.closeMenu!();
await CreateInvitationDialog.show(context);
}).paddingLTRB(0, 0, 0, 8),
]; ];
return Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ return StarMenu(
Column(mainAxisSize: MainAxisSize.min, children: [ items: inviteMenuItems,
IconButton(
onPressed: () async {
await CreateInvitationDialog.show(context);
},
iconSize: 32,
icon: const Icon(Icons.contact_page),
color: menuIconColor,
),
Text(translate('add_contact_sheet.create_invite'),
maxLines: 2,
textAlign: TextAlign.center,
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
]),
StarMenu(
items: receiveInviteMenuItems,
onItemTapped: (_index, controller) { onItemTapped: (_index, controller) {
controller.closeMenu!(); controller.closeMenu!();
}, },
controller: _receiveInviteMenuController, controller: _invitationMenuController,
params: menuParams, params: menuParams,
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: IconButton(
IconButton(
onPressed: () {}, onPressed: () {},
iconSize: 32, iconSize: 24,
icon: ImageIcon( icon: Icon(Icons.person_add, color: menuIconColor),
const AssetImage('assets/images/handshake.png'), tooltip: translate('add_contact_sheet.add_contact')),
size: 32, );
color: menuIconColor,
)),
Text(translate('add_contact_sheet.receive_invite'),
maxLines: 2,
textAlign: TextAlign.center,
style: textTheme.labelSmall!.copyWith(color: menuIconColor))
]),
),
]).paddingAll(16);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
//final scaleConfig = theme.extension<ScaleConfig>()!; //final scaleConfig = theme.extension<ScaleConfig>()!;
@ -196,40 +175,23 @@ class _ContactsBrowserState extends State<ContactsBrowser>
final contactList = final contactList =
ciState.state.asData?.value.map((x) => x.value).toIList(); ciState.state.asData?.value.map((x) => x.value).toIList();
final expansionListData = final initialList = <ContactsBrowserElement>[];
<ContactsBrowserElementKind, List<ContactsBrowserElement>>{};
if (contactInvitationRecordList.isNotEmpty) {
expansionListData[ContactsBrowserElementKind.invitation] =
contactInvitationRecordList
.toList()
.map(ContactsBrowserElement.invitation)
.toList();
}
if (contactList != null) { if (contactList != null) {
expansionListData[ContactsBrowserElementKind.contact] = initialList
contactList.toList().map(ContactsBrowserElement.contact).toList(); .addAll(contactList.toList().map(ContactsBrowserElement.contact));
} }
if (contactInvitationRecordList.isNotEmpty) {
initialList.addAll(contactInvitationRecordList
.toList()
.map(ContactsBrowserElement.invitation));
}
initialList.sort((a, b) => a.sortKey.compareTo(b.sortKey));
return Column(children: [ return Column(children: [
buildInvitationBar(context), SearchableList<ContactsBrowserElement>(
SearchableList<ContactsBrowserElement>.expansion( initialList: initialList,
expansionListData: expansionListData, itemBuilder: (element) {
expansionTitleBuilder: (k) {
final kind = k as ContactsBrowserElementKind;
late final String title;
switch (kind) {
case ContactsBrowserElementKind.contact:
title = translate('contacts_dialog.contacts');
case ContactsBrowserElementKind.invitation:
title = translate('contacts_dialog.invitations');
}
return Center(
child: Text(title, style: textTheme.titleSmall),
);
},
expansionInitiallyExpanded: (k) => true,
expansionListBuilder: (_index, element) {
switch (element.kind) { switch (element.kind) {
case ContactsBrowserElementKind.contact: case ContactsBrowserElementKind.contact:
final contact = element.contact!; final contact = element.contact!;
@ -245,51 +207,57 @@ class _ContactsBrowserState extends State<ContactsBrowser>
case ContactsBrowserElementKind.invitation: case ContactsBrowserElementKind.invitation:
final invitation = element.invitation!; final invitation = element.invitation!;
return ContactInvitationItemWidget( return ContactInvitationItemWidget(
contactInvitationRecord: invitation, disabled: false) contactInvitationRecord: invitation,
disabled: false)
.paddingLTRB(0, 4, 0, 0); .paddingLTRB(0, 4, 0, 0);
} }
}, },
filterExpansionData: (value) { filter: (value) {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();
final filteredMap = {
for (final entry in expansionListData.entries) final filtered = <ContactsBrowserElement>[];
entry.key: (expansionListData[entry.key] ?? []).where((element) { for (final element in initialList) {
switch (element.kind) { switch (element.kind) {
case ContactsBrowserElementKind.contact: case ContactsBrowserElementKind.contact:
final contact = element.contact!; final contact = element.contact!;
return contact.nickname if (contact.nickname.toLowerCase().contains(lowerValue) ||
.toLowerCase()
.contains(lowerValue) ||
contact.profile.name contact.profile.name
.toLowerCase() .toLowerCase()
.contains(lowerValue) || .contains(lowerValue) ||
contact.profile.pronouns contact.profile.pronouns
.toLowerCase() .toLowerCase()
.contains(lowerValue); .contains(lowerValue)) {
filtered.add(element);
}
case ContactsBrowserElementKind.invitation: case ContactsBrowserElementKind.invitation:
final invitation = element.invitation!; final invitation = element.invitation!;
return invitation.message if (invitation.message
.toLowerCase() .toLowerCase()
.contains(lowerValue) || .contains(lowerValue) ||
invitation.recipient.toLowerCase().contains(lowerValue); invitation.recipient
.toLowerCase()
.contains(lowerValue)) {
filtered.add(element);
} }
}).toList() }
}; }
return filteredMap; return filtered;
}, },
hideEmptyExpansionItems: true,
searchFieldHeight: 40, searchFieldHeight: 40,
listViewPadding: const EdgeInsets.all(4), listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4),
spaceBetweenSearchAndList: 4, searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4),
emptyWidget: contactList == null emptyWidget: contactList == null
? waitingPage(text: translate('contact_list.loading_contacts')) ? waitingPage(
text: translate('contact_list.loading_contacts'))
: const EmptyContactListWidget(), : const EmptyContactListWidget(),
defaultSuffixIconColor: scale.primaryScale.border, defaultSuffixIconColor: scale.primaryScale.border,
closeKeyboardWhenScrolling: true, closeKeyboardWhenScrolling: true,
searchFieldEnabled: contactList != null, searchFieldEnabled: contactList != null,
inputDecoration: inputDecoration:
InputDecoration(labelText: translate('contact_list.search')), InputDecoration(labelText: translate('contact_list.search')),
).expanded() secondaryWidget:
buildInvitationButton(context).paddingLTRB(4, 0, 0, 0))
.expanded()
]); ]);
} }
@ -318,5 +286,5 @@ class _ContactsBrowserState extends State<ContactsBrowser>
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
final _receiveInviteMenuController = StarMenuController(); final _invitationMenuController = StarMenuController();
} }

View file

@ -1,196 +0,0 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import '../../chat/chat.dart';
import '../../chat_list/chat_list.dart';
import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../contacts.dart';
const _kDoBackArrow = 'doBackArrow';
class ContactsDialog extends StatefulWidget {
const ContactsDialog._({required this.modalContext});
@override
State<ContactsDialog> createState() => _ContactsDialogState();
static Future<void> show(BuildContext modalContext) async {
await showDialog<void>(
context: modalContext,
barrierDismissible: false,
useRootNavigator: false,
builder: (context) => ContactsDialog._(modalContext: modalContext));
}
final BuildContext modalContext;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
.add(DiagnosticsProperty<BuildContext>('modalContext', modalContext));
}
}
class _ContactsDialogState extends State<ContactsDialog> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final appBarIconColor = scale.primaryScale.borderText;
final enableSplit = !isMobileWidth(context);
final enableLeft = enableSplit || _selectedContact == null;
final enableRight = enableSplit || _selectedContact != null;
return SizedBox(
width: MediaQuery.of(context).size.width,
child: StyledScaffold(
appBar: DefaultAppBar(
title: Text(!enableSplit && enableRight
? translate('contacts_dialog.edit_contact')
: translate('contacts_dialog.contacts')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
singleFuture((this, _kDoBackArrow), () async {
final confirmed = await _onContactSelected(null);
if (!enableSplit && enableRight) {
} else {
if (confirmed) {
if (context.mounted) {
Navigator.pop(context);
}
}
}
});
},
),
actions: [
if (_selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child:
Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.chat_bubble),
color: appBarIconColor,
tooltip: translate('contacts_dialog.new_chat'),
onPressed: () async {
await _onChatStarted(_selectedContact!);
}),
Text(translate('contacts_dialog.new_chat'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
if (enableSplit && _selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child:
Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.close),
color: appBarIconColor,
tooltip:
translate('contacts_dialog.close_contact'),
onPressed: () async {
await _onContactSelected(null);
}),
Text(translate('contacts_dialog.close_contact'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
]),
body: LayoutBuilder(builder: (context, constraint) {
final maxWidth = constraint.maxWidth;
return ColoredBox(
color: scale.primaryScale.appBackground,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Offstage(
offstage: !enableLeft,
child: SizedBox(
width: enableLeft && !enableRight
? maxWidth
: (maxWidth / 3).clamp(200, 500),
child: DecoratedBox(
decoration: BoxDecoration(
color: scale
.primaryScale.subtleBackground),
child: ContactsBrowser(
selectedContactRecordKey: _selectedContact
?.localConversationRecordKey
.toVeilid(),
onContactSelected: _onContactSelected,
onStartChat: _onChatStarted,
).paddingLTRB(8, 0, 8, 8)))),
if (enableRight && enableLeft)
Container(
constraints: const BoxConstraints(
minWidth: 1, maxWidth: 1),
color: scale.primaryScale.subtleBorder),
if (enableRight)
if (_selectedContact == null)
const NoContactWidget().expanded()
else
ContactDetailsWidget(
contact: _selectedContact!,
onModifiedState: _onModifiedState)
.paddingLTRB(16, 16, 16, 16)
.expanded(),
]));
})));
}
void _onModifiedState(bool isModified) {
setState(() {
_isModified = isModified;
});
}
Future<bool> _onContactSelected(proto.Contact? contact) async {
if (contact != _selectedContact && _isModified) {
final ok = await showConfirmModal(
context: context,
title: translate('confirmation.discard_changes'),
text: translate('confirmation.are_you_sure_discard'));
if (!ok) {
return false;
}
}
setState(() {
_selectedContact = contact;
_isModified = false;
});
return true;
}
Future<void> _onChatStarted(proto.Contact contact) async {
final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
if (mounted) {
context
.read<ActiveChatCubit>()
.setActiveChat(contact.localConversationRecordKey.toVeilid());
Navigator.pop(context);
}
}
proto.Contact? _selectedContact;
bool _isModified = false;
}

View file

@ -0,0 +1,171 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart';
import '../../chat/chat.dart';
import '../../chat_list/chat_list.dart';
import '../../layout/layout.dart';
import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
import '../contacts.dart';
const _kDoBackArrow = 'doBackArrow';
class ContactsPage extends StatefulWidget {
const ContactsPage({super.key});
@override
State<ContactsPage> createState() => _ContactsPageState();
}
class _ContactsPageState extends State<ContactsPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final appBarIconColor = scale.primaryScale.borderText;
final enableSplit = !isMobileSize(context);
final enableLeft = enableSplit || _selectedContact == null;
final enableRight = enableSplit || _selectedContact != null;
return StyledScaffold(
appBar: DefaultAppBar(
title: Text(!enableSplit && enableRight
? translate('contacts_dialog.edit_contact')
: translate('contacts_dialog.contacts')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
singleFuture((this, _kDoBackArrow), () async {
final confirmed = await _onContactSelected(null);
if (!enableSplit && enableRight) {
} else {
if (confirmed) {
if (context.mounted) {
Navigator.pop(context);
}
}
}
});
},
),
actions: [
if (_selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child: Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.chat_bubble),
color: appBarIconColor,
tooltip: translate('contacts_dialog.new_chat'),
onPressed: () async {
await _onChatStarted(_selectedContact!);
}),
Text(translate('contacts_dialog.new_chat'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
if (enableSplit && _selectedContact != null)
FittedBox(
fit: BoxFit.scaleDown,
child: Column(mainAxisSize: MainAxisSize.min, children: [
IconButton(
icon: const Icon(Icons.close),
color: appBarIconColor,
tooltip: translate('contacts_dialog.close_contact'),
onPressed: () async {
await _onContactSelected(null);
}),
Text(translate('contacts_dialog.close_contact'),
style: theme.textTheme.labelSmall!
.copyWith(color: appBarIconColor)),
])).paddingLTRB(8, 0, 8, 0),
]),
body: LayoutBuilder(builder: (context, constraint) {
final maxWidth = constraint.maxWidth;
return ColoredBox(
color: scale.primaryScale.appBackground,
child:
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Offstage(
offstage: !enableLeft,
child: SizedBox(
width: enableLeft && !enableRight
? maxWidth
: (maxWidth / 3).clamp(200, 500),
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.subtleBackground),
child: ContactsBrowser(
selectedContactRecordKey: _selectedContact
?.localConversationRecordKey
.toVeilid(),
onContactSelected: _onContactSelected,
onStartChat: _onChatStarted,
).paddingLTRB(8, 0, 8, 8)))),
if (enableRight && enableLeft)
Container(
constraints:
const BoxConstraints(minWidth: 1, maxWidth: 1),
color: scale.primaryScale.subtleBorder),
if (enableRight)
if (_selectedContact == null)
const NoContactWidget().expanded()
else
ContactDetailsWidget(
contact: _selectedContact!,
onModifiedState: _onModifiedState)
.paddingLTRB(16, 16, 16, 16)
.expanded(),
]));
}));
}
void _onModifiedState(bool isModified) {
setState(() {
_isModified = isModified;
});
}
Future<bool> _onContactSelected(proto.Contact? contact) async {
if (contact != _selectedContact && _isModified) {
final ok = await showConfirmModal(
context: context,
title: translate('confirmation.discard_changes'),
text: translate('confirmation.are_you_sure_discard'));
if (!ok) {
return false;
}
}
setState(() {
_selectedContact = contact;
_isModified = false;
});
return true;
}
Future<void> _onChatStarted(proto.Contact contact) async {
final chatListCubit = context.read<ChatListCubit>();
await chatListCubit.getOrCreateChatSingleContact(contact: contact);
if (mounted) {
context
.read<ActiveChatCubit>()
.setActiveChat(contact.localConversationRecordKey.toVeilid());
Navigator.pop(context);
}
}
proto.Contact? _selectedContact;
bool _isModified = false;
}

View file

@ -2,7 +2,7 @@ export 'availability_widget.dart';
export 'contact_details_widget.dart'; export 'contact_details_widget.dart';
export 'contact_item_widget.dart'; export 'contact_item_widget.dart';
export 'contacts_browser.dart'; export 'contacts_browser.dart';
export 'contacts_dialog.dart'; export 'contacts_page.dart';
export 'edit_contact_form.dart'; export 'edit_contact_form.dart';
export 'empty_contact_list_widget.dart'; export 'empty_contact_list_widget.dart';
export 'no_contact_widget.dart'; export 'no_contact_widget.dart';

View file

@ -84,8 +84,9 @@ class _DrawerMenuState extends State<DrawerMenu> {
hoverBorder = border; hoverBorder = border;
activeBorder = border; activeBorder = border;
} else { } else {
background = background = selected
selected ? scale.activeElementBackground : scale.elementBackground; ? scale.elementBackground
: scale.elementBackground.withAlpha(128);
hoverBackground = scale.hoverElementBackground; hoverBackground = scale.hoverElementBackground;
activeBackground = scale.activeElementBackground; activeBackground = scale.activeElementBackground;
border = loggedIn ? scale.border : scale.subtleBorder; border = loggedIn ? scale.border : scale.subtleBorder;
@ -132,9 +133,16 @@ class _DrawerMenuState extends State<DrawerMenu> {
callback: callback, callback: callback,
footerButtonIcon: loggedIn ? Icons.edit_outlined : null, footerButtonIcon: loggedIn ? Icons.edit_outlined : null,
footerCallback: footerCallback, footerCallback: footerCallback,
footerButtonIconColor: border, footerButtonIconColor:
footerButtonIconHoverColor: hoverBackground, scaleConfig.preferBorders ? scale.border : scale.borderText,
footerButtonIconFocusColor: activeBackground, footerButtonIconHoverColor:
(scaleConfig.preferBorders || scaleConfig.useVisualIndicators)
? null
: hoverBorder,
footerButtonIconFocusColor:
(scaleConfig.preferBorders || scaleConfig.useVisualIndicators)
? null
: activeBorder,
minHeight: 48, minHeight: 48,
)); ));
} }
@ -318,7 +326,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
scale.subtleBorder, scale.subtleBorder,
]); ]);
return DecoratedBox( Widget menu = DecoratedBox(
decoration: ShapeDecoration( decoration: ShapeDecoration(
shadows: themedShadow(scaleConfig, scale), shadows: themedShadow(scaleConfig, scale),
gradient: scaleConfig.useVisualIndicators ? null : gradient, gradient: scaleConfig.useVisualIndicators ? null : gradient,
@ -393,6 +401,12 @@ class _DrawerMenuState extends State<DrawerMenu> {
), ),
]) ])
]).paddingAll(16), ]).paddingAll(16),
).paddingLTRB(0, 2, 2, 2); );
if (scaleConfig.preferBorders || scaleConfig.useVisualIndicators) {
menu = menu.paddingLTRB(0, 2, 2, 2);
}
return menu;
} }
} }

View file

@ -39,6 +39,8 @@ class MenuItemWidget extends StatelessWidget {
} }
return backgroundColor; return backgroundColor;
}), }),
overlayColor:
WidgetStateProperty.resolveWith((states) => backgroundHoverColor),
side: WidgetStateBorderSide.resolveWith((states) { side: WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
return borderColor != null return borderColor != null

View file

@ -89,11 +89,18 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
)), )),
tooltip: translate('menu.contacts_tooltip'), tooltip: translate('menu.contacts_tooltip'),
onPressed: () async { onPressed: () async {
await ContactsDialog.show(context); await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ContactsPage(),
),
);
}); });
}); });
Widget buildUserPanel() => Builder(builder: (context) { Widget buildLeftPane(BuildContext context) => Builder(
builder: (context) => Material(
color: Colors.transparent,
child: Builder(builder: (context) {
final profile = context.select<AccountRecordCubit, proto.Profile>( final profile = context.select<AccountRecordCubit, proto.Profile>(
(c) => c.state.asData!.value.profile); (c) => c.state.asData!.value.profile);
final theme = Theme.of(context); final theme = Theme.of(context);
@ -115,11 +122,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
]).paddingAll(8), ]).paddingAll(8),
const ChatListWidget().expanded() const ChatListWidget().expanded()
])); ]));
}); })));
Widget buildLeftPane(BuildContext context) => Builder(
builder: (context) =>
Material(color: Colors.transparent, child: buildUserPanel()));
Widget buildRightPane(BuildContext context) { Widget buildRightPane(BuildContext context) {
final activeChatCubit = context.watch<ActiveChatCubit>(); final activeChatCubit = context.watch<ActiveChatCubit>();
@ -140,10 +143,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLarge = responsiveVisibility( final isSmallScreen = isMobileSize(context);
context: context,
phone: false,
);
final theme = Theme.of(context); final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!; final scaleScheme = theme.extension<ScaleScheme>()!;
@ -160,14 +160,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
late final bool visibleRight; late final bool visibleRight;
late final double leftWidth; late final double leftWidth;
late final double rightWidth; late final double rightWidth;
if (isLarge) { if (isSmallScreen) {
visibleLeft = true;
visibleRight = true;
leftWidth = leftColumnSize;
rightWidth = constraints.maxWidth -
leftColumnSize -
(scaleConfig.useVisualIndicators ? 2 : 0);
} else {
if (hasActiveChat) { if (hasActiveChat) {
visibleLeft = false; visibleLeft = false;
visibleRight = true; visibleRight = true;
@ -179,6 +172,13 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
leftWidth = constraints.maxWidth; leftWidth = constraints.maxWidth;
rightWidth = 400; // whatever rightWidth = 400; // whatever
} }
} else {
visibleLeft = true;
visibleRight = true;
leftWidth = leftColumnSize;
rightWidth = constraints.maxWidth -
leftColumnSize -
(scaleConfig.useVisualIndicators ? 2 : 0);
} }
return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ return Row(crossAxisAlignment: CrossAxisAlignment.stretch, children: [

View file

@ -136,15 +136,11 @@ class HomeScreenState extends State<HomeScreen>
} }
// Re-export all ready blocs to the account display subtree // Re-export all ready blocs to the account display subtree
return perAccountCollectionState.provide( final pages = <MaterialPage<void>>[
child: Navigator( const MaterialPage<void>(child: HomeAccountReady())
onPopPage: (route, result) { ];
if (!route.didPop(result)) { return perAccountCollectionState.provideReady(
return false; child: Navigator(onDidRemovePage: pages.remove, pages: pages));
}
return true;
},
pages: const [MaterialPage(child: HomeAccountReady())]));
} }
} }
@ -208,8 +204,7 @@ class HomeScreenState extends State<HomeScreen>
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1; final canClose = activeIndex != -1;
return SafeArea( return DefaultTextStyle(
child: DefaultTextStyle(
style: theme.textTheme.bodySmall!, style: theme.textTheme.bodySmall!,
child: ZoomDrawer( child: ZoomDrawer(
controller: _zoomDrawerController, controller: _zoomDrawerController,
@ -237,7 +232,7 @@ class HomeScreenState extends State<HomeScreen>
disableDragGesture: !canClose, disableDragGesture: !canClose,
mainScreenScale: .25, mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
))); ));
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View file

@ -65,29 +65,49 @@ class RouterCubit extends Cubit<RouterState> {
), ),
GoRoute( GoRoute(
path: '/edit_account', path: '/edit_account',
redirect: (_, state) {
final extra = state.extra;
if (extra == null ||
extra is! List<Object> ||
extra[0] is! TypedKey) {
return '/';
}
return null;
},
builder: (context, state) { builder: (context, state) {
final extra = state.extra! as List<Object?>; final extra = state.extra! as List<Object>;
return EditAccountPage( return EditAccountPage(
superIdentityRecordKey: extra[0]! as TypedKey, superIdentityRecordKey: extra[0] as TypedKey,
initialValue: extra[1]! as AccountSpec, initialValue: extra[1] as AccountSpec,
accountRecord: extra[2]! as OwnedDHTRecordPointer, accountRecord: extra[2] as OwnedDHTRecordPointer,
); );
}, },
), ),
GoRoute( GoRoute(
path: '/new_account', path: '/new_account',
builder: (context, state) => const NewAccountPage(), builder: (context, state) => const NewAccountPage(),
), routes: [
GoRoute( GoRoute(
path: '/new_account/recovery_key', path: 'recovery_key',
redirect: (_, state) {
final extra = state.extra;
if (extra == null ||
extra is! List<Object> ||
extra[0] is! WritableSuperIdentity ||
extra[1] is! String) {
return '/';
}
return null;
},
builder: (context, state) { builder: (context, state) {
final extra = state.extra! as List<Object?>; final extra = state.extra! as List<Object>;
return ShowRecoveryKeyPage( return ShowRecoveryKeyPage(
writableSuperIdentity: writableSuperIdentity:
extra[0]! as WritableSuperIdentity, extra[0] as WritableSuperIdentity,
name: extra[1]! as String); name: extra[1] as String);
}), }),
]),
GoRoute( GoRoute(
path: '/settings', path: '/settings',
builder: (context, state) => const SettingsPage(), builder: (context, state) => const SettingsPage(),

View file

@ -16,7 +16,7 @@ ChatTheme makeChatTheme(
: scale.secondaryScale.calloutBackground, : scale.secondaryScale.calloutBackground,
backgroundColor: backgroundColor:
scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha), scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha),
messageBorderRadius: scaleConfig.borderRadiusScale * 16, messageBorderRadius: scaleConfig.borderRadiusScale * 12,
bubbleBorderSide: scaleConfig.preferBorders bubbleBorderSide: scaleConfig.preferBorders
? BorderSide( ? BorderSide(
color: scale.primaryScale.calloutBackground, color: scale.primaryScale.calloutBackground,
@ -37,7 +37,7 @@ ChatTheme makeChatTheme(
filled: !scaleConfig.preferBorders, filled: !scaleConfig.preferBorders,
fillColor: scale.primaryScale.subtleBackground, fillColor: scale.primaryScale.subtleBackground,
isDense: true, isDense: true,
contentPadding: const EdgeInsets.fromLTRB(8, 12, 8, 12), contentPadding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
disabledBorder: OutlineInputBorder( disabledBorder: OutlineInputBorder(
borderSide: scaleConfig.preferBorders borderSide: scaleConfig.preferBorders
? BorderSide(color: scale.grayScale.border, width: 2) ? BorderSide(color: scale.grayScale.border, width: 2)
@ -65,10 +65,12 @@ ChatTheme makeChatTheme(
color: scaleConfig.preferBorders color: scaleConfig.preferBorders
? scale.primaryScale.elementBackground ? scale.primaryScale.elementBackground
: scale.primaryScale.border), : scale.primaryScale.border),
inputPadding: const EdgeInsets.all(12), inputPadding: const EdgeInsets.all(6),
inputTextColor: !scaleConfig.preferBorders inputTextColor: !scaleConfig.preferBorders
? scale.primaryScale.appText ? scale.primaryScale.appText
: scale.primaryScale.border, : scale.primaryScale.border,
messageInsetsHorizontal: 12,
messageInsetsVertical: 8,
attachmentButtonIcon: const Icon(Icons.attach_file), attachmentButtonIcon: const Icon(Icons.attach_file),
sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith( sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith(
color: scaleConfig.preferBorders color: scaleConfig.preferBorders

View file

@ -263,6 +263,36 @@ ThemeData contrastGenerator({
final baseThemeData = scaleTheme.toThemeData(brightness); final baseThemeData = scaleTheme.toThemeData(brightness);
WidgetStateProperty<BorderSide?> elementBorderWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F));
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
}
return BorderSide(color: scheme.primaryScale.border);
});
WidgetStateProperty<Color?> elementBackgroundWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return scheme.grayScale.elementBackground;
} else if (states.contains(WidgetState.pressed)) {
return scheme.primaryScale.activeElementBackground;
} else if (states.contains(WidgetState.hovered)) {
return scheme.primaryScale.hoverElementBackground;
} else if (states.contains(WidgetState.focused)) {
return scheme.primaryScale.activeElementBackground;
}
return scheme.primaryScale.elementBackground;
});
final elevatedButtonTheme = ElevatedButtonThemeData( final elevatedButtonTheme = ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: scheme.primaryScale.elementBackground, backgroundColor: scheme.primaryScale.elementBackground,
@ -274,20 +304,9 @@ ThemeData contrastGenerator({
side: BorderSide(color: scheme.primaryScale.border), side: BorderSide(color: scheme.primaryScale.border),
borderRadius: borderRadius:
BorderRadius.circular(8 * scaleConfig.borderRadiusScale))) BorderRadius.circular(8 * scaleConfig.borderRadiusScale)))
.copyWith(side: WidgetStateProperty.resolveWith((states) { .copyWith(
if (states.contains(WidgetState.disabled)) { side: elementBorderWidgetStateProperty(),
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); backgroundColor: elementBackgroundWidgetStateProperty()));
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(color: scheme.primaryScale.hoverBorder);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
}
return BorderSide(color: scheme.primaryScale.border);
})));
final themeData = baseThemeData.copyWith( final themeData = baseThemeData.copyWith(
// chipTheme: baseThemeData.chipTheme.copyWith( // chipTheme: baseThemeData.chipTheme.copyWith(

View file

@ -97,7 +97,7 @@ class ScaleScheme extends ThemeExtension<ScaleScheme> {
onSurfaceVariant: secondaryScale.appText, onSurfaceVariant: secondaryScale.appText,
outline: primaryScale.border, outline: primaryScale.border,
outlineVariant: secondaryScale.border, outlineVariant: secondaryScale.border,
shadow: primaryScale.appBackground.darken(60), shadow: primaryScale.primary.darken(60),
//scrim: primaryScale.background, //scrim: primaryScale.background,
// inverseSurface: primaryScale.subtleText, // inverseSurface: primaryScale.subtleText,
// onInverseSurface: primaryScale.subtleBackground, // onInverseSurface: primaryScale.subtleBackground,

View file

@ -43,6 +43,50 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
config: config.lerp(other.config, t)); config: config.lerp(other.config, t));
} }
WidgetStateProperty<BorderSide?> elementBorderWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return BorderSide(
color: scheme.grayScale.border.withAlpha(0x7F),
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.pressed)) {
return BorderSide(
color: scheme.primaryScale.border,
);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(
color: scheme.primaryScale.hoverBorder,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(
color: scheme.primaryScale.hoverBorder,
width: 2,
strokeAlign: BorderSide.strokeAlignOutside);
}
return BorderSide(
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
});
WidgetStateProperty<Color?> elementColorWidgetStateProperty() =>
WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.disabled)) {
return scheme.grayScale.primary.withAlpha(0x7F);
} else if (states.contains(WidgetState.pressed)) {
return scheme.primaryScale.borderText;
} else if (states.contains(WidgetState.hovered)) {
return scheme.primaryScale.borderText;
} else if (states.contains(WidgetState.focused)) {
return scheme.primaryScale.borderText;
}
return Color.lerp(
scheme.primaryScale.borderText, scheme.primaryScale.primary, 0.25);
});
// WidgetStateProperty<Color?> elementBackgroundWidgetStateProperty() {
// return null;
// }
ThemeData toThemeData(Brightness brightness) { ThemeData toThemeData(Brightness brightness) {
final colorScheme = scheme.toColorScheme(brightness); final colorScheme = scheme.toColorScheme(brightness);
@ -51,8 +95,9 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
final elevatedButtonTheme = ElevatedButtonThemeData( final elevatedButtonTheme = ElevatedButtonThemeData(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
elevation: 0,
textStyle: textTheme.labelSmall,
backgroundColor: scheme.primaryScale.elementBackground, backgroundColor: scheme.primaryScale.elementBackground,
foregroundColor: scheme.primaryScale.appText,
disabledBackgroundColor: disabledBackgroundColor:
scheme.grayScale.elementBackground.withAlpha(0x7F), scheme.grayScale.elementBackground.withAlpha(0x7F),
disabledForegroundColor: disabledForegroundColor:
@ -61,20 +106,11 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
side: BorderSide(color: scheme.primaryScale.border), side: BorderSide(color: scheme.primaryScale.border),
borderRadius: borderRadius:
BorderRadius.circular(8 * config.borderRadiusScale))) BorderRadius.circular(8 * config.borderRadiusScale)))
.copyWith(side: WidgetStateProperty.resolveWith((states) { .copyWith(
if (states.contains(WidgetState.disabled)) { foregroundColor: elementColorWidgetStateProperty(),
return BorderSide(color: scheme.grayScale.border.withAlpha(0x7F)); side: elementBorderWidgetStateProperty(),
} else if (states.contains(WidgetState.pressed)) { iconColor: elementColorWidgetStateProperty(),
return BorderSide( ));
color: scheme.primaryScale.border,
strokeAlign: BorderSide.strokeAlignOutside);
} else if (states.contains(WidgetState.hovered)) {
return BorderSide(color: scheme.primaryScale.hoverBorder);
} else if (states.contains(WidgetState.focused)) {
return BorderSide(color: scheme.primaryScale.hoverBorder, width: 2);
}
return BorderSide(color: scheme.primaryScale.border);
})));
final themeData = baseThemeData.copyWith( final themeData = baseThemeData.copyWith(
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(

View file

@ -29,11 +29,12 @@ class PopControl extends StatelessWidget {
return PopScope( return PopScope(
canPop: false, canPop: false,
onPopInvoked: (didPop) { onPopInvokedWithResult: (didPop, _) {
if (didPop) { if (didPop) {
return; return;
} }
_doDismiss(navigator); _doDismiss(navigator);
return;
}, },
child: child); child: child);
} }

View file

@ -1,4 +1,3 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../theme.dart';
@ -13,7 +12,7 @@ class StyledScaffold extends StatelessWidget {
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
final scale = scaleScheme.scale(ScaleKind.primary); final scale = scaleScheme.scale(ScaleKind.primary);
final enableBorder = !isMobileSize(context); const enableBorder = false; //!isMobileSize(context);
var scaffold = clipBorder( var scaffold = clipBorder(
clipEnabled: enableBorder, clipEnabled: enableBorder,
@ -28,7 +27,7 @@ class StyledScaffold extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(), onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
child: scaffold.paddingAll(enableBorder ? 32 : 0)); child: scaffold /*.paddingAll(enableBorder ? 32 : 0) */);
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View file

@ -1,111 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:native_device_orientation/native_device_orientation.dart';
class NativeSafeArea extends StatelessWidget {
const NativeSafeArea({
required this.child,
this.left = true,
this.top = true,
this.right = true,
this.bottom = true,
this.minimum = EdgeInsets.zero,
this.maintainBottomViewPadding = false,
super.key,
});
/// Whether to avoid system intrusions on the left.
final bool left;
/// Whether to avoid system intrusions at the top of the screen, typically the
/// system status bar.
final bool top;
/// Whether to avoid system intrusions on the right.
final bool right;
/// Whether to avoid system intrusions on the bottom side of the screen.
final bool bottom;
/// This minimum padding to apply.
///
/// The greater of the minimum insets and the media padding will be applied.
final EdgeInsets minimum;
/// Specifies whether the [SafeArea] should maintain the bottom
/// [MediaQueryData.viewPadding] instead of the bottom
/// [MediaQueryData.padding], defaults to false.
///
/// For example, if there is an onscreen keyboard displayed above the
/// SafeArea, the padding can be maintained below the obstruction rather than
/// being consumed. This can be helpful in cases where your layout contains
/// flexible widgets, which could visibly move when opening a software
/// keyboard due to the change in the padding value. Setting this to true will
/// avoid the UI shift.
final bool maintainBottomViewPadding;
/// The widget below this widget in the tree.
///
/// The padding on the [MediaQuery] for the [child] will be suitably adjusted
/// to zero out any sides that were avoided by this widget.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
@override
Widget build(BuildContext context) {
final nativeOrientation =
NativeDeviceOrientationReader.orientation(context);
late final bool realLeft;
late final bool realRight;
late final bool realTop;
late final bool realBottom;
switch (nativeOrientation) {
case NativeDeviceOrientation.unknown:
case NativeDeviceOrientation.portraitUp:
realLeft = left;
realRight = right;
realTop = top;
realBottom = bottom;
case NativeDeviceOrientation.portraitDown:
realLeft = right;
realRight = left;
realTop = bottom;
realBottom = top;
case NativeDeviceOrientation.landscapeRight:
realLeft = bottom;
realRight = top;
realTop = left;
realBottom = right;
case NativeDeviceOrientation.landscapeLeft:
realLeft = top;
realRight = bottom;
realTop = right;
realBottom = left;
}
return SafeArea(
left: realLeft,
right: realRight,
top: realTop,
bottom: realBottom,
minimum: minimum,
maintainBottomViewPadding: maintainBottomViewPadding,
child: child);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<bool>('left', left))
..add(DiagnosticsProperty<bool>('top', top))
..add(DiagnosticsProperty<bool>('right', right))
..add(DiagnosticsProperty<bool>('bottom', bottom))
..add(DiagnosticsProperty<EdgeInsets>('minimum', minimum))
..add(DiagnosticsProperty<bool>(
'maintainBottomViewPadding', maintainBottomViewPadding));
}
}

View file

@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import '../theme/views/responsive.dart'; import '../theme/views/responsive.dart';
import 'tools.dart';
export 'package:window_manager/window_manager.dart' show TitleBarStyle; export 'package:window_manager/window_manager.dart' show TitleBarStyle;

View file

@ -260,7 +260,6 @@ class _DeveloperPageState extends State<DeveloperPage> {
), ),
body: GestureDetector( body: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
child: SafeArea(
child: Column(children: [ child: Column(children: [
Stack(alignment: AlignmentDirectional.center, children: [ Stack(alignment: AlignmentDirectional.center, children: [
Image.asset('assets/images/ellet.png'), Image.asset('assets/images/ellet.png'),
@ -333,7 +332,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
} }
}, },
).paddingAll(4) ).paddingAll(4)
])))); ])));
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View file

@ -937,14 +937,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.7" version: "6.0.7"
native_device_orientation:
dependency: "direct main"
description:
name: native_device_orientation
sha256: "0c330c068575e4be72cce5968ca479a3f8d5d1e5dfce7d89d5c13a1e943b338c"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -1318,10 +1310,10 @@ packages:
description: description:
path: "." path: "."
ref: main ref: main
resolved-ref: db0f7b6f1baec0250ecba82f3d161bac1cf23d7d resolved-ref: f367c2f713dcc0c965a4f7af5952d94b2f699998
url: "https://gitlab.com/veilid/Searchable-Listview.git" url: "https://gitlab.com/veilid/Searchable-Listview.git"
source: git source: git
version: "2.14.1" version: "2.16.0"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -63,7 +63,6 @@ dependencies:
loggy: ^2.0.3 loggy: ^2.0.3
meta: ^1.16.0 meta: ^1.16.0
mobile_scanner: ^6.0.7 mobile_scanner: ^6.0.7
native_device_orientation: ^2.0.3
package_info_plus: ^8.3.0 package_info_plus: ^8.3.0
pasteboard: ^0.3.0 pasteboard: ^0.3.0
path: ^1.9.1 path: ^1.9.1