add multiple accounts menu

This commit is contained in:
Christien Rioux 2024-06-11 21:27:20 -04:00
parent b0d4e35c6f
commit 87bb1657c7
25 changed files with 583 additions and 70 deletions

View file

@ -0,0 +1,262 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import 'menu_item_widget.dart';
class DrawerMenu extends StatefulWidget {
const DrawerMenu({super.key});
@override
State createState() => _DrawerMenuState();
}
class _DrawerMenuState extends State<DrawerMenu> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
void _doLoginClick(TypedKey superIdentityRecordKey) {
//
}
void _doEditClick(TypedKey superIdentityRecordKey) {
//
}
Widget _wrapInBox({required Widget child, required Color color}) =>
DecoratedBox(
decoration: ShapeDecoration(
color: color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16))),
child: child);
Widget _makeAccountWidget(
{required String name,
required bool loggedIn,
required void Function() clickHandler}) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!.tertiaryScale;
final abbrev = name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join();
late final String shortname;
if (abbrev.length >= 3) {
shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1];
} else {
shortname = abbrev;
}
final avatar = AvatarImage(
size: 32,
backgroundColor: loggedIn ? scale.primary : scale.elementBackground,
foregroundColor: loggedIn ? scale.primaryText : scale.subtleText,
child: Text(shortname, style: theme.textTheme.titleLarge));
return MenuItemWidget(
title: name,
headerWidget: avatar,
titleStyle: theme.textTheme.titleLarge!,
foregroundColor: scale.primary,
backgroundColor: scale.elementBackground,
backgroundHoverColor: scale.hoverElementBackground,
backgroundFocusColor: scale.activeElementBackground,
borderColor: scale.border,
borderHoverColor: scale.hoverBorder,
borderFocusColor: scale.primary,
footerButtonIcon: loggedIn ? Icons.edit_outlined : Icons.login_outlined,
footerCallback: clickHandler,
footerButtonIconColor: scale.border,
footerButtonIconHoverColor: scale.hoverElementBackground,
footerButtonIconFocusColor: scale.activeElementBackground,
);
}
Widget _getAccountList(
{required TypedKey? activeLocalAccount,
required AccountRecordsBlocMapState accountRecords}) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final accountRepo = AccountRepository.instance;
final localAccounts = accountRepo.getLocalAccounts();
//final userLogins = accountRepo.getUserLogins();
final loggedInAccounts = <Widget>[];
final loggedOutAccounts = <Widget>[];
for (final la in localAccounts) {
final superIdentityRecordKey = la.superIdentity.recordKey;
// See if this account is logged in
final acctRecord = accountRecords.get(superIdentityRecordKey);
if (acctRecord != null) {
// Account is logged in
final loggedInAccount = acctRecord.when(
data: (value) => _makeAccountWidget(
name: value.profile.name,
loggedIn: true,
clickHandler: () {
_doEditClick(superIdentityRecordKey);
}),
loading: () => _wrapInBox(
child: buildProgressIndicator(),
color: scale.grayScale.subtleBorder),
error: (err, st) => _wrapInBox(
child: errorPage(err, st), color: scale.errorScale.subtleBorder),
);
loggedInAccounts.add(loggedInAccount);
} else {
// Account is not logged in
final loggedOutAccount = _makeAccountWidget(
name: la.name,
loggedIn: false,
clickHandler: () {
_doLoginClick(superIdentityRecordKey);
});
loggedOutAccounts.add(loggedOutAccount);
}
}
// Assemble main menu
final mainMenu = <Widget>[...loggedInAccounts, ...loggedOutAccounts];
// Return main menu widgets
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[...mainMenu],
);
}
Widget _getButton(
{required Icon icon,
required ScaleColor scale,
required String tooltip,
required void Function()? onPressed}) =>
IconButton(
icon: icon,
color: scale.hoverBorder,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return scale.hoverElementBackground;
}
if (states.contains(WidgetState.focused)) {
return scale.activeElementBackground;
}
return scale.elementBackground;
}), shape: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return RoundedRectangleBorder(
side: BorderSide(color: scale.hoverBorder),
borderRadius: const BorderRadius.all(Radius.circular(16)));
}
if (states.contains(WidgetState.focused)) {
return RoundedRectangleBorder(
side: BorderSide(color: scale.primary),
borderRadius: const BorderRadius.all(Radius.circular(16)));
}
return RoundedRectangleBorder(
side: BorderSide(color: scale.border),
borderRadius: const BorderRadius.all(Radius.circular(16)));
})),
tooltip: tooltip,
onPressed: onPressed);
Widget _getBottomButtons() {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final settingsButton = _getButton(
icon: const Icon(Icons.settings),
tooltip: translate('menu.settings_tooltip'),
scale: scale.tertiaryScale,
onPressed: () async {
await GoRouterHelper(context).push('/settings');
}).paddingLTRB(0, 0, 16, 0);
final addButton = _getButton(
icon: const Icon(Icons.add),
tooltip: translate('menu.add_account_tooltip'),
scale: scale.tertiaryScale,
onPressed: () async {
await GoRouterHelper(context).push('/new_account');
}).paddingLTRB(0, 0, 16, 0);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [settingsButton, addButton]).paddingLTRB(0, 16, 0, 0);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
//final textTheme = theme.textTheme;
final accountRecords = context.watch<AccountRecordsBlocMapCubit>().state;
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
final gradient = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
scale.tertiaryScale.hoverElementBackground,
scale.tertiaryScale.subtleBackground,
]);
return DecoratedBox(
decoration: ShapeDecoration(
shadows: [
BoxShadow(
color: scale.tertiaryScale.appBackground,
blurRadius: 6,
offset: const Offset(
0,
3,
),
),
],
gradient: gradient,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16),
bottomRight: Radius.circular(16)))),
child: Column(children: [
FittedBox(
fit: BoxFit.scaleDown,
child: Row(children: [
SvgPicture.asset(
height: 48,
'assets/images/icon.svg',
).paddingLTRB(0, 0, 16, 0),
SvgPicture.asset(
height: 48,
'assets/images/title.svg',
),
])),
const Spacer(),
_getAccountList(
activeLocalAccount: activeLocalAccount,
accountRecords: accountRecords),
_getBottomButtons(),
const Spacer(),
Text('Version $packageInfoVersion',
style: theme.textTheme.labelMedium!
.copyWith(color: scale.tertiaryScale.hoverBorder))
]).paddingAll(16),
);
}
}

View file

@ -0,0 +1,130 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class MenuItemWidget extends StatelessWidget {
const MenuItemWidget({
required this.title,
required this.titleStyle,
required this.foregroundColor,
this.headerWidget,
this.widthBox,
this.callback,
this.backgroundColor,
this.backgroundHoverColor,
this.backgroundFocusColor,
this.borderColor,
this.borderHoverColor,
this.borderFocusColor,
this.footerButtonIcon,
this.footerButtonIconColor,
this.footerButtonIconHoverColor,
this.footerButtonIconFocusColor,
this.footerCallback,
super.key,
});
@override
Widget build(BuildContext context) => TextButton(
onPressed: () => callback,
style: TextButton.styleFrom(foregroundColor: foregroundColor).copyWith(
backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return backgroundHoverColor;
}
if (states.contains(WidgetState.focused)) {
return backgroundFocusColor;
}
return backgroundColor;
}),
side: WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.hovered)) {
return borderColor != null
? BorderSide(color: borderHoverColor!)
: null;
}
if (states.contains(WidgetState.focused)) {
return borderColor != null
? BorderSide(color: borderFocusColor!)
: null;
}
return borderColor != null ? BorderSide(color: borderColor!) : null;
}),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)))),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (headerWidget != null) headerWidget!,
if (widthBox != null) widthBox!,
Expanded(
child: FittedBox(
alignment: Alignment.centerLeft,
fit: BoxFit.scaleDown,
child: Text(
title,
style: titleStyle,
).paddingAll(8)),
),
if (footerButtonIcon != null)
IconButton.outlined(
color: footerButtonIconColor,
focusColor: footerButtonIconFocusColor,
hoverColor: footerButtonIconHoverColor,
icon: Icon(
footerButtonIcon,
size: 24,
),
onPressed: footerCallback),
],
),
));
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextStyle?>('textStyle', titleStyle))
..add(ObjectFlagProperty<void Function()?>.has('callback', callback))
..add(DiagnosticsProperty<Color>('foregroundColor', foregroundColor))
..add(StringProperty('title', title))
..add(
DiagnosticsProperty<IconData?>('footerButtonIcon', footerButtonIcon))
..add(ObjectFlagProperty<void Function()?>.has(
'footerCallback', footerCallback))
..add(ColorProperty('footerButtonIconColor', footerButtonIconColor))
..add(ColorProperty(
'footerButtonIconHoverColor', footerButtonIconHoverColor))
..add(ColorProperty(
'footerButtonIconFocusColor', footerButtonIconFocusColor))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('backgroundHoverColor', backgroundHoverColor))
..add(ColorProperty('backgroundFocusColor', backgroundFocusColor))
..add(ColorProperty('borderColor', borderColor))
..add(ColorProperty('borderHoverColor', borderHoverColor))
..add(ColorProperty('borderFocusColor', borderFocusColor));
}
////////////////////////////////////////////////////////////////////////////
final String title;
final Widget? headerWidget;
final Widget? widthBox;
final TextStyle titleStyle;
final Color foregroundColor;
final void Function()? callback;
final IconData? footerButtonIcon;
final void Function()? footerCallback;
final Color? backgroundColor;
final Color? backgroundHoverColor;
final Color? backgroundFocusColor;
final Color? borderColor;
final Color? borderHoverColor;
final Color? borderFocusColor;
final Color? footerButtonIconColor;
final Color? footerButtonIconHoverColor;
final Color? footerButtonIconFocusColor;
}

View file

@ -1,3 +1,4 @@
export 'drawer_menu/drawer_menu.dart';
export 'home_account_invalid.dart';
export 'home_account_locked.dart';
export 'home_account_missing.dart';

View file

@ -2,7 +2,7 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import '../../../account_manager/account_manager.dart';
import '../../../chat/chat.dart';
@ -36,7 +36,7 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
return Column(children: <Widget>[
Row(children: [
IconButton(
icon: const Icon(Icons.settings),
icon: const Icon(Icons.menu),
color: scale.secondaryScale.borderText,
constraints: const BoxConstraints.expand(height: 64, width: 64),
style: ButtonStyle(
@ -46,7 +46,9 @@ class _HomeAccountReadyMainState extends State<HomeAccountReadyMain> {
borderRadius: BorderRadius.all(Radius.circular(16))))),
tooltip: translate('app_bar.settings_tooltip'),
onPressed: () async {
await GoRouterHelper(context).push('/settings');
final ctrl = context.read<ZoomDrawerController>();
await ctrl.toggle?.call();
//await GoRouterHelper(context).push('/settings');
}).paddingLTRB(0, 0, 8, 0),
asyncValueBuilder(account,
(_, account) => ProfileWidget(profile: account.profile))

View file

@ -1,9 +1,14 @@
import 'dart:math';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:provider/provider.dart';
import '../../account_manager/account_manager.dart';
import '../../theme/theme.dart';
import 'drawer_menu/drawer_menu.dart';
import 'home_account_invalid.dart';
import 'home_account_locked.dart';
import 'home_account_missing.dart';
@ -31,7 +36,7 @@ class HomeShellState extends State<HomeShell> {
Widget buildWithLogin(BuildContext context) {
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
final accountRecordsCubit = context.watch<AccountRecordsBlocMapCubit>();
if (activeLocalAccount == null) {
// If no logged in user is active, show the loading panel
return const HomeNoActive();
@ -39,6 +44,11 @@ class HomeShellState extends State<HomeShell> {
final accountInfo =
AccountRepository.instance.getAccountInfo(activeLocalAccount);
final activeCubit =
accountRecordsCubit.tryOperate(activeLocalAccount, closure: (c) => c);
if (activeCubit == null) {
return waitingPage();
}
switch (accountInfo.status) {
case AccountInfoStatus.noAccount:
@ -48,14 +58,13 @@ class HomeShellState extends State<HomeShell> {
case AccountInfoStatus.accountLocked:
return const HomeAccountLocked();
case AccountInfoStatus.accountReady:
return Provider<ActiveAccountInfo>.value(
return MultiProvider(providers: [
Provider<ActiveAccountInfo>.value(
value: accountInfo.activeAccountInfo!,
child: BlocProvider(
create: (context) => AccountRecordCubit(
open: () async => AccountRepository.instance
.openAccountRecord(
accountInfo.activeAccountInfo!.userLogin)),
child: widget.accountReadyBuilder));
),
Provider<AccountRecordCubit>.value(value: activeCubit),
Provider<ZoomDrawerController>.value(value: _zoomDrawerController),
], child: widget.accountReadyBuilder);
}
}
@ -64,11 +73,38 @@ class HomeShellState extends State<HomeShell> {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
// XXX: eventually write account switcher here
final gradient = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
scale.tertiaryScale.subtleBackground,
scale.tertiaryScale.appBackground,
]);
return SafeArea(
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: buildWithLogin(context)));
decoration: BoxDecoration(gradient: gradient),
child: ZoomDrawer(
controller: _zoomDrawerController,
//menuBackgroundColor: Colors.transparent,
menuScreen: const DrawerMenu(),
mainScreen: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: buildWithLogin(context)),
borderRadius: 24,
showShadow: true,
angle: 0,
drawerShadowsBackgroundColor: theme.shadowColor,
mainScreenOverlayColor: theme.shadowColor.withAlpha(0x3F),
openCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250),
menuScreenTapClose: true,
mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
)));
}
final ZoomDrawerController _zoomDrawerController = ZoomDrawerController();
}