From 3ad96ff4cedf3d0f16719517e7dd9c476c1b68b5 Mon Sep 17 00:00:00 2001 From: Tom Bursch Date: Tue, 12 Nov 2024 19:57:29 +0100 Subject: [PATCH] feat: Improve mobile settings UX (#541) --- kitchenowl/lib/enums/views_enum.dart | 30 +++- kitchenowl/lib/models/household.dart | 2 +- kitchenowl/lib/pages/household_add_page.dart | 1 - kitchenowl/lib/pages/household_page.dart | 16 +- .../lib/pages/household_page/_export.dart | 2 +- .../household_page/household_drawer.dart | 22 +-- .../household_navigation_rail.dart | 12 +- kitchenowl/lib/pages/household_page/more.dart | 157 ++++++++++++++++++ .../lib/pages/household_page/profile.dart | 117 ------------- kitchenowl/lib/router.dart | 8 - kitchenowl/lib/widgets/household_card.dart | 22 +-- kitchenowl/lib/widgets/household_image.dart | 31 ++++ .../sliver_household_feature_settings.dart | 7 - 13 files changed, 241 insertions(+), 186 deletions(-) create mode 100644 kitchenowl/lib/pages/household_page/more.dart delete mode 100644 kitchenowl/lib/pages/household_page/profile.dart create mode 100644 kitchenowl/lib/widgets/household_image.dart diff --git a/kitchenowl/lib/enums/views_enum.dart b/kitchenowl/lib/enums/views_enum.dart index c7b0f82ca..228f4eac1 100644 --- a/kitchenowl/lib/enums/views_enum.dart +++ b/kitchenowl/lib/enums/views_enum.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kitchenowl/app.dart'; +import 'package:kitchenowl/cubits/household_cubit.dart'; import 'package:kitchenowl/kitchenowl.dart'; import 'package:kitchenowl/models/household.dart'; import 'package:kitchenowl/widgets/expense_create_fab.dart'; @@ -11,7 +13,7 @@ enum ViewsEnum { recipes, planner, balances, - profile; + more; String toLocalizedString(BuildContext context) { final loc = AppLocalizations.of(context)!; @@ -21,7 +23,7 @@ enum ViewsEnum { loc.recipes, loc.mealPlanner, loc.balances, - loc.profile, + loc.more, ][index]; } @@ -33,17 +35,32 @@ enum ViewsEnum { loc.recipes, loc.planner, loc.balances, - loc.profile, + loc.more, ][index]; } + Widget? toIconWidget(BuildContext context) { + if (this == ViewsEnum.more) { + Household? household = context.read().state.household; + if (!App.isOffline && household.image != null) + return CircleAvatar( + radius: 16, + foregroundImage: getImageProvider( + context, + household.image!, + ), + ); + } + return null; + } + IconData toIcon(BuildContext context) { return [ Icons.shopping_bag_outlined, Icons.receipt_outlined, Icons.calendar_today_outlined, Icons.account_balance_outlined, - App.isOffline ? Icons.cloud_off_rounded : Icons.person_outline_rounded, + App.isOffline ? Icons.cloud_off_rounded : Icons.house_rounded, ][index]; } @@ -53,7 +70,7 @@ enum ViewsEnum { Icons.receipt_rounded, Icons.calendar_today_rounded, Icons.account_balance_rounded, - App.isOffline ? Icons.cloud_off_rounded : Icons.person_rounded, + App.isOffline ? Icons.cloud_off_rounded : Icons.house_rounded, ][index]; } @@ -112,7 +129,8 @@ enum ViewsEnum { case 'balances': return ViewsEnum.balances; case 'profile': - return ViewsEnum.profile; + case 'more': + return ViewsEnum.more; default: return null; } diff --git a/kitchenowl/lib/models/household.dart b/kitchenowl/lib/models/household.dart index dbd3f2b74..024f007be 100644 --- a/kitchenowl/lib/models/household.dart +++ b/kitchenowl/lib/models/household.dart @@ -111,7 +111,7 @@ class Household extends Model { } if (viewOrdering != null) { data['view_ordering'] = viewOrdering!.map((e) => e.toString()).toList() - ..remove(ViewsEnum.profile.toString()); + ..remove(ViewsEnum.more.toString()); } return data; diff --git a/kitchenowl/lib/pages/household_add_page.dart b/kitchenowl/lib/pages/household_add_page.dart index ea2c966c7..dd573397f 100644 --- a/kitchenowl/lib/pages/household_add_page.dart +++ b/kitchenowl/lib/pages/household_add_page.dart @@ -84,7 +84,6 @@ class _HouseholdAddPageState extends State { HouseholdAddState>( askConfirmation: false, languageCanBeChanged: true, - showProfile: false, ), ), SliverCrossAxisConstrained( diff --git a/kitchenowl/lib/pages/household_page.dart b/kitchenowl/lib/pages/household_page.dart index 90b9fa777..99ecd8844 100644 --- a/kitchenowl/lib/pages/household_page.dart +++ b/kitchenowl/lib/pages/household_page.dart @@ -11,6 +11,7 @@ import 'package:kitchenowl/enums/views_enum.dart'; import 'package:kitchenowl/models/household.dart'; import 'package:kitchenowl/pages/household_page/household_drawer.dart'; import 'package:kitchenowl/pages/household_page/household_navigation_rail.dart'; +import 'package:kitchenowl/pages/household_page/more.dart'; import 'package:kitchenowl/pages/page_not_found.dart'; import 'package:responsive_builder/responsive_builder.dart'; @@ -94,6 +95,17 @@ class _HouseholdPageState extends State case ViewsEnum.balances: expenseListCubit.refresh(); break; + case ViewsEnum.more: + showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: true, + builder: (context) => BlocProvider.value( + value: householdCubit, + child: MorePage(), + ), + ); + return; default: break; } @@ -204,11 +216,13 @@ class _HouseholdPageState extends State NavigationDestinationLabelBehavior.onlyShowSelected, destinations: pages .map((e) => NavigationDestination( - icon: Icon(e.toIcon(context)), + icon: e.toIconWidget(context) ?? + Icon(e.toIcon(context)), selectedIcon: Icon(e.toSelectedIcon(context)), label: e.toLocalizedShortString(context), tooltip: e.toLocalizedString(context), )) + .take(5) .toList(), selectedIndex: _selectedIndex, onDestinationSelected: (i) => _onItemTapped( diff --git a/kitchenowl/lib/pages/household_page/_export.dart b/kitchenowl/lib/pages/household_page/_export.dart index c3ecc7540..a41279c2f 100644 --- a/kitchenowl/lib/pages/household_page/_export.dart +++ b/kitchenowl/lib/pages/household_page/_export.dart @@ -1,4 +1,4 @@ -export 'profile.dart'; +export 'more.dart'; export 'recipe_list.dart'; export 'shoppinglist.dart'; export 'planner.dart'; diff --git a/kitchenowl/lib/pages/household_page/household_drawer.dart b/kitchenowl/lib/pages/household_page/household_drawer.dart index 39670237d..f0a444349 100644 --- a/kitchenowl/lib/pages/household_page/household_drawer.dart +++ b/kitchenowl/lib/pages/household_page/household_drawer.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:go_router/go_router.dart'; import 'package:kitchenowl/app.dart'; import 'package:kitchenowl/cubits/auth_cubit.dart'; @@ -8,7 +7,7 @@ import 'package:kitchenowl/cubits/household_cubit.dart'; import 'package:kitchenowl/enums/update_enum.dart'; import 'package:kitchenowl/enums/views_enum.dart'; import 'package:kitchenowl/kitchenowl.dart'; -import 'package:transparent_image/transparent_image.dart'; +import 'package:kitchenowl/widgets/household_image.dart'; class HouseholdDrawer extends StatelessWidget { final int selectedIndex; @@ -63,22 +62,7 @@ class HouseholdDrawer extends StatelessWidget { if (state.household.image != null) Padding( padding: const EdgeInsets.only(bottom: 16), - child: ClipRRect( - borderRadius: BorderRadius.circular(14), - child: SizedBox( - height: 150, - child: FadeInImage( - fit: BoxFit.cover, - placeholder: state.household.imageHash != null - ? BlurHashImage(state.household.imageHash!) - : MemoryImage(kTransparentImage) as ImageProvider, - image: getImageProvider( - context, - state.household.image!, - ), - ), - ), - ), + child: HouseholdImage(household: state.household), ), Text( state.household.name, @@ -90,7 +74,7 @@ class HouseholdDrawer extends StatelessWidget { ), ), ), - ...pages.where((e) => e != ViewsEnum.profile).map( + ...pages.where((e) => e != ViewsEnum.more).map( (ViewsEnum destination) { return NavigationDrawerDestination( label: Text( diff --git a/kitchenowl/lib/pages/household_page/household_navigation_rail.dart b/kitchenowl/lib/pages/household_page/household_navigation_rail.dart index b7303513f..5544bddd4 100644 --- a/kitchenowl/lib/pages/household_page/household_navigation_rail.dart +++ b/kitchenowl/lib/pages/household_page/household_navigation_rail.dart @@ -37,11 +37,13 @@ class HouseholdNavigationRail extends StatelessWidget { icon: const Icon(Icons.menu_rounded), label: Text(AppLocalizations.of(context)!.more), ), - ...pages.map((e) => NavigationRailDestination( - label: Text(e.toLocalizedString(context)), - icon: Icon(e.toIcon(context)), - selectedIcon: Icon(e.toSelectedIcon(context)), - )), + ...pages + .where((e) => e != ViewsEnum.more) + .map((e) => NavigationRailDestination( + label: Text(e.toLocalizedString(context)), + icon: Icon(e.toIcon(context)), + selectedIcon: Icon(e.toSelectedIcon(context)), + )), ], selectedIndex: selectedIndex + 1, onDestinationSelected: (i) { diff --git a/kitchenowl/lib/pages/household_page/more.dart b/kitchenowl/lib/pages/household_page/more.dart new file mode 100644 index 000000000..26cafbf8b --- /dev/null +++ b/kitchenowl/lib/pages/household_page/more.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:collection/collection.dart'; +import 'package:kitchenowl/app.dart'; +import 'package:kitchenowl/cubits/auth_cubit.dart'; +import 'package:kitchenowl/cubits/household_cubit.dart'; +import 'package:kitchenowl/enums/update_enum.dart'; +import 'package:kitchenowl/enums/views_enum.dart'; +import 'package:kitchenowl/kitchenowl.dart'; +import 'package:kitchenowl/pages/household_update_page.dart'; +import 'package:kitchenowl/widgets/household_image.dart'; + +class MorePage extends StatelessWidget { + const MorePage({super.key}); + + @override + Widget build(BuildContext context) { + final householdCubit = BlocProvider.of(context); + + return SafeArea( + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.symmetric(horizontal: 8), + children: [ + BlocBuilder( + bloc: householdCubit, + builder: (context, state) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (state.household.image != null) + Padding( + padding: EdgeInsets.only( + bottom: !App.isOffline && + context.read().getUser() != null && + state.household.hasAdminRights( + context.read().getUser()!) + ? 6 + : 16), + child: HouseholdImage(household: state.household), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + state.household.name, + style: Theme.of(context).textTheme.titleSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + if (!App.isOffline && + context.read().getUser() != null && + state.household.hasAdminRights( + context.read().getUser()!)) + IconButton( + icon: Icon(Icons.edit_rounded), + onPressed: () { + context.pop(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => HouseholdUpdatePage( + household: state.household, + ), + ), + ); + }, + ), + ], + ), + ], + ), + ), + const Divider(), + BlocBuilder( + bloc: householdCubit, + buildWhen: (previous, current) => + previous.household.viewOrdering + ?.equals(current.household.viewOrdering ?? const []) ?? + true, + builder: (context, state) { + List pages = + (state.household.viewOrdering ?? ViewsEnum.values) + .where((e) => e.isViewActive(state.household)) + .skip(5) + .toList(); + if (pages.isEmpty) return const SizedBox(); + return Column( + children: pages + .map((e) => Card( + child: ListTile( + title: Text( + e.toLocalizedString(context), + ), + leading: e.toIconWidget(context) ?? + Icon(e.toIcon(context)), + minLeadingWidth: 16, + onTap: () => context.go("/household"), + ), + ) as Widget) + .toList() + + [ + const Divider(), + ], + ); + }, + ), + Card( + child: ListTile( + title: Text( + AppLocalizations.of(context)!.householdSwitch, + ), + leading: const Icon(Icons.swap_horiz_rounded), + minLeadingWidth: 16, + onTap: () => context.go("/household"), + ), + ), + Card( + child: ListTile( + title: Text( + AppLocalizations.of(context)!.profile, + ), + leading: const Icon(Icons.person_rounded), + minLeadingWidth: 16, + onTap: () { + context.pop(); + context.push("/settings/account").then((res) { + if (res == UpdateEnum.updated) { + context.read().refreshUser(); + } + }); + }, + ), + ), + Card( + child: ListTile( + title: Text( + AppLocalizations.of(context)!.settings, + ), + leading: const Icon(Icons.settings), + minLeadingWidth: 16, + onTap: () { + context.pop(); + context.push( + "/settings", + extra: householdCubit.state.household, + ); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + ); + } +} diff --git a/kitchenowl/lib/pages/household_page/profile.dart b/kitchenowl/lib/pages/household_page/profile.dart deleted file mode 100644 index 190f60292..000000000 --- a/kitchenowl/lib/pages/household_page/profile.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:kitchenowl/cubits/auth_cubit.dart'; -import 'package:kitchenowl/cubits/household_cubit.dart'; -import 'package:kitchenowl/cubits/settings_cubit.dart'; -import 'package:kitchenowl/kitchenowl.dart'; - -class ProfilePage extends StatelessWidget { - const ProfilePage({super.key}); - - @override - Widget build(BuildContext context) { - final user = - (BlocProvider.of(context).state as Authenticated).user; - - final householdCubit = BlocProvider.of(context); - - return CustomScrollView( - primary: true, - physics: const ClampingScrollPhysics(), - slivers: [ - SliverPadding( - padding: const EdgeInsets.fromLTRB(16, 64, 16, 16), - sliver: SliverList( - delegate: SliverChildListDelegate([ - CircleAvatar( - foregroundImage: user.image?.isEmpty ?? true - ? null - : getImageProvider( - context, - user.image!, - ), - radius: 45, - child: Text( - user.name.substring(0, 1), - textScaler: - MediaQuery.textScalerOf(context).clamp(minScaleFactor: 2), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Text( - user.name, - style: Theme.of(context).textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ), - ]), - ), - ), - BlocBuilder( - builder: (context, state) => SliverFillRemaining( - hasScrollBody: false, - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 0, 8, 6), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - if (!kIsWeb) - ListTile( - title: - Text(AppLocalizations.of(context)!.forceOfflineMode), - leading: const Icon(Icons.mobiledata_off_outlined), - contentPadding: const EdgeInsets.only(left: 20, right: 0), - onTap: () => BlocProvider.of(context) - .setForcedOfflineMode( - !BlocProvider.of(context) - .state - .forcedOfflineMode, - ), - trailing: BlocBuilder( - buildWhen: (previous, current) => - previous.forcedOfflineMode != - current.forcedOfflineMode, - builder: (context, state) => KitchenOwlSwitch( - value: state.forcedOfflineMode, - onChanged: (value) => - BlocProvider.of(context) - .setForcedOfflineMode(value), - ), - ), - ), - Card( - child: ListTile( - title: Text( - AppLocalizations.of(context)!.householdSwitch, - ), - leading: const Icon(Icons.swap_horiz_rounded), - minLeadingWidth: 16, - onTap: () => context.go("/household"), - ), - ), - Card( - child: ListTile( - title: Text( - AppLocalizations.of(context)!.settings, - ), - leading: const Icon(Icons.manage_accounts_rounded), - minLeadingWidth: 16, - onTap: () => context.push( - "/settings", - extra: householdCubit.state.household, - ), - ), - ), - ], - ), - ), - ), - ), - ], - ); - } -} diff --git a/kitchenowl/lib/router.dart b/kitchenowl/lib/router.dart index aadf44b9c..c9da5eb82 100644 --- a/kitchenowl/lib/router.dart +++ b/kitchenowl/lib/router.dart @@ -335,14 +335,6 @@ final router = GoRouter( ), ], ), - GoRoute( - path: "profile", - pageBuilder: (context, state) => FadeThroughTransitionPage( - key: state.pageKey, - name: state.name, - child: const ProfilePage(), - ), - ), ], ), ], diff --git a/kitchenowl/lib/widgets/household_card.dart b/kitchenowl/lib/widgets/household_card.dart index c6ae16ef9..24c02e36a 100644 --- a/kitchenowl/lib/widgets/household_card.dart +++ b/kitchenowl/lib/widgets/household_card.dart @@ -1,14 +1,13 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_blurhash/flutter_blurhash.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:kitchenowl/cubits/auth_cubit.dart'; import 'package:kitchenowl/cubits/household_list_cubit.dart'; import 'package:kitchenowl/kitchenowl.dart'; import 'package:kitchenowl/models/household.dart'; -import 'package:transparent_image/transparent_image.dart'; +import 'package:kitchenowl/widgets/household_image.dart'; class HouseholdCard extends StatelessWidget { final Household household; @@ -64,24 +63,7 @@ class HouseholdCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (household.image?.isNotEmpty ?? false) - ClipRRect( - borderRadius: const BorderRadius.vertical( - bottom: Radius.circular(14), - ), - child: SizedBox( - height: 150, - child: FadeInImage( - fit: BoxFit.cover, - placeholder: household.imageHash != null - ? BlurHashImage(household.imageHash!) - : MemoryImage(kTransparentImage) as ImageProvider, - image: getImageProvider( - context, - household.image!, - ), - ), - ), - ), + HouseholdImage(household: household), Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 6), child: Text( diff --git a/kitchenowl/lib/widgets/household_image.dart b/kitchenowl/lib/widgets/household_image.dart new file mode 100644 index 000000000..b0e9037c5 --- /dev/null +++ b/kitchenowl/lib/widgets/household_image.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_blurhash/flutter_blurhash.dart'; +import 'package:kitchenowl/models/household.dart'; +import 'package:kitchenowl/widgets/image_provider.dart'; +import 'package:transparent_image/transparent_image.dart'; + +class HouseholdImage extends StatelessWidget { + final Household household; + + const HouseholdImage({super.key, required this.household}); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(14), + child: SizedBox( + height: 150, + child: FadeInImage( + fit: BoxFit.cover, + placeholder: household.imageHash != null + ? BlurHashImage(household.imageHash!) + : MemoryImage(kTransparentImage) as ImageProvider, + image: getImageProvider( + context, + household.image!, + ), + ), + ), + ); + } +} diff --git a/kitchenowl/lib/widgets/settings_household/sliver_household_feature_settings.dart b/kitchenowl/lib/widgets/settings_household/sliver_household_feature_settings.dart index 128cc6d65..f1a28b7e1 100644 --- a/kitchenowl/lib/widgets/settings_household/sliver_household_feature_settings.dart +++ b/kitchenowl/lib/widgets/settings_household/sliver_household_feature_settings.dart @@ -15,13 +15,11 @@ class SliverHouseholdFeatureSettings< State extends HouseholdAddUpdateState> extends StatelessWidget { final bool languageCanBeChanged; final bool askConfirmation; - final bool showProfile; const SliverHouseholdFeatureSettings({ super.key, this.languageCanBeChanged = false, this.askConfirmation = true, - this.showProfile = true, }); @override @@ -66,11 +64,6 @@ class SliverHouseholdFeatureSettings< .toList(), ), ), - if (showProfile) - const ViewSettingsListTile( - view: ViewsEnum.profile, - showHandleIfNotOptional: false, - ), Center(child: Text(AppLocalizations.of(context)!.longPressToReorder)), const SizedBox(height: 8), const Divider(indent: 16, endIndent: 16),