From 66ddfea68d9e74299124f6280bfb2db25d2cc091 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 6 Jul 2024 20:55:53 +0800 Subject: [PATCH] :lipstick: Better bottom navigation --- lib/main.dart | 8 +- lib/router.dart | 390 +++++++++++---------- lib/screens/auth/signin.dart | 36 +- lib/screens/{social.dart => feed.dart} | 53 +-- lib/shells/listener_shell.dart | 39 --- lib/shells/nav_shell.dart | 5 +- lib/shells/root_shell.dart | 63 ++++ lib/translations.dart | 4 +- lib/widgets/navigation/app_navigation.dart | 6 +- 9 files changed, 326 insertions(+), 278 deletions(-) rename lib/screens/{social.dart => feed.dart} (73%) delete mode 100644 lib/shells/listener_shell.dart create mode 100644 lib/shells/root_shell.dart diff --git a/lib/main.dart b/lib/main.dart index 229bb2c..39dacba 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,7 +19,6 @@ import 'package:solian/providers/content/realm.dart'; import 'package:solian/providers/friend.dart'; import 'package:solian/providers/account_status.dart'; import 'package:solian/router.dart'; -import 'package:solian/shells/listener_shell.dart'; import 'package:solian/theme.dart'; import 'package:solian/translations.dart'; @@ -83,15 +82,14 @@ class SolianApp extends StatelessWidget { routerDelegate: AppRouter.instance.routerDelegate, routeInformationParser: AppRouter.instance.routeInformationParser, routeInformationProvider: AppRouter.instance.routeInformationProvider, + backButtonDispatcher: AppRouter.instance.backButtonDispatcher, translations: SolianMessages(), locale: Get.deviceLocale, fallbackLocale: const Locale('en', 'US'), onInit: () => _initializeProviders(context), builder: (context, child) { - return ListenerShell( - child: ScaffoldMessenger( - child: child ?? Container(), - ), + return ScaffoldMessenger( + child: child ?? const SizedBox(), ); }, ); diff --git a/lib/router.dart b/lib/router.dart index acedfe8..6bfcdff 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -13,10 +13,10 @@ import 'package:solian/screens/realms.dart'; import 'package:solian/screens/realms/realm_detail.dart'; import 'package:solian/screens/realms/realm_organize.dart'; import 'package:solian/screens/realms/realm_view.dart'; -import 'package:solian/screens/social.dart'; +import 'package:solian/screens/feed.dart'; import 'package:solian/screens/posts/post_publish.dart'; import 'package:solian/shells/basic_shell.dart'; -import 'package:solian/shells/nav_shell.dart'; +import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/title_shell.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/sidebar/empty_placeholder.dart'; @@ -25,204 +25,206 @@ abstract class AppRouter { static GoRouter instance = GoRouter( routes: [ ShellRoute( - builder: (context, state, child) => NavShell( + builder: (context, state, child) => RootShell( state: state, - showAppBar: false, - showSidebar: false, child: child, ), routes: [ - ShellRoute( - builder: (context, state, child) => BasicShell( - state: state, - sidebarFirst: true, - showAppBar: false, - sidebar: const SocialScreen(), - child: child, - ), - routes: [ - GoRoute( - path: '/', - name: 'social', - builder: (context, state) => - SolianTheme.isExtraLargeScreen(context) - ? const EmptyPagePlaceholder() - : const SocialScreen(), - ), - GoRoute( - path: '/posts/view/:alias', - name: 'postDetail', - builder: (context, state) => TitleShell( - state: state, - child: PostDetailScreen( - alias: state.pathParameters['alias']!, - ), - ), - ), - GoRoute( - path: '/posts/publish', - name: 'postPublishing', - builder: (context, state) { - final arguments = state.extra as PostPublishingArguments?; - return PostPublishingScreen( - edit: arguments?.edit, - reply: arguments?.reply, - repost: arguments?.repost, - realm: arguments?.realm, - ); - }, - ), - ], - ), - ShellRoute( - builder: (context, state, child) => BasicShell( - state: state, - sidebarFirst: true, - showAppBar: false, - sidebar: const ChatScreen(), - child: child, - ), - routes: [ - GoRoute( - path: '/chat', - name: 'chat', - builder: (context, state) => - SolianTheme.isExtraLargeScreen(context) - ? const EmptyPagePlaceholder() - : const ChatScreen(), - ), - GoRoute( - path: '/chat/organize', - name: 'channelOrganizing', - builder: (context, state) { - final arguments = state.extra as ChannelOrganizeArguments?; - return ChannelOrganizeScreen( - edit: arguments?.edit, - realm: arguments?.realm, - ); - }, - ), - GoRoute( - path: '/chat/:alias', - name: 'channelChat', - builder: (context, state) { - return ChannelChatScreen( - alias: state.pathParameters['alias']!, - realm: state.uri.queryParameters['realm'] ?? 'global', - ); - }, - ), - GoRoute( - path: '/chat/:alias/detail', - name: 'channelDetail', - builder: (context, state) { - final arguments = state.extra as ChannelDetailArguments; - return TitleShell( - state: state, - child: ChannelDetailScreen( - channel: arguments.channel, - profile: arguments.profile, - realm: state.uri.queryParameters['realm'] ?? 'global', - ), - ); - }, - ), - ], - ), - ShellRoute( - builder: (context, state, child) => BasicShell( - state: state, - sidebarFirst: true, - showAppBar: false, - sidebar: const RealmListScreen(), - child: child, - ), - routes: [ - GoRoute( - path: '/realms', - name: 'realms', - builder: (context, state) => - SolianTheme.isExtraLargeScreen(context) - ? const EmptyPagePlaceholder() - : const RealmListScreen(), - ), - GoRoute( - path: '/realms/:alias/detail', - name: 'realmDetail', - builder: (context, state) => TitleShell( - state: state, - child: RealmDetailScreen( - realm: state.extra as Realm, - alias: state.pathParameters['alias']!, - ), - ), - ), - GoRoute( - path: '/realm/organize', - name: 'realmOrganizing', - builder: (context, state) { - final arguments = state.extra as RealmOrganizeArguments?; - return RealmOrganizeScreen( - edit: arguments?.edit, - ); - }, - ), - GoRoute( - path: '/realm/:alias', - name: 'realmView', - builder: (context, state) { - return RealmViewScreen( - alias: state.pathParameters['alias']!, - ); - }, - ), - ], - ), - ShellRoute( - builder: (context, state, child) => BasicShell( - state: state, - sidebarFirst: true, - showAppBar: false, - sidebar: const AccountScreen(), - child: child, - ), - routes: [ - GoRoute( - path: '/account', - name: 'account', - builder: (context, state) => - SolianTheme.isExtraLargeScreen(context) - ? const EmptyPagePlaceholder() - : const AccountScreen(), - ), - GoRoute( - path: '/account/friend', - name: 'accountFriend', - builder: (context, state) => TitleShell( - state: state, - child: const FriendScreen(), - ), - ), - GoRoute( - path: '/account/personalize', - name: 'accountPersonalize', - builder: (context, state) => TitleShell( - state: state, - child: const PersonalizeScreen(), - ), - ), - GoRoute( - path: '/about', - name: 'about', - builder: (context, state) => TitleShell( - state: state, - child: const AboutScreen(), - ), - ), - ], - ), + _feedRoute, + _chatRoute, + _realmRoute, + _accountRoute, ], ), ], ); + + static final ShellRoute _feedRoute = ShellRoute( + builder: (context, state, child) => BasicShell( + state: state, + sidebarFirst: true, + showAppBar: false, + sidebar: const FeedScreen(), + child: child, + ), + routes: [ + GoRoute( + path: '/', + name: 'feed', + builder: (context, state) => SolianTheme.isExtraLargeScreen(context) + ? const EmptyPagePlaceholder() + : const FeedScreen(), + ), + GoRoute( + path: '/posts/view/:alias', + name: 'postDetail', + builder: (context, state) => TitleShell( + state: state, + child: PostDetailScreen( + alias: state.pathParameters['alias']!, + ), + ), + ), + GoRoute( + path: '/posts/publish', + name: 'postPublishing', + builder: (context, state) { + final arguments = state.extra as PostPublishingArguments?; + return PostPublishingScreen( + edit: arguments?.edit, + reply: arguments?.reply, + repost: arguments?.repost, + realm: arguments?.realm, + ); + }, + ), + ], + ); + + static final ShellRoute _chatRoute = ShellRoute( + builder: (context, state, child) => BasicShell( + state: state, + sidebarFirst: true, + showAppBar: false, + sidebar: const ChatScreen(), + child: child, + ), + routes: [ + GoRoute( + path: '/chat', + name: 'chat', + builder: (context, state) => SolianTheme.isExtraLargeScreen(context) + ? const EmptyPagePlaceholder() + : const ChatScreen(), + ), + GoRoute( + path: '/chat/organize', + name: 'channelOrganizing', + builder: (context, state) { + final arguments = state.extra as ChannelOrganizeArguments?; + return ChannelOrganizeScreen( + edit: arguments?.edit, + realm: arguments?.realm, + ); + }, + ), + GoRoute( + path: '/chat/:alias', + name: 'channelChat', + builder: (context, state) { + return ChannelChatScreen( + alias: state.pathParameters['alias']!, + realm: state.uri.queryParameters['realm'] ?? 'global', + ); + }, + ), + GoRoute( + path: '/chat/:alias/detail', + name: 'channelDetail', + builder: (context, state) { + final arguments = state.extra as ChannelDetailArguments; + return TitleShell( + state: state, + child: ChannelDetailScreen( + channel: arguments.channel, + profile: arguments.profile, + realm: state.uri.queryParameters['realm'] ?? 'global', + ), + ); + }, + ), + ], + ); + + static final ShellRoute _realmRoute = ShellRoute( + builder: (context, state, child) => BasicShell( + state: state, + sidebarFirst: true, + showAppBar: false, + sidebar: const RealmListScreen(), + child: child, + ), + routes: [ + GoRoute( + path: '/realms', + name: 'realms', + builder: (context, state) => SolianTheme.isExtraLargeScreen(context) + ? const EmptyPagePlaceholder() + : const RealmListScreen(), + ), + GoRoute( + path: '/realms/:alias/detail', + name: 'realmDetail', + builder: (context, state) => TitleShell( + state: state, + child: RealmDetailScreen( + realm: state.extra as Realm, + alias: state.pathParameters['alias']!, + ), + ), + ), + GoRoute( + path: '/realm/organize', + name: 'realmOrganizing', + builder: (context, state) { + final arguments = state.extra as RealmOrganizeArguments?; + return RealmOrganizeScreen( + edit: arguments?.edit, + ); + }, + ), + GoRoute( + path: '/realm/:alias', + name: 'realmView', + builder: (context, state) { + return RealmViewScreen( + alias: state.pathParameters['alias']!, + ); + }, + ), + ], + ); + + static final ShellRoute _accountRoute = ShellRoute( + builder: (context, state, child) => BasicShell( + state: state, + sidebarFirst: true, + showAppBar: false, + sidebar: const AccountScreen(), + child: child, + ), + routes: [ + GoRoute( + path: '/account', + name: 'account', + builder: (context, state) => SolianTheme.isExtraLargeScreen(context) + ? const EmptyPagePlaceholder() + : const AccountScreen(), + ), + GoRoute( + path: '/account/friend', + name: 'accountFriend', + builder: (context, state) => TitleShell( + state: state, + child: const FriendScreen(), + ), + ), + GoRoute( + path: '/account/personalize', + name: 'accountPersonalize', + builder: (context, state) => TitleShell( + state: state, + child: const PersonalizeScreen(), + ), + ), + GoRoute( + path: '/about', + name: 'about', + builder: (context, state) => TitleShell( + state: state, + child: const AboutScreen(), + ), + ), + ], + ); } diff --git a/lib/screens/auth/signin.dart b/lib/screens/auth/signin.dart index 864430b..8cda733 100644 --- a/lib/screens/auth/signin.dart +++ b/lib/screens/auth/signin.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:protocol_handler/protocol_handler.dart'; import 'package:solian/exts.dart'; import 'package:solian/providers/account.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/services.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SignInPopup extends StatefulWidget { @@ -13,13 +15,13 @@ class SignInPopup extends StatefulWidget { State createState() => _SignInPopupState(); } -class _SignInPopupState extends State { +class _SignInPopupState extends State with ProtocolListener { bool _isBusy = false; final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); - void requestResetPassword(BuildContext context) async { + void requestResetPassword() async { final username = _usernameController.value.text; if (username.isEmpty) { context.showErrorDialog('signinResetPasswordHint'.tr); @@ -49,7 +51,7 @@ class _SignInPopupState extends State { context.showModalDialog('done'.tr, 'signinResetPasswordSent'.tr); } - void performAction(BuildContext context) async { + void performAction() async { final AuthProvider provider = Get.find(); final username = _usernameController.value.text; @@ -96,6 +98,27 @@ class _SignInPopupState extends State { Navigator.pop(context, true); } + @override + void initState() { + protocolHandler.addListener(this); + super.initState(); + } + + @override + void dispose() { + protocolHandler.removeListener(this); + super.dispose(); + } + + @override + void onProtocolUrlReceived(String url) { + final uri = url.replaceFirst('solink://', ''); + if (uri == 'auth?status=done') { + closeInAppWebView(); + performAction(); + } + } + @override Widget build(BuildContext context) { return SizedBox( @@ -144,20 +167,19 @@ class _SignInPopupState extends State { ), onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), - onSubmitted: (_) => performAction(context), + onSubmitted: (_) => performAction(), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( - onPressed: - _isBusy ? null : () => requestResetPassword(context), + onPressed: _isBusy ? null : () => requestResetPassword(), style: TextButton.styleFrom(foregroundColor: Colors.grey), child: Text('forgotPassword'.tr), ), TextButton( - onPressed: _isBusy ? null : () => performAction(context), + onPressed: _isBusy ? null : () => performAction(), child: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/screens/social.dart b/lib/screens/feed.dart similarity index 73% rename from lib/screens/social.dart rename to lib/screens/feed.dart index 511e77d..3a48404 100644 --- a/lib/screens/social.dart +++ b/lib/screens/feed.dart @@ -12,14 +12,14 @@ import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/current_state_action.dart'; import 'package:solian/widgets/posts/post_list.dart'; -class SocialScreen extends StatefulWidget { - const SocialScreen({super.key}); +class FeedScreen extends StatefulWidget { + const FeedScreen({super.key}); @override - State createState() => _SocialScreenState(); + State createState() => _FeedScreenState(); } -class _SocialScreenState extends State { +class _FeedScreenState extends State { final PagingController _pagingController = PagingController(firstPageKey: 0); @@ -52,26 +52,7 @@ class _SocialScreenState extends State { @override Widget build(BuildContext context) { - final AuthProvider auth = Get.find(); - return Scaffold( - floatingActionButton: FutureBuilder( - future: auth.isAuthorized, - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data == true) { - return FloatingActionButton( - child: const Icon(Icons.add), - onPressed: () async { - final value = - await AppRouter.instance.pushNamed('postPublishing'); - if (value != null) { - _pagingController.refresh(); - } - }, - ); - } - return Container(); - }), body: Material( color: Theme.of(context).colorScheme.surface, child: RefreshIndicator( @@ -79,7 +60,7 @@ class _SocialScreenState extends State { child: CustomScrollView( slivers: [ SliverAppBar( - title: AppBarTitle('social'.tr), + title: AppBarTitle('feed'.tr), centerTitle: false, floating: true, titleSpacing: SolianTheme.titleSpacing(context), @@ -87,6 +68,7 @@ class _SocialScreenState extends State { actions: [ const BackgroundStateWidget(), const NotificationButton(), + const FeedCreationButton(), SizedBox( width: SolianTheme.isLargeScreen(context) ? 8 : 16, ), @@ -100,3 +82,26 @@ class _SocialScreenState extends State { ); } } + +class FeedCreationButton extends StatelessWidget { + const FeedCreationButton({super.key}); + + @override + Widget build(BuildContext context) { + final AuthProvider auth = Get.find(); + + return FutureBuilder( + future: auth.isAuthorized, + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data == true) { + return IconButton( + icon: const Icon(Icons.add_circle), + onPressed: () { + AppRouter.instance.pushNamed('postPublishing'); + }, + ); + } + return const SizedBox(); + }); + } +} diff --git a/lib/shells/listener_shell.dart b/lib/shells/listener_shell.dart deleted file mode 100644 index fd6d7de..0000000 --- a/lib/shells/listener_shell.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:protocol_handler/protocol_handler.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ListenerShell extends StatefulWidget { - final Widget child; - - const ListenerShell({super.key, required this.child}); - - @override - State createState() => _ListenerShellState(); -} - -class _ListenerShellState extends State with ProtocolListener { - @override - void initState() { - protocolHandler.addListener(this); - super.initState(); - } - - @override - void dispose() { - protocolHandler.removeListener(this); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } - - @override - void onProtocolUrlReceived(String url) { - final uri = url.replaceFirst('solink://', ''); - if (uri == 'auth?status=done') { - closeInAppWebView(); - } - } -} diff --git a/lib/shells/nav_shell.dart b/lib/shells/nav_shell.dart index 144d18d..f0674aa 100644 --- a/lib/shells/nav_shell.dart +++ b/lib/shells/nav_shell.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:go_router/go_router.dart'; import 'package:solian/router.dart'; import 'package:solian/theme.dart'; import 'package:solian/widgets/navigation/app_navigation.dart'; @@ -14,7 +13,6 @@ class NavShell extends StatelessWidget { final bool showSidebar; final bool showNavigation; final bool? showBottomNavigation; - final GoRouterState state; final Widget child; final bool sidebarFirst; @@ -23,7 +21,6 @@ class NavShell extends StatelessWidget { const NavShell({ super.key, required this.child, - required this.state, this.showAppBar = true, this.showSidebar = true, this.showNavigation = true, @@ -60,7 +57,7 @@ class NavShell extends StatelessWidget { return Scaffold( appBar: showAppBar ? AppBar( - title: Text(state.topRoute?.name?.tr ?? 'page'.tr), + title: Text(routeName ?? 'page'.tr), centerTitle: false, titleSpacing: canPop ? null : 24, elevation: SolianTheme.isLargeScreen(context) ? 1 : 0, diff --git a/lib/shells/root_shell.dart b/lib/shells/root_shell.dart new file mode 100644 index 0000000..f5f9c94 --- /dev/null +++ b/lib/shells/root_shell.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:solian/router.dart'; +import 'package:solian/theme.dart'; +import 'package:solian/widgets/navigation/app_navigation.dart'; +import 'package:solian/widgets/navigation/app_navigation_bottom_bar.dart'; +import 'package:solian/widgets/navigation/app_navigation_rail.dart'; + +class RootShell extends StatelessWidget { + final bool showSidebar; + final bool showNavigation; + final bool? showBottomNavigation; + final GoRouterState state; + final Widget child; + + const RootShell({ + super.key, + required this.state, + required this.child, + this.showSidebar = true, + this.showNavigation = true, + this.showBottomNavigation, + }); + + @override + Widget build(BuildContext context) { + final routeName = AppRouter + .instance.routerDelegate.currentConfiguration.lastOrNull?.route.name; + final showBottom = showBottomNavigation ?? + AppNavigation.destinationPages.contains(routeName); + + return Scaffold( + body: SolianTheme.isLargeScreen(context) + ? Row( + children: [ + if (showNavigation) const AppNavigationRail(), + if (showNavigation) + const VerticalDivider(thickness: 0.3, width: 1), + Expanded(child: child), + ], + ) + : Stack( + children: [ + child, + Positioned( + bottom: 0, + left: 0, + right: 0, + child: const AppNavigationBottomBar() + .animate(target: showBottom ? 0 : 1) + .slideY( + duration: 250.ms, + begin: 0, + end: 1, + curve: Curves.easeInToLinear, + ), + ), + ], + ), + ); + } +} diff --git a/lib/translations.dart b/lib/translations.dart index 3e7196a..5d4fae3 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -10,7 +10,7 @@ class SolianMessages extends Translations { 'next': 'Next', 'reset': 'Reset', 'page': 'Page', - 'social': 'Social', + 'feed': 'Feed', 'chat': 'Chat', 'apply': 'Apply', 'cancel': 'Cancel', @@ -263,7 +263,7 @@ class SolianMessages extends Translations { 'edit': '编辑', 'delete': '删除', 'page': '页面', - 'social': '社交', + 'feed': '资讯', 'chat': '聊天', 'apply': '应用', 'search': '搜索', diff --git a/lib/widgets/navigation/app_navigation.dart b/lib/widgets/navigation/app_navigation.dart index 7c2f955..31c5cfd 100644 --- a/lib/widgets/navigation/app_navigation.dart +++ b/lib/widgets/navigation/app_navigation.dart @@ -4,9 +4,9 @@ import 'package:get/utils.dart'; abstract class AppNavigation { static List destinations = [ AppNavigationDestination( - icon: const Icon(Icons.public), - label: 'social'.tr, - page: 'social', + icon: const Icon(Icons.feed), + label: 'feed'.tr, + page: 'feed', ), AppNavigationDestination( icon: const Icon(Icons.forum),