From f73cf10a54666074d7360e9cc95c1a0ea7b293bf Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 21 Jun 2025 21:11:46 +0800 Subject: [PATCH] :sparkles: Blurry tab background :recycle: Refactored tabs --- lib/main.dart | 19 +--- lib/route.dart | 73 ++++++++------- lib/route.gr.dart | 58 ++---------- lib/screens/account.dart | 1 + lib/screens/auth/tabs.dart | 161 ---------------------------------- lib/screens/chat/chat.dart | 3 + lib/screens/explore.dart | 6 +- lib/screens/realm/realms.dart | 3 + lib/screens/tabs.dart | 137 +++++++++++++++++++++++++++++ lib/widgets/app_scaffold.dart | 10 ++- 10 files changed, 208 insertions(+), 263 deletions(-) delete mode 100644 lib/screens/auth/tabs.dart create mode 100644 lib/screens/tabs.dart diff --git a/lib/main.dart b/lib/main.dart index 29470a3..26c645c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,7 +18,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/pods/websocket.dart'; import 'package:island/route.dart'; -import 'package:island/screens/auth/tabs.dart'; import 'package:island/services/notify.dart'; import 'package:island/services/timezone.dart'; import 'package:island/widgets/alert.dart'; @@ -164,22 +163,13 @@ class IslandApp extends HookConsumerWidget { theme: theme?.light, darkTheme: theme?.dark, themeMode: ThemeMode.system, - routerConfig: appRouter.config( - navigatorObservers: - () => [ - TabNavigationObserver( - onChange: (route) { - ref.read(currentRouteProvider.notifier).state = route; - }, - ), - ], - ), + routerConfig: appRouter.config(), supportedLocales: context.supportedLocales, localizationsDelegates: [ ...context.localizationDelegates, CroppyLocalizations.delegate, RelativeTimeLocalizations.delegate, - ], // this contains the cupertino one + ], locale: context.locale, builder: (context, child) { return Overlay( @@ -188,10 +178,7 @@ class IslandApp extends HookConsumerWidget { builder: (_) => WindowScaffold( router: appRouter, - child: TabsNavigationWidget( - router: appRouter, - child: child ?? const SizedBox.shrink(), - ), + child: child ?? const SizedBox.shrink(), ), ), ], diff --git a/lib/route.dart b/lib/route.dart index f8a840b..9b592d7 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -8,44 +8,53 @@ class AppRouter extends RootStackRouter { @override List get routes => [ - AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), - AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute( - page: ExploreShellRoute.page, + page: TabsRoute.page, path: '/', children: [ - AutoRoute(page: ExploreRoute.page, path: ''), - AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), - AutoRoute(page: PublisherProfileRoute.page, path: 'publishers/:name'), - ], - ), - AutoRoute( - page: AccountShellRoute.page, - path: '/account', - children: [ - AutoRoute(page: AccountRoute.page, path: ''), - AutoRoute(page: NotificationRoute.page, path: 'notifications'), - AutoRoute(page: WalletRoute.page, path: 'wallet'), - AutoRoute(page: RelationshipRoute.page, path: 'relationships'), - AutoRoute(page: AccountProfileRoute.page, path: ':name'), - AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'), - AutoRoute(page: AccountSettingsRoute.page, path: 'settings'), + AutoRoute( + page: ExploreShellRoute.page, + path: '', + children: [ + AutoRoute(page: ExploreRoute.page, path: ''), + AutoRoute(page: PostDetailRoute.page, path: 'posts/:id'), + AutoRoute( + page: PublisherProfileRoute.page, + path: 'publishers/:name', + ), + ], + ), + AutoRoute( + page: AccountShellRoute.page, + path: 'account', + children: [ + AutoRoute(page: AccountRoute.page, path: ''), + AutoRoute(page: NotificationRoute.page, path: 'notifications'), + AutoRoute(page: WalletRoute.page, path: 'wallet'), + AutoRoute(page: RelationshipRoute.page, path: 'relationships'), + AutoRoute(page: AccountProfileRoute.page, path: ':name'), + AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'), + AutoRoute(page: AccountSettingsRoute.page, path: 'settings'), + ], + ), + AutoRoute(page: RealmListRoute.page, path: 'realms'), + AutoRoute( + page: ChatShellRoute.page, + path: 'chat', + children: [ + AutoRoute(page: ChatListRoute.page, path: ''), + AutoRoute(page: ChatRoomRoute.page, path: ':id'), + AutoRoute(page: CallRoute.page, path: ':id/call'), + AutoRoute(page: NewChatRoute.page, path: 'new'), + AutoRoute(page: EditChatRoute.page, path: ':id/edit'), + AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), + ], + ), ], ), + AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), + AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute(page: EventCalanderRoute.page, path: '/account/:name/calendar'), - AutoRoute(page: RealmListRoute.page, path: '/realms'), - AutoRoute( - page: ChatShellRoute.page, - path: '/chat', - children: [ - AutoRoute(page: ChatListRoute.page, path: ''), - AutoRoute(page: ChatRoomRoute.page, path: ':id'), - AutoRoute(page: CallRoute.page, path: ':id/call'), - AutoRoute(page: NewChatRoute.page, path: 'new'), - AutoRoute(page: EditChatRoute.page, path: ':id/edit'), - AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), - ], - ), AutoRoute( page: CreatorHubShellRoute.page, path: '/creators', diff --git a/lib/route.gr.dart b/lib/route.gr.dart index 9b877dc..bde5b1f 100644 --- a/lib/route.gr.dart +++ b/lib/route.gr.dart @@ -13,7 +13,6 @@ import 'package:auto_route/auto_route.dart' as _i29; import 'package:flutter/foundation.dart' as _i31; import 'package:flutter/material.dart' as _i30; import 'package:island/models/post.dart' as _i32; -import 'package:island/route.dart' as _i33; import 'package:island/screens/account.dart' as _i2; import 'package:island/screens/account/event_calendar.dart' as _i16; import 'package:island/screens/account/me/settings.dart' as _i3; @@ -22,7 +21,6 @@ import 'package:island/screens/account/profile.dart' as _i1; import 'package:island/screens/account/relationship.dart' as _i24; import 'package:island/screens/auth/create_account.dart' as _i9; import 'package:island/screens/auth/login.dart' as _i18; -import 'package:island/screens/auth/tabs.dart' as _i26; import 'package:island/screens/chat/call.dart' as _i5; import 'package:island/screens/chat/chat.dart' as _i7; import 'package:island/screens/chat/room.dart' as _i8; @@ -41,6 +39,7 @@ import 'package:island/screens/posts/pub_profile.dart' as _i22; import 'package:island/screens/realm/detail.dart' as _i23; import 'package:island/screens/realm/realms.dart' as _i13; import 'package:island/screens/settings.dart' as _i25; +import 'package:island/screens/tabs.dart' as _i26; import 'package:island/screens/wallet.dart' as _i28; /// generated route for @@ -1684,64 +1683,21 @@ class StickersRouteArgs { } /// generated route for -/// [_i26.TabsNavigationWidget] -class TabsNavigationWidget - extends _i29.PageRouteInfo { - TabsNavigationWidget({ - _i30.Key? key, - required _i30.Widget child, - required _i33.AppRouter router, - List<_i29.PageRouteInfo>? children, - }) : super( - TabsNavigationWidget.name, - args: TabsNavigationWidgetArgs(key: key, child: child, router: router), - initialChildren: children, - ); +/// [_i26.TabsScreen] +class TabsRoute extends _i29.PageRouteInfo { + const TabsRoute({List<_i29.PageRouteInfo>? children}) + : super(TabsRoute.name, initialChildren: children); - static const String name = 'TabsNavigationWidget'; + static const String name = 'TabsRoute'; static _i29.PageInfo page = _i29.PageInfo( name, builder: (data) { - final args = data.argsAs(); - return _i26.TabsNavigationWidget( - key: args.key, - child: args.child, - router: args.router, - ); + return const _i26.TabsScreen(); }, ); } -class TabsNavigationWidgetArgs { - const TabsNavigationWidgetArgs({ - this.key, - required this.child, - required this.router, - }); - - final _i30.Key? key; - - final _i30.Widget child; - - final _i33.AppRouter router; - - @override - String toString() { - return 'TabsNavigationWidgetArgs{key: $key, child: $child, router: $router}'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other is! TabsNavigationWidgetArgs) return false; - return key == other.key && child == other.child && router == other.router; - } - - @override - int get hashCode => key.hashCode ^ child.hashCode ^ router.hashCode; -} - /// generated route for /// [_i27.UpdateProfileScreen] class UpdateProfileRoute extends _i29.PageRouteInfo { diff --git a/lib/screens/account.dart b/lib/screens/account.dart index cd203a4..422fcd0 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -66,6 +66,7 @@ class AccountScreen extends HookConsumerWidget { } return AppScaffold( + extendBody: false, // Prevent conflicts with tabs navigation noBackground: isWide, appBar: AppBar(title: const Text('account').tr()), body: SingleChildScrollView( diff --git a/lib/screens/auth/tabs.dart b/lib/screens/auth/tabs.dart deleted file mode 100644 index 352c9a7..0000000 --- a/lib/screens/auth/tabs.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'dart:developer'; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:island/route.dart'; -import 'package:island/route.gr.dart'; -import 'package:island/screens/notification.dart'; -import 'package:island/services/responsive.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -final currentRouteProvider = StateProvider((ref) => null); - -class TabNavigationObserver extends AutoRouterObserver { - Function(String?) onChange; - TabNavigationObserver({required this.onChange}); - - @override - void didPush(Route route, Route? previousRoute) { - log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}'); - if (route is DialogRoute) return; - Future(() { - onChange(route.settings.name); - }); - } - - @override - void didPop(Route route, Route? previousRoute) { - log('popped ${route.settings.name} -> ${previousRoute?.settings.name}'); - if (route is DialogRoute) return; - Future(() { - onChange(previousRoute?.settings.name); - }); - } -} - -@RoutePage() -class TabsNavigationWidget extends HookConsumerWidget { - final Widget child; - final AppRouter router; - const TabsNavigationWidget({ - super.key, - required this.child, - required this.router, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final useHorizontalLayout = isWideScreen(context); - final currentRoute = ref.watch(currentRouteProvider); - - final notificationUnreadCount = ref.watch( - notificationUnreadCountNotifierProvider, - ); - - int activeIndex = 0; - - final destinations = [ - NavigationDestination( - label: 'explore'.tr(), - icon: const Icon(Symbols.explore), - ), - NavigationDestination(label: 'chat'.tr(), icon: const Icon(Symbols.chat)), - NavigationDestination( - label: 'realms'.tr(), - icon: const Icon(Symbols.workspaces), - ), - NavigationDestination( - label: 'account'.tr(), - icon: Badge.count( - count: notificationUnreadCount.value ?? 0, - isLabelVisible: (notificationUnreadCount.value ?? 0) > 0, - child: const Icon(Symbols.account_circle), - ), - ), - ]; - - final routes = [ - ExploreRoute(), - ChatListRoute(), - RealmListRoute(), - AccountRoute(), - ]; - final routeNames = [ - ExploreRoute.name, - ExploreShellRoute.name, - ChatListRoute.name, - RealmListRoute.name, - AccountRoute.name, - ChatShellRoute.name, - AccountShellRoute.name, - ]; - - activeIndex = routes.indexWhere((route) => route.routeName == currentRoute); - if (activeIndex == -1) { - activeIndex = 0; - } - - final isTabRoute = routeNames.any((route) { - return route == currentRoute; - }); - - return Scaffold( - extendBodyBehindAppBar: true, - backgroundColor: Colors.transparent, - body: - useHorizontalLayout - ? Row( - children: [ - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: Column( - children: [ - Gap(MediaQuery.of(context).padding.top + 8), - Expanded( - child: NavigationRail( - selectedIndex: activeIndex, - onDestinationSelected: (index) { - router.replace(routes[index]); - }, - // labelType: NavigationRailLabelType.all, - destinations: - destinations - .map( - (d) => NavigationRailDestination( - icon: d.icon, - label: Text(d.label), - ), - ) - .toList(), - ), - ), - Gap(MediaQuery.of(context).padding.bottom + 8), - ], - ), - ), - VerticalDivider( - color: Theme.of(context).dividerColor, - width: 1 / MediaQuery.of(context).devicePixelRatio, - ), - Expanded(child: child), - ], - ) - : child, - bottomNavigationBar: - !useHorizontalLayout && isTabRoute - ? NavigationBar( - height: 56, - labelBehavior: NavigationDestinationLabelBehavior.alwaysHide, - selectedIndex: activeIndex, - onDestinationSelected: (index) { - router.replace(routes[index]); - }, - destinations: destinations, - ) - : null, - ); - } -} diff --git a/lib/screens/chat/chat.dart b/lib/screens/chat/chat.dart index e4eb856..31df158 100644 --- a/lib/screens/chat/chat.dart +++ b/lib/screens/chat/chat.dart @@ -27,6 +27,7 @@ import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/realms/selection_dropdown.dart'; import 'package:island/widgets/response.dart'; +import 'package:island/screens/tabs.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:relative_time/relative_time.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -241,6 +242,7 @@ class ChatListScreen extends HookConsumerWidget { } return AppScaffold( + extendBody: false, // Prevent conflicts with tabs navigation appBar: AppBar( title: Text('chat').tr(), bottom: TabBar( @@ -339,6 +341,7 @@ class ChatListScreen extends HookConsumerWidget { }, child: const Icon(Symbols.add), ), + floatingActionButtonLocation: TabbedFabLocation(context), body: Stack( children: [ Column( diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index bf4e808..f68288b 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -12,6 +12,7 @@ import 'package:island/models/post.dart'; import 'package:island/widgets/check_in.dart'; import 'package:island/widgets/post/post_item.dart'; import 'package:island/widgets/tour/tour.dart'; +import 'package:island/screens/tabs.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_paging_utils/riverpod_paging_utils.dart'; @@ -45,6 +46,8 @@ class ExploreShellScreen extends ConsumerWidget { } } + + @RoutePage() class ExploreScreen extends ConsumerWidget { final bool isAside; @@ -61,6 +64,7 @@ class ExploreScreen extends ConsumerWidget { return TourTriggerWidget( child: AppScaffold( + extendBody: false, // Prevent conflicts with tabs navigation appBar: AppBar(title: const Text('explore').tr()), floatingActionButton: FloatingActionButton( heroTag: Key("explore-page-fab"), @@ -73,7 +77,7 @@ class ExploreScreen extends ConsumerWidget { }, child: const Icon(Symbols.edit), ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + floatingActionButtonLocation: TabbedFabLocation(context), body: RefreshIndicator( onRefresh: () => Future.sync(activitiesNotifier.forceRefresh), child: PagingHelperView( diff --git a/lib/screens/realm/realms.dart b/lib/screens/realm/realms.dart index fc55a6c..209843c 100644 --- a/lib/screens/realm/realms.dart +++ b/lib/screens/realm/realms.dart @@ -18,6 +18,7 @@ import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/sheet.dart'; import 'package:island/widgets/response.dart'; +import 'package:island/screens/tabs.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -41,6 +42,7 @@ class RealmListScreen extends HookConsumerWidget { final realmInvites = ref.watch(realmInvitesProvider); return AppScaffold( + extendBody: false, // Prevent conflicts with tabs navigation noBackground: false, appBar: AppBar( title: const Text('realms').tr(), @@ -83,6 +85,7 @@ class RealmListScreen extends HookConsumerWidget { }); }, ), + floatingActionButtonLocation: TabbedFabLocation(context), body: RefreshIndicator( child: realms.when( data: diff --git a/lib/screens/tabs.dart b/lib/screens/tabs.dart new file mode 100644 index 0000000..dac767c --- /dev/null +++ b/lib/screens/tabs.dart @@ -0,0 +1,137 @@ +import 'dart:ui'; +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/route.gr.dart'; +import 'package:island/screens/notification.dart'; +import 'package:island/services/responsive.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +@RoutePage() +class TabsScreen extends HookConsumerWidget { + const TabsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final useHorizontalLayout = isWideScreen(context); + + final notificationUnreadCount = ref.watch( + notificationUnreadCountNotifierProvider, + ); + + final destinations = [ + NavigationDestination( + label: 'explore'.tr(), + icon: const Icon(Symbols.explore), + ), + NavigationDestination(label: 'chat'.tr(), icon: const Icon(Symbols.chat)), + NavigationDestination( + label: 'realms'.tr(), + icon: const Icon(Symbols.workspaces), + ), + NavigationDestination( + label: 'account'.tr(), + icon: Badge.count( + count: notificationUnreadCount.value ?? 0, + isLabelVisible: (notificationUnreadCount.value ?? 0) > 0, + child: const Icon(Symbols.account_circle), + ), + ), + ]; + + final routes = [ + ExploreRoute(), + ChatListRoute(), + RealmListRoute(), + AccountRoute(), + ]; + + return AutoTabsRouter.tabBar( + routes: routes, + scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + builder: (context, child, _) { + final tabsRouter = AutoTabsRouter.of(context); + + // Check if current route is a tab route + final currentRoute = context.router.topRoute; + final isTabRoute = routes.any( + (route) => route.routeName == currentRoute.name, + ); + + return Stack( + children: [ + Positioned.fill(child: child), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surface.withOpacity(0.8), + ), + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: NavigationBar( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + overlayColor: WidgetStatePropertyAll( + Colors.transparent, + ), + surfaceTintColor: Colors.transparent, + height: 56, + labelBehavior: + NavigationDestinationLabelBehavior.alwaysHide, + selectedIndex: tabsRouter.activeIndex, + onDestinationSelected: tabsRouter.setActiveIndex, + destinations: destinations, + ), + ), + ), + ), + ), + ), + ], + ); + }, + ); + } +} + +class TabbedFabLocation extends FloatingActionButtonLocation { + final BuildContext context; + + const TabbedFabLocation(this.context); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final mediaQuery = MediaQuery.of(context); + final safeAreaPadding = mediaQuery.padding; + + // Calculate position with proper safe area considerations + final double fabX = + scaffoldGeometry.scaffoldSize.width - + scaffoldGeometry.floatingActionButtonSize.width - + 16.0 - + safeAreaPadding.right; + + // Use safe area bottom padding + navigation bar height (typically 80px) + final double fabY = + scaffoldGeometry.scaffoldSize.height - + scaffoldGeometry.floatingActionButtonSize.height - + scaffoldGeometry.bottomSheetSize.height - + safeAreaPadding.bottom - + 80.0 + + 16; + + return Offset(fabX, fabY); + } +} diff --git a/lib/widgets/app_scaffold.dart b/lib/widgets/app_scaffold.dart index 3329b9a..685830d 100644 --- a/lib/widgets/app_scaffold.dart +++ b/lib/widgets/app_scaffold.dart @@ -92,7 +92,11 @@ class WindowScaffold extends HookConsumerWidget { return Stack( fit: StackFit.expand, - children: [child, _WebSocketIndicator(), AppNotificationToast()], + children: [ + Positioned.fill(child: child), + _WebSocketIndicator(), + AppNotificationToast(), + ], ); } } @@ -112,6 +116,7 @@ class AppScaffold extends StatelessWidget { final DrawerCallback? onDrawerChanged; final DrawerCallback? onEndDrawerChanged; final bool? noBackground; + final bool? extendBody; const AppScaffold({ super.key, @@ -127,6 +132,7 @@ class AppScaffold extends StatelessWidget { this.onDrawerChanged, this.onEndDrawerChanged, this.noBackground, + this.extendBody, }); @override @@ -146,7 +152,7 @@ class AppScaffold extends StatelessWidget { ); return Scaffold( - extendBody: true, + extendBody: extendBody ?? true, extendBodyBehindAppBar: true, backgroundColor: noBackground