From 1f3741b4fe1d805eb05103ea5506876005ed072f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Harrault?= <benoit@harrault.fr> Date: Fri, 29 Nov 2024 16:47:59 +0100 Subject: [PATCH] Add application navigation widgets --- CHANGELOG.md | 4 + lib/flutter_toolbox.dart | 15 ++ .../application_navigation_definition.dart | 130 ++++++++++++++++++ lib/nav/cubit/nav_cubit_pages.dart | 27 ++++ lib/nav/cubit/nav_cubit_screens.dart | 27 ++++ lib/nav/ui/bottom_nav_bar.dart | 48 +++++++ lib/nav/ui/global_app_bar.dart | 98 +++++++++++++ lib/nav/ui/screens/about.dart | 38 +++++ lib/nav/ui/screens/activity.dart | 29 ++++ lib/nav/ui/screens/settings.dart | 24 ++++ .../application_config_definition.dart | 6 + lib/parameters/pages/parameters.dart | 14 +- pubspec.yaml | 2 +- 13 files changed, 454 insertions(+), 8 deletions(-) create mode 100644 lib/nav/application_navigation_definition.dart create mode 100644 lib/nav/cubit/nav_cubit_pages.dart create mode 100644 lib/nav/cubit/nav_cubit_screens.dart create mode 100644 lib/nav/ui/bottom_nav_bar.dart create mode 100644 lib/nav/ui/global_app_bar.dart create mode 100644 lib/nav/ui/screens/about.dart create mode 100644 lib/nav/ui/screens/activity.dart create mode 100644 lib/nav/ui/screens/settings.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index ca4d549..6ecfe23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.0 + +- Add application navigation widgets + ## 0.5.0 - Add activity parameters widgets diff --git a/lib/flutter_toolbox.dart b/lib/flutter_toolbox.dart index f4b591f..4676362 100644 --- a/lib/flutter_toolbox.dart +++ b/lib/flutter_toolbox.dart @@ -33,6 +33,21 @@ export 'parameters/settings/settings_activity_cubit.dart' show ActivitySettingsCubit, ActivitySettingsState; export 'parameters/models/settings/settings_activity.dart' show ActivitySettings; +export 'nav/application_navigation_definition.dart' + show + ScreenItem, + ActivityPageItem, + ApplicationNavigation, + AppBarConfiguration, + AppBarButton; +export 'nav/cubit/nav_cubit_screens.dart' show NavCubitScreen; +export 'nav/cubit/nav_cubit_pages.dart' show NavCubitPage; +export 'nav/ui/screens/about.dart' show ScreenAbout; +export 'nav/ui/screens/activity.dart' show ScreenActivity; +export 'nav/ui/screens/settings.dart' show ScreenSettings; +export 'nav/ui/bottom_nav_bar.dart' show BottomNavBar; +export 'nav/ui/global_app_bar.dart' show GlobalAppBar; + // dependencies export 'package:easy_localization/easy_localization.dart' diff --git a/lib/nav/application_navigation_definition.dart b/lib/nav/application_navigation_definition.dart new file mode 100644 index 0000000..5e5809e --- /dev/null +++ b/lib/nav/application_navigation_definition.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +/// A screen item, +/// +/// Normalized screens are: "activity", "settings" and "about". +/// +/// These are set in [ApplicationNavigation] with +/// - [ApplicationNavigation.screenActivity] +/// - [ApplicationNavigation.screenSettings] +/// - [ApplicationNavigation.screenAbout] +/// +class ScreenItem { + final String code; + final Icon icon; + final Widget Function({required ApplicationConfigDefinition appConfig}) screen; + + const ScreenItem({ + required this.code, + required this.icon, + required this.screen, + }); +} + +/// A page in [ScreenItem] given in [ApplicationNavigation.screenActivity] +/// +/// These are set in [ApplicationNavigation] with +/// [ApplicationNavigation.activityPages]. +/// +class ActivityPageItem { + final String code; + final Icon? icon; + final Widget Function({required ApplicationConfigDefinition appConfig}) builder; + + const ActivityPageItem({ + required this.code, + this.icon, + required this.builder, + }); + + factory ActivityPageItem.empty() { + return ActivityPageItem( + code: '', + builder: ({required ApplicationConfigDefinition appConfig}) => Text(''), + ); + } +} + +/// Custom AppBar configuration +/// +class AppBarConfiguration { + final bool? hideApplicationTitle; + final bool? pushQuitActivityButtonLeft; + final bool? hideQuitActivityButton; + final List<AppBarButton> Function(BuildContext context)? topBarButtonsBuilder; + + const AppBarConfiguration({ + this.hideApplicationTitle = false, + this.pushQuitActivityButtonLeft = false, + this.hideQuitActivityButton = false, + this.topBarButtonsBuilder, + }); +} + +/// Custom AppBar button (will generate a widget [IconButton]) +/// +class AppBarButton { + final Icon icon; + final Function(BuildContext context)? onPressed; + final Function(BuildContext context)? onLongPress; + + const AppBarButton({ + required this.icon, + this.onPressed, + this.onLongPress, + }); +} + +/// Navigation configuration for application +/// +class ApplicationNavigation { + final ScreenItem screenActivity; + final ScreenItem screenSettings; + final ScreenItem screenAbout; + final AppBarConfiguration? appBarConfiguration; + final Map<int, ActivityPageItem> activityPages; + final bool displayBottomNavBar; + + const ApplicationNavigation({ + required this.screenActivity, + required this.screenSettings, + required this.screenAbout, + this.appBarConfiguration, + required this.activityPages, + this.displayBottomNavBar = false, + }); + + static const indexActivity = 0; + static const indexSettings = 1; + static const indexAbout = 2; + + ScreenItem getScreen(int screenIndex) { + switch (screenIndex) { + case indexActivity: + return screenActivity; + case indexSettings: + return screenSettings; + case indexAbout: + return screenAbout; + default: + return screenActivity; + } + } + + ScreenItem getActivityScreen() { + return screenActivity; + } + + Widget getScreenWidget({ + required ApplicationConfigDefinition appConfig, + required int screenIndex, + }) { + return getScreen(screenIndex).screen(appConfig: appConfig); + } + + bool isActivityPageIndexAllowed(int pageIndex) { + return activityPages.keys.contains(pageIndex); + } +} diff --git a/lib/nav/cubit/nav_cubit_pages.dart b/lib/nav/cubit/nav_cubit_pages.dart new file mode 100644 index 0000000..cf38b76 --- /dev/null +++ b/lib/nav/cubit/nav_cubit_pages.dart @@ -0,0 +1,27 @@ +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class NavCubitPage extends HydratedCubit<int> { + NavCubitPage({ + required this.appConfig, + }) : super(0); + + final ApplicationConfigDefinition appConfig; + + void updateIndex(int index) { + if (appConfig.navigation.isActivityPageIndexAllowed(index)) { + emit(index); + } else { + emit(0); + } + } + + @override + int fromJson(Map<String, dynamic> json) { + return 0; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'index': state}; + } +} diff --git a/lib/nav/cubit/nav_cubit_screens.dart b/lib/nav/cubit/nav_cubit_screens.dart new file mode 100644 index 0000000..1b15747 --- /dev/null +++ b/lib/nav/cubit/nav_cubit_screens.dart @@ -0,0 +1,27 @@ +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class NavCubitScreen extends HydratedCubit<int> { + NavCubitScreen() : super(0); + + void goToScreenActivity() { + emit(0); + } + + void goToScreenSettings() { + emit(1); + } + + void goToScreenAbout() { + emit(2); + } + + @override + int fromJson(Map<String, dynamic> json) { + return 0; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'index': state}; + } +} diff --git a/lib/nav/ui/bottom_nav_bar.dart b/lib/nav/ui/bottom_nav_bar.dart new file mode 100644 index 0000000..3dc549a --- /dev/null +++ b/lib/nav/ui/bottom_nav_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class BottomNavBar extends StatelessWidget { + const BottomNavBar({ + super.key, + required this.appConfig, + }); + + final ApplicationConfigDefinition appConfig; + + @override + Widget build(BuildContext context) { + return Card( + margin: const EdgeInsets.only(top: 1, right: 4, left: 4), + elevation: 4, + shadowColor: Theme.of(context).colorScheme.shadow, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: BlocBuilder<NavCubitPage, int>(builder: (BuildContext context, int state) { + final List<BottomNavigationBarItem> items = []; + + appConfig.navigation.activityPages.forEach((int pageIndex, ActivityPageItem item) { + items.add(BottomNavigationBarItem( + icon: item.icon ?? Icon(null), + label: tr(item.code), + )); + }); + + return BottomNavigationBar( + currentIndex: state, + onTap: (int index) => BlocProvider.of<NavCubitPage>(context).updateIndex(index), + type: BottomNavigationBarType.fixed, + elevation: 0, + backgroundColor: Colors.transparent, + selectedItemColor: Theme.of(context).colorScheme.primary, + unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color, + items: items, + ); + }), + ); + } +} diff --git a/lib/nav/ui/global_app_bar.dart b/lib/nav/ui/global_app_bar.dart new file mode 100644 index 0000000..86d42a4 --- /dev/null +++ b/lib/nav/ui/global_app_bar.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { + const GlobalAppBar({ + super.key, + required this.appConfig, + required this.pageIndex, + required this.isActivityRunning, + }); + + final ApplicationConfigDefinition appConfig; + final int pageIndex; + final bool isActivityRunning; + + @override + Widget build(BuildContext context) { + final bool displayQuitActivityButton = + !(appConfig.navigation.appBarConfiguration?.hideQuitActivityButton ?? false); + + final bool pushQuitActivityButtonLeft = + (appConfig.navigation.appBarConfiguration?.pushQuitActivityButtonLeft == true); + + final bool showApplicationTitle = + !(appConfig.navigation.appBarConfiguration?.hideApplicationTitle ?? false); + + final List<AppBarButton> Function(BuildContext context) builder = + appConfig.navigation.appBarConfiguration?.topBarButtonsBuilder ?? + // Default top bar buttons + (BuildContext context) { + if (isActivityRunning) { + return <AppBarButton>[]; + } + + return <AppBarButton>[ + // Go to About page + AppBarButton( + onPressed: (BuildContext context) => + BlocProvider.of<NavCubitScreen>(context).goToScreenAbout(), + icon: appConfig.navigation.screenAbout.icon, + ), + + // Go to Settings page + AppBarButton( + onPressed: (BuildContext context) => + BlocProvider.of<NavCubitScreen>(context).goToScreenSettings(), + icon: appConfig.navigation.screenSettings.icon, + ), + + // Back to Home page + AppBarButton( + onPressed: (BuildContext context) => + BlocProvider.of<NavCubitScreen>(context).goToScreenActivity(), + icon: appConfig.navigation.screenActivity.icon, + ), + ]; + }; + + final List<Widget> menuActions = []; + + // left pushed "quit activity" button + if (isActivityRunning && displayQuitActivityButton && pushQuitActivityButtonLeft) { + menuActions.add(ActivityButtonQuit( + onPressed: () {}, + onLongPress: () => appConfig.quitCurrentActivity(context), + )); + + menuActions.add(const Spacer(flex: 6)); + } + + // add buttons + final List<AppBarButton> buttons = builder(context); + for (AppBarButton button in buttons) { + menuActions.add(ElevatedButton( + onPressed: () => button.onPressed!(context), + onLongPress: () => button.onLongPress!(context), + style: ElevatedButton.styleFrom(shape: const CircleBorder()), + child: button.icon, + )); + } + + // standard right pushed "quit activity" button + if (isActivityRunning && displayQuitActivityButton && !pushQuitActivityButtonLeft) { + menuActions.add(ActivityButtonQuit( + onPressed: () {}, + onLongPress: () => appConfig.quitCurrentActivity(context), + )); + } + + return AppBar( + title: showApplicationTitle ? const AppHeader(text: 'app_name') : null, + actions: menuActions, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/nav/ui/screens/about.dart b/lib/nav/ui/screens/about.dart new file mode 100644 index 0000000..f7a14a9 --- /dev/null +++ b/lib/nav/ui/screens/about.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class ScreenAbout extends StatelessWidget { + const ScreenAbout({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + const SizedBox(height: 8), + const AppTitle(text: 'about_title'), + const Text('about_content').tr(), + FutureBuilder<PackageInfo>( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return const Text('about_version').tr( + namedArgs: { + 'version': snapshot.data!.version, + }, + ); + default: + return const SizedBox(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/nav/ui/screens/activity.dart b/lib/nav/ui/screens/activity.dart new file mode 100644 index 0000000..f1d1f31 --- /dev/null +++ b/lib/nav/ui/screens/activity.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class ScreenActivity extends StatelessWidget { + const ScreenActivity({ + super.key, + required this.appConfig, + }); + + final ApplicationConfigDefinition appConfig; + + ActivityPageItem getActivityPage(int pageIndex) { + if (appConfig.navigation.activityPages.keys.contains(pageIndex)) { + return appConfig.navigation.activityPages[pageIndex] ?? ActivityPageItem.empty(); + } else { + return getActivityPage(appConfig.navigation.activityPages.keys.first); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder<NavCubitPage, int>( + builder: (BuildContext context, int pageIndex) { + final ActivityPageItem page = getActivityPage(pageIndex); + return page.builder(appConfig: appConfig); + }, + ); + } +} diff --git a/lib/nav/ui/screens/settings.dart b/lib/nav/ui/screens/settings.dart new file mode 100644 index 0000000..7981b1c --- /dev/null +++ b/lib/nav/ui/screens/settings.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; + +class ScreenSettings extends StatelessWidget { + const ScreenSettings({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 8), + AppTitle(text: 'settings_title'), + SizedBox(height: 8), + ApplicationSettingsForm(), + ], + ), + ); + } +} diff --git a/lib/parameters/application_config_definition.dart b/lib/parameters/application_config_definition.dart index 82c0e2e..2abba76 100644 --- a/lib/parameters/application_config_definition.dart +++ b/lib/parameters/application_config_definition.dart @@ -5,16 +5,22 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; class ApplicationConfigDefinition { final String appTitle; final List<ApplicationSettingsParameter> activitySettings; + final bool autoStartActivity; final void Function(BuildContext context) startNewActivity; + final void Function(BuildContext context) quitCurrentActivity; final void Function(BuildContext context) deleteCurrentActivity; final void Function(BuildContext context) resumeActivity; + final ApplicationNavigation navigation; const ApplicationConfigDefinition({ required this.appTitle, required this.activitySettings, + this.autoStartActivity = false, required this.startNewActivity, + required this.quitCurrentActivity, required this.deleteCurrentActivity, required this.resumeActivity, + required this.navigation, }); ApplicationSettingsParameter getFromCode(String paramCode) { diff --git a/lib/parameters/pages/parameters.dart b/lib/parameters/pages/parameters.dart index 92a065d..4d98e61 100644 --- a/lib/parameters/pages/parameters.dart +++ b/lib/parameters/pages/parameters.dart @@ -5,11 +5,11 @@ import 'package:flutter_custom_toolbox/flutter_toolbox.dart'; class PageParameters extends StatelessWidget { const PageParameters({ super.key, - required this.config, + required this.appConfig, required this.canBeResumed, }); - final ApplicationConfigDefinition config; + final ApplicationConfigDefinition appConfig; final bool canBeResumed; final double separatorHeight = 8.0; @@ -19,7 +19,7 @@ class PageParameters extends StatelessWidget { final List<Widget> lines = []; // Activity settings (top) - for (ApplicationSettingsParameter parameter in config.activitySettings) { + for (ApplicationSettingsParameter parameter in appConfig.activitySettings) { if (parameter.displayedOnTop) { lines.add(Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -43,7 +43,7 @@ class PageParameters extends StatelessWidget { aspectRatio: 3, child: ActivityButtonResumeSaved( onPressed: () { - config.resumeActivity(context); + appConfig.resumeActivity(context); }, ), )); @@ -52,7 +52,7 @@ class PageParameters extends StatelessWidget { dimension: MediaQuery.of(context).size.width / 5, child: ActivityButtonDeleteSaved( onPressed: () { - config.deleteCurrentActivity(context); + appConfig.deleteCurrentActivity(context); }, ), )); @@ -63,7 +63,7 @@ class PageParameters extends StatelessWidget { aspectRatio: 3, child: ActivityButtonStartNew( onPressed: () { - config.startNewActivity(context); + appConfig.startNewActivity(context); }, ), ), @@ -73,7 +73,7 @@ class PageParameters extends StatelessWidget { lines.add(SizedBox(height: separatorHeight)); // Activity settings (bottom) - for (ApplicationSettingsParameter parameter in config.activitySettings) { + for (ApplicationSettingsParameter parameter in appConfig.activitySettings) { if (!parameter.displayedOnTop) { lines.add(Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/pubspec.yaml b/pubspec.yaml index 231b7b6..883ba0b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "Flutter custom toolbox for org.benoitharrault.* projects." publish_to: "none" -version: 0.5.0 +version: 0.6.0 homepage: https://git.harrault.fr/android/flutter-toolbox -- GitLab