mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-04-03 21:05:50 -04:00
Merge branch 'debugging' into 'main'
Improved contact invitation menus See merge request veilid/veilidchat!37
This commit is contained in:
commit
e4358586aa
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
175
lib/app.dart
175
lib/app.dart
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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))));
|
||||
}
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
171
lib/contacts/views/contacts_page.dart
Normal file
171
lib/contacts/views/contacts_page.dart
Normal 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;
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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: [
|
||||
|
@ -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),
|
||||
));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -29,11 +29,12 @@ class PopControl extends StatelessWidget {
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (didPop) {
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop) {
|
||||
return;
|
||||
}
|
||||
_doDismiss(navigator);
|
||||
return;
|
||||
},
|
||||
child: child);
|
||||
}
|
||||
|
@ -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) */);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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)
|
||||
]))));
|
||||
])));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
12
pubspec.lock
12
pubspec.lock
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user