Blurry tab background

♻️ Refactored tabs
This commit is contained in:
LittleSheep 2025-06-21 21:11:46 +08:00
parent b39248cc58
commit f73cf10a54
10 changed files with 208 additions and 263 deletions

View File

@ -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(),
),
),
],

View File

@ -8,44 +8,53 @@ class AppRouter extends RootStackRouter {
@override
List<AutoRoute> 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',

View File

@ -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<TabsNavigationWidgetArgs> {
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<void> {
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<TabsNavigationWidgetArgs>();
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<void> {

View File

@ -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(

View File

@ -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<String?>((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 = <PageRouteInfo>[
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,
);
}
}

View File

@ -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(

View File

@ -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(

View File

@ -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:

137
lib/screens/tabs.dart Normal file
View File

@ -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 = <PageRouteInfo>[
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);
}
}

View File

@ -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