From 9c8dad0176268759ccc477d0af07b62fb482840c Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Thu, 14 Nov 2024 00:08:09 +0800 Subject: [PATCH] :sparkles: Drawer navigation --- assets/translations/en-US.json | 2 + assets/translations/zh-CN.json | 2 + lib/main.dart | 3 + lib/providers/navigation.dart | 112 ++++++++++++++++++ lib/router.dart | 14 +++ lib/screens/album.dart | 10 ++ lib/screens/chat.dart | 10 ++ .../navigation/app_bottom_navigation.dart | 53 ++++++--- lib/widgets/navigation/app_destinations.dart | 33 ------ .../navigation/app_drawer_navigation.dart | 76 ++++++++++++ lib/widgets/navigation/app_scaffold.dart | 7 +- 11 files changed, 274 insertions(+), 48 deletions(-) create mode 100644 lib/providers/navigation.dart create mode 100644 lib/screens/album.dart create mode 100644 lib/screens/chat.dart delete mode 100644 lib/widgets/navigation/app_destinations.dart create mode 100644 lib/widgets/navigation/app_drawer_navigation.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index d8c1f02..d8dd878 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -15,6 +15,8 @@ "screenAccountPublisherEdit": "Edit Publisher", "screenAccountProfileEdit": "Edit Profile", "screenSettings": "Settings", + "screenAlbum": "Album", + "screenChat": "Chat", "dialogOkay": "Okay", "dialogCancel": "Cancel", "dialogConfirm": "Confirm", diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 8fe5d09..c6c4202 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -15,6 +15,8 @@ "screenAccountPublisherEdit": "编辑发布者", "screenAccountProfileEdit": "编辑资料", "screenSettings": "设置", + "screenAlbum": "相册", + "screenChat": "聊天", "dialogOkay": "好的", "dialogCancel": "取消", "dialogConfirm": "确认", diff --git a/lib/main.dart b/lib/main.dart index ab347d1..640c460 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; import 'package:responsive_framework/responsive_framework.dart'; +import 'package:surface/providers/navigation.dart'; import 'package:surface/providers/sn_attachment.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/theme.dart'; @@ -39,6 +40,7 @@ class SolianApp extends StatelessWidget { providers: [ Provider(create: (_) => SnNetworkProvider()), Provider(create: (ctx) => SnAttachmentProvider(ctx)), + ChangeNotifierProvider(create: (ctx) => NavigationProvider()), ChangeNotifierProvider(create: (ctx) => UserProvider(ctx)), ChangeNotifierProvider(create: (_) => ThemeProvider()), ], @@ -59,6 +61,7 @@ class AppMainContent extends StatelessWidget { @override Widget build(BuildContext context) { + context.read(); context.read(); final th = context.watch(); diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart new file mode 100644 index 0000000..3fb101e --- /dev/null +++ b/lib/providers/navigation.dart @@ -0,0 +1,112 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppNavDestination { + final String label; + final String screen; + final Widget icon; + final bool isPinned; + + const AppNavDestination({ + required this.label, + required this.screen, + required this.icon, + this.isPinned = false, + }); +} + +class NavigationProvider extends ChangeNotifier { + int? _currentIndex; + + int? get currentIndex => _currentIndex; + + static const List kAllDestination = [ + AppNavDestination( + icon: Icon(Symbols.home, weight: 400, opticalSize: 20), + screen: 'home', + label: 'screenHome', + ), + AppNavDestination( + icon: Icon(Symbols.explore, weight: 400, opticalSize: 20), + screen: 'explore', + label: 'screenExplore', + ), + AppNavDestination( + icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20), + screen: 'account', + label: 'screenAccount', + ), + AppNavDestination( + icon: Icon(Symbols.album, weight: 400, opticalSize: 20), + screen: 'album', + label: 'screenAlbum', + ), + AppNavDestination( + icon: Icon(Symbols.chat, weight: 400, opticalSize: 20), + screen: 'chat', + label: 'screenChat', + ), + ]; + static const List kDefaultPinnedDestination = [ + 'home', + 'explore', + 'account' + ]; + + List destinations = []; + + int get pinnedDestinationCount => + destinations.where((ele) => ele.isPinned).length; + + NavigationProvider() { + buildDestinations(kDefaultPinnedDestination); + SharedPreferences.getInstance().then((prefs) { + final pinned = prefs.getStringList("app_pinned_navigation"); + if (pinned != null) buildDestinations(pinned); + }); + } + + void buildDestinations(List pinned) { + destinations = kAllDestination + .map( + (ele) => AppNavDestination( + label: ele.label, + screen: ele.screen, + icon: ele.icon, + isPinned: pinned.contains(ele.screen), + ), + ) + .toList(); + notifyListeners(); + } + + int getIndexInRange(int min, int max) { + return math.max(min, math.min(_currentIndex ?? 0, max)); + } + + bool isIndexInRange(int min, int max) { + return _currentIndex != null && + _currentIndex! >= min && + _currentIndex! < max; + } + + void autoDetectIndex(GoRouter? state) { + if (state == null) return; + final idx = destinations.indexWhere( + (ele) => + ele.screen == + state.routerDelegate.currentConfiguration.last.route.name, + ); + _currentIndex = idx == -1 ? null : idx; + notifyListeners(); + } + + void setIndex(int idx) { + _currentIndex = idx; + notifyListeners(); + } +} diff --git a/lib/router.dart b/lib/router.dart index 5b2b6d7..d0ab996 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,8 +4,10 @@ import 'package:surface/screens/account/profile_edit.dart'; import 'package:surface/screens/account/publishers/publisher_edit.dart'; import 'package:surface/screens/account/publishers/publisher_new.dart'; import 'package:surface/screens/account/publishers/publishers.dart'; +import 'package:surface/screens/album.dart'; import 'package:surface/screens/auth/login.dart'; import 'package:surface/screens/auth/register.dart'; +import 'package:surface/screens/chat.dart'; import 'package:surface/screens/explore.dart'; import 'package:surface/screens/home.dart'; import 'package:surface/screens/post/post_detail.dart'; @@ -20,6 +22,7 @@ final appRouter = GoRouter( builder: (context, state, child) => AppScaffold( body: child, showBottomNavigation: true, + showDrawer: true, ), routes: [ GoRoute( @@ -37,6 +40,16 @@ final appRouter = GoRouter( name: 'account', builder: (context, state) => const AccountScreen(), ), + GoRoute( + path: '/chat', + name: 'chat', + builder: (context, state) => const ChatScreen(), + ), + GoRoute( + path: '/album', + name: 'album', + builder: (context, state) => const AlbumScreen(), + ), ], ), ShellRoute( @@ -74,6 +87,7 @@ final appRouter = GoRouter( builder: (context, state, child) => AppScaffold( body: child, autoImplyAppBar: true, + showDrawer: true, ), routes: [ GoRoute( diff --git a/lib/screens/album.dart b/lib/screens/album.dart new file mode 100644 index 0000000..4c91e30 --- /dev/null +++ b/lib/screens/album.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class AlbumScreen extends StatelessWidget { + const AlbumScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart new file mode 100644 index 0000000..437de3f --- /dev/null +++ b/lib/screens/chat.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ChatScreen extends StatelessWidget { + const ChatScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/lib/widgets/navigation/app_bottom_navigation.dart b/lib/widgets/navigation/app_bottom_navigation.dart index 71c207a..1a40060 100644 --- a/lib/widgets/navigation/app_bottom_navigation.dart +++ b/lib/widgets/navigation/app_bottom_navigation.dart @@ -1,6 +1,8 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:surface/widgets/navigation/app_destinations.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/navigation.dart'; class AppBottomNavigationBar extends StatefulWidget { const AppBottomNavigationBar({super.key}); @@ -10,23 +12,46 @@ class AppBottomNavigationBar extends StatefulWidget { } class _AppBottomNavigationBarState extends State { - int _currentIndex = 0; + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context + .read() + .autoDetectIndex(GoRouter.maybeOf(context)); + }); + } @override Widget build(BuildContext context) { - return BottomNavigationBar( - currentIndex: _currentIndex, - type: BottomNavigationBarType.fixed, - showUnselectedLabels: false, - items: appDestinations.map((ele) { - return BottomNavigationBarItem( - icon: ele.icon, - label: ele.label, + final nav = context.watch(); + + return ListenableBuilder( + listenable: nav, + builder: (context, _) { + if (!nav.isIndexInRange(0, nav.pinnedDestinationCount)) { + return const SizedBox.shrink(); + } + + final destinations = [ + ...nav.destinations.where((ele) => ele.isPinned), + ]; + + return BottomNavigationBar( + currentIndex: nav.getIndexInRange(0, nav.pinnedDestinationCount), + type: BottomNavigationBarType.fixed, + showUnselectedLabels: false, + items: destinations.map((ele) { + return BottomNavigationBarItem( + icon: ele.icon, + label: ele.label.tr(), + ); + }).toList(), + onTap: (idx) { + nav.setIndex(idx); + GoRouter.of(context).goNamed(destinations[idx].screen); + }, ); - }).toList(), - onTap: (idx) { - setState(() => _currentIndex = idx); - GoRouter.of(context).goNamed(appDestinations[idx].screen); }, ); } diff --git a/lib/widgets/navigation/app_destinations.dart b/lib/widgets/navigation/app_destinations.dart deleted file mode 100644 index 01e371b..0000000 --- a/lib/widgets/navigation/app_destinations.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -class AppNavDestination { - final String label; - final String screen; - final Widget icon; - - AppNavDestination({ - required this.label, - required this.screen, - required this.icon, - }); -} - -List appDestinations = [ - AppNavDestination( - icon: Icon(Symbols.home, weight: 400, opticalSize: 20), - screen: 'home', - label: tr('screenHome'), - ), - AppNavDestination( - icon: Icon(Symbols.explore, weight: 400, opticalSize: 20), - screen: 'explore', - label: tr('screenExplore'), - ), - AppNavDestination( - icon: Icon(Symbols.account_circle, weight: 400, opticalSize: 20), - screen: 'account', - label: tr('screenAccount'), - ), -]; diff --git a/lib/widgets/navigation/app_drawer_navigation.dart b/lib/widgets/navigation/app_drawer_navigation.dart new file mode 100644 index 0000000..41e94c8 --- /dev/null +++ b/lib/widgets/navigation/app_drawer_navigation.dart @@ -0,0 +1,76 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/navigation.dart'; + +class AppNavigationDrawer extends StatefulWidget { + const AppNavigationDrawer({super.key}); + + @override + State createState() => _AppNavigationDrawerState(); +} + +class _AppNavigationDrawerState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context + .read() + .autoDetectIndex(GoRouter.maybeOf(context)); + }); + } + + @override + Widget build(BuildContext context) { + final nav = context.watch(); + + return ListenableBuilder( + listenable: nav, + builder: (context, _) { + final destinations = [ + ...nav.destinations.where((ele) => ele.isPinned), + ...nav.destinations.where((ele) => !ele.isPinned), + ]; + + return NavigationDrawer( + selectedIndex: nav.currentIndex, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Solar Network').bold(), + Text('Solar Network 2.0α').fontSize(12).textColor( + Theme.of(context).colorScheme.onSurface.withOpacity(0.5)), + ], + ).padding( + horizontal: 32, + vertical: 8, + ), + ...destinations.where((ele) => ele.isPinned).map((ele) { + return NavigationDrawerDestination( + icon: ele.icon, + label: Text(ele.label).tr(), + ); + }), + const Divider(), + ...destinations.where((ele) => !ele.isPinned).map((ele) { + return NavigationDrawerDestination( + icon: ele.icon, + label: Text(ele.label).tr(), + ); + }), + ], + onDestinationSelected: (idx) { + nav.setIndex(idx); + GoRouter.of(context).goNamed(destinations[idx].screen); + Scaffold.of(context).closeDrawer(); + }, + ); + }, + ); + } +} diff --git a/lib/widgets/navigation/app_scaffold.dart b/lib/widgets/navigation/app_scaffold.dart index 302f4d3..acf932e 100644 --- a/lib/widgets/navigation/app_scaffold.dart +++ b/lib/widgets/navigation/app_scaffold.dart @@ -5,6 +5,7 @@ import 'package:responsive_framework/responsive_framework.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_bottom_navigation.dart'; +import 'package:surface/widgets/navigation/app_drawer_navigation.dart'; class AppScaffold extends StatelessWidget { final PreferredSizeWidget? appBar; @@ -14,6 +15,7 @@ class AppScaffold extends StatelessWidget { final Widget? body; final bool autoImplyAppBar; final bool showBottomNavigation; + final bool showDrawer; const AppScaffold({ super.key, this.appBar, @@ -23,12 +25,14 @@ class AppScaffold extends StatelessWidget { this.body, this.autoImplyAppBar = false, this.showBottomNavigation = false, + this.showDrawer = false, }); @override Widget build(BuildContext context) { + final isShowDrawer = showDrawer; final isShowBottomNavigation = (showBottomNavigation) - ? ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE) + ? (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE)) : false; final state = GoRouter.maybeOf(context); @@ -50,6 +54,7 @@ class AppScaffold extends StatelessWidget { body: body, floatingActionButtonLocation: floatingActionButtonLocation, floatingActionButton: floatingActionButton, + drawer: isShowDrawer ? AppNavigationDrawer() : null, bottomNavigationBar: isShowBottomNavigation ? AppBottomNavigationBar() : null, ),