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"
},
"add_contact_sheet": {
"new_contact": "New Contact",
"add_contact": "Add Contact",
"create_invite": "Create\nInvitation",
"receive_invite": "Receive\nInvitation",
"scan_invite": "Scan\nInvitation",
"paste_invite": "Paste\nInvitation"
},

View File

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

View File

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

View File

@ -32,6 +32,7 @@ class PerAccountCollectionState with _$PerAccountCollectionState {
}
extension PerAccountCollectionStateExt on PerAccountCollectionState {
// Returns if the account is ready and logged in
bool get isReady =>
avAccountRecordState != null &&
avAccountRecordState!.isData &&
@ -45,7 +46,11 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState {
activeConversationsBlocMapCubit != 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: accountRecordCubit!),
BlocProvider.value(value: contactInvitationListCubit!),
@ -56,4 +61,9 @@ extension PerAccountCollectionStateExt on PerAccountCollectionState {
BlocProvider.value(value: activeConversationsBlocMapCubit!),
BlocProvider.value(value: activeSingleContactChatBlocMapCubit!),
], child: child);
} else {
// Otherwise we just provide the child
return child;
}
}
}

View File

@ -100,6 +100,94 @@ class VeilidChatApp extends StatelessWidget {
onInvoke: (intent) => _attachDetach(context)),
}, child: Focus(autofocus: true, child: builder(context)))));
Widget appBuilder(
BuildContext context, LocalizationDelegate localizationDelegate) =>
ThemeProvider(
initTheme: initialThemeData,
builder: (context, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: MultiBlocProvider(
providers: [
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
),
BlocProvider<NotificationsCubit>(
create: (context) => NotificationsCubit(
const NotificationsState(queue: IList.empty()))),
BlocProvider<ConnectionStateCubit>(
create: (context) =>
ConnectionStateCubit(ProcessorRepository.instance)),
BlocProvider<RouterCubit>(
create: (context) => RouterCubit(AccountRepository.instance),
),
BlocProvider<LocalAccountsCubit>(
create: (context) =>
LocalAccountsCubit(AccountRepository.instance),
),
BlocProvider<UserLoginsCubit>(
create: (context) =>
UserLoginsCubit(AccountRepository.instance),
),
BlocProvider<ActiveLocalAccountCubit>(
create: (context) =>
ActiveLocalAccountCubit(AccountRepository.instance),
),
BlocProvider<PerAccountCollectionBlocMapCubit>(
create: (context) => PerAccountCollectionBlocMapCubit(
accountRepository: AccountRepository.instance,
locator: context.read)),
],
child:
BackgroundTicker(child: _buildShortcuts(builder: (context) {
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
final gradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: scaleConfig.preferBorders &&
theme.brightness == Brightness.light
? [
scale.grayScale.hoverElementBackground,
scale.grayScale.subtleBackground,
]
: [
scale.primaryScale.hoverElementBackground,
scale.primaryScale.subtleBackground,
]);
final wallpaper = PreferencesRepository
.instance.value.themePreference
.wallpaper();
return Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
wallpaper ??
DecoratedBox(
decoration: BoxDecoration(gradient: gradient)),
MaterialApp.router(
scrollBehavior: const ScrollBehaviorModified(),
debugShowCheckedModeBanner: false,
routerConfig: context.read<RouterCubit>().router(),
title: translate('app.title'),
theme: theme,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
],
supportedLocales: localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
)
]);
})),
)),
);
@override
Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>(
initialData: null,
@ -112,93 +200,8 @@ class VeilidChatApp extends StatelessWidget {
}
// Once init is done, we proceed with the app
final localizationDelegate = LocalizedApp.of(context).delegate;
return ThemeProvider(
initTheme: initialThemeData,
builder: (context, theme) => LocalizationProvider(
state: LocalizationProvider.of(context).state,
child: MultiBlocProvider(
providers: [
BlocProvider<PreferencesCubit>(
create: (context) =>
PreferencesCubit(PreferencesRepository.instance),
),
BlocProvider<NotificationsCubit>(
create: (context) => NotificationsCubit(
const NotificationsState(queue: IList.empty()))),
BlocProvider<ConnectionStateCubit>(
create: (context) =>
ConnectionStateCubit(ProcessorRepository.instance)),
BlocProvider<RouterCubit>(
create: (context) =>
RouterCubit(AccountRepository.instance),
),
BlocProvider<LocalAccountsCubit>(
create: (context) =>
LocalAccountsCubit(AccountRepository.instance),
),
BlocProvider<UserLoginsCubit>(
create: (context) =>
UserLoginsCubit(AccountRepository.instance),
),
BlocProvider<ActiveLocalAccountCubit>(
create: (context) =>
ActiveLocalAccountCubit(AccountRepository.instance),
),
BlocProvider<PerAccountCollectionBlocMapCubit>(
create: (context) => PerAccountCollectionBlocMapCubit(
accountRepository: AccountRepository.instance,
locator: context.read)),
],
child:
BackgroundTicker(child: _buildShortcuts(builder: (context) {
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
final gradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: scaleConfig.preferBorders &&
theme.brightness == Brightness.light
? [
scale.grayScale.hoverElementBackground,
scale.grayScale.subtleBackground,
]
: [
scale.primaryScale.hoverElementBackground,
scale.primaryScale.subtleBackground,
]);
final wallpaper = PreferencesRepository
.instance.value.themePreference
.wallpaper();
return Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
wallpaper ??
DecoratedBox(
decoration: BoxDecoration(gradient: gradient)),
MaterialApp.router(
scrollBehavior: const ScrollBehaviorModified(),
debugShowCheckedModeBanner: false,
routerConfig: context.read<RouterCubit>().router(),
title: translate('app.title'),
theme: theme,
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
localizationDelegate
],
supportedLocales:
localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
)
]);
})),
)),
);
return SafeArea(child: appBuilder(context, localizationDelegate));
});
@override

View File

@ -33,11 +33,6 @@ class ChatComponentWidget extends StatelessWidget {
@override
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
final accountInfo = context.watch<AccountInfoCubit>().state;
@ -125,7 +120,7 @@ class ChatComponentWidget extends StatelessWidget {
return Column(
children: [
Container(
height: 48,
height: 40,
decoration: BoxDecoration(
color: scale.border,
),
@ -141,9 +136,10 @@ class ChatComponentWidget extends StatelessWidget {
)),
const Spacer(),
IconButton(
iconSize: 24,
icon: Icon(Icons.close, color: scale.borderText),
onPressed: _onClose)
.paddingLTRB(16, 0, 16, 0)
.paddingLTRB(0, 0, 8, 0)
]),
),
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_translate/flutter_translate.dart';
import '../../theme/models/scale_theme/scale_scheme.dart';
import '../../theme/theme.dart';
class NoConversationWidget extends StatelessWidget {
const NoConversationWidget({super.key});
@ -17,27 +17,26 @@ class NoConversationWidget extends StatelessWidget {
final scale = scaleScheme.scale(ScaleKind.primary);
return DecoratedBox(
decoration: BoxDecoration(
color: scale.appBackground.withAlpha(scaleConfig.wallpaperAlpha),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.diversity_3,
color: scale.appText.withAlpha(127),
size: 48,
),
Text(
textAlign: TextAlign.center,
translate('chat.start_a_conversation'),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: scale.appText.withAlpha(127),
),
),
],
),
);
decoration: BoxDecoration(
color: scale.appBackground.withAlpha(scaleConfig.wallpaperAlpha),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.diversity_3,
color: scale.appText.withAlpha(127),
size: 48,
),
Text(
textAlign: TextAlign.center,
translate('chat.start_a_conversation'),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: scale.appText.withAlpha(127),
),
),
],
));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -89,37 +89,40 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
)),
tooltip: translate('menu.contacts_tooltip'),
onPressed: () async {
await ContactsDialog.show(context);
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const ContactsPage(),
),
);
});
});
Widget buildUserPanel() => Builder(builder: (context) {
final profile = context.select<AccountRecordCubit, proto.Profile>(
(c) => c.state.asData!.value.profile);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
return ColoredBox(
color: scaleConfig.preferBorders
? scale.primaryScale.subtleBackground
: scale.primaryScale.subtleBorder,
child: Column(children: <Widget>[
Row(children: [
buildMenuButton().paddingLTRB(0, 0, 8, 0),
ProfileWidget(
profile: profile,
showPronouns: false,
).expanded(),
buildContactsButton().paddingLTRB(8, 0, 0, 0),
]).paddingAll(8),
const ChatListWidget().expanded()
]));
});
Widget buildLeftPane(BuildContext context) => Builder(
builder: (context) =>
Material(color: Colors.transparent, child: buildUserPanel()));
builder: (context) => Material(
color: Colors.transparent,
child: Builder(builder: (context) {
final profile = context.select<AccountRecordCubit, proto.Profile>(
(c) => c.state.asData!.value.profile);
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
return ColoredBox(
color: scaleConfig.preferBorders
? scale.primaryScale.subtleBackground
: scale.primaryScale.subtleBorder,
child: Column(children: <Widget>[
Row(children: [
buildMenuButton().paddingLTRB(0, 0, 8, 0),
ProfileWidget(
profile: profile,
showPronouns: false,
).expanded(),
buildContactsButton().paddingLTRB(8, 0, 0, 0),
]).paddingAll(8),
const ChatListWidget().expanded()
]));
})));
Widget buildRightPane(BuildContext context) {
final activeChatCubit = context.watch<ActiveChatCubit>();
@ -140,10 +143,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
@override
Widget build(BuildContext context) {
final isLarge = responsiveVisibility(
context: context,
phone: false,
);
final isSmallScreen = isMobileSize(context);
final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!;
@ -160,14 +160,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
late final bool visibleRight;
late final double leftWidth;
late final double rightWidth;
if (isLarge) {
visibleLeft = true;
visibleRight = true;
leftWidth = leftColumnSize;
rightWidth = constraints.maxWidth -
leftColumnSize -
(scaleConfig.useVisualIndicators ? 2 : 0);
} else {
if (isSmallScreen) {
if (hasActiveChat) {
visibleLeft = false;
visibleRight = true;
@ -179,6 +172,13 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
leftWidth = constraints.maxWidth;
rightWidth = 400; // whatever
}
} else {
visibleLeft = true;
visibleRight = true;
leftWidth = leftColumnSize;
rightWidth = constraints.maxWidth -
leftColumnSize -
(scaleConfig.useVisualIndicators ? 2 : 0);
}
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
return perAccountCollectionState.provide(
child: Navigator(
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
return true;
},
pages: const [MaterialPage(child: HomeAccountReady())]));
final pages = <MaterialPage<void>>[
const MaterialPage<void>(child: HomeAccountReady())
];
return perAccountCollectionState.provideReady(
child: Navigator(onDidRemovePage: pages.remove, pages: pages));
}
}
@ -208,36 +204,35 @@ class HomeScreenState extends State<HomeScreen>
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1;
return SafeArea(
child: DefaultTextStyle(
style: theme.textTheme.bodySmall!,
child: ZoomDrawer(
controller: _zoomDrawerController,
menuScreen: Builder(builder: (context) {
final zoomDrawer = ZoomDrawer.of(context);
zoomDrawer!.stateNotifier.addListener(() {
if (zoomDrawer.isOpen()) {
FocusManager.instance.primaryFocus?.unfocus();
}
});
return const DrawerMenu();
}),
mainScreen: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildAccountPageView)),
borderRadius: 0,
angle: 0,
//mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
openCurve: Curves.fastEaseInToSlowEaseOut,
closeCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250),
menuScreenTapClose: canClose,
mainScreenTapClose: canClose,
disableDragGesture: !canClose,
mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
)));
return DefaultTextStyle(
style: theme.textTheme.bodySmall!,
child: ZoomDrawer(
controller: _zoomDrawerController,
menuScreen: Builder(builder: (context) {
final zoomDrawer = ZoomDrawer.of(context);
zoomDrawer!.stateNotifier.addListener(() {
if (zoomDrawer.isOpen()) {
FocusManager.instance.primaryFocus?.unfocus();
}
});
return const DrawerMenu();
}),
mainScreen: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildAccountPageView)),
borderRadius: 0,
angle: 0,
//mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
openCurve: Curves.fastEaseInToSlowEaseOut,
closeCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250),
menuScreenTapClose: canClose,
mainScreenTapClose: canClose,
disableDragGesture: !canClose,
mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
));
}
////////////////////////////////////////////////////////////////////////////

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import '../theme.dart';
@ -13,7 +12,7 @@ class StyledScaffold extends StatelessWidget {
final scaleConfig = theme.extension<ScaleConfig>()!;
final scale = scaleScheme.scale(ScaleKind.primary);
final enableBorder = !isMobileSize(context);
const enableBorder = false; //!isMobileSize(context);
var scaffold = clipBorder(
clipEnabled: enableBorder,
@ -28,7 +27,7 @@ class StyledScaffold extends StatelessWidget {
return GestureDetector(
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 '../theme/views/responsive.dart';
import 'tools.dart';
export 'package:window_manager/window_manager.dart' show TitleBarStyle;

View File

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

View File

@ -937,14 +937,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -1318,10 +1310,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: db0f7b6f1baec0250ecba82f3d161bac1cf23d7d
resolved-ref: f367c2f713dcc0c965a4f7af5952d94b2f699998
url: "https://gitlab.com/veilid/Searchable-Listview.git"
source: git
version: "2.14.1"
version: "2.16.0"
share_plus:
dependency: "direct main"
description:

View File

@ -63,7 +63,6 @@ dependencies:
loggy: ^2.0.3
meta: ^1.16.0
mobile_scanner: ^6.0.7
native_device_orientation: ^2.0.3
package_info_plus: ^8.3.0
pasteboard: ^0.3.0
path: ^1.9.1
@ -112,13 +111,13 @@ dependencies:
xterm: ^4.0.0
zxing2: ^0.2.3
#dependency_overrides:
# dependency_overrides:
# async_tools:
# path: ../dart_async_tools
# bloc_advanced_tools:
# path: ../bloc_advanced_tools
# searchable_listview:
# path: ../Searchable-Listview
# searchable_listview:
# path: ../Searchable-Listview
# flutter_chat_ui:
# path: ../flutter_chat_ui