♻️ New tab navigation

This commit is contained in:
LittleSheep 2025-05-19 00:55:51 +08:00
parent 911439f3af
commit b918986fc5
6 changed files with 305 additions and 139 deletions

View File

@ -17,6 +17,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart'; import 'package:island/pods/websocket.dart';
import 'package:island/route.dart'; import 'package:island/route.dart';
import 'package:island/screens/auth/tabs.dart';
import 'package:island/services/notify.dart'; import 'package:island/services/notify.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
@ -102,7 +103,16 @@ class IslandApp extends HookConsumerWidget {
theme: theme?.light, theme: theme?.light,
darkTheme: theme?.dark, darkTheme: theme?.dark,
themeMode: ThemeMode.system, themeMode: ThemeMode.system,
routerConfig: _appRouter.config(), routerConfig: _appRouter.config(
navigatorObservers:
() => [
TabNavigationObserver(
onChange: (route) {
ref.read(currentRouteProvider.notifier).state = route;
},
),
],
),
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
localizationsDelegates: [ localizationsDelegates: [
...context.localizationDelegates, ...context.localizationDelegates,
@ -117,7 +127,10 @@ class IslandApp extends HookConsumerWidget {
builder: builder:
(_) => WindowScaffold( (_) => WindowScaffold(
router: _appRouter, router: _appRouter,
child: child ?? const SizedBox.shrink(), child: TabsNavigationWidget(
router: _appRouter,
child: child ?? const SizedBox.shrink(),
),
), ),
), ),
], ],

View File

@ -8,49 +8,39 @@ class AppRouter extends RootStackRouter {
@override @override
List<AutoRoute> get routes => [ List<AutoRoute> get routes => [
RedirectRoute(path: '/', redirectTo: '/explore'),
AutoRoute(page: ExploreRoute.page, path: '/explore'),
AutoRoute( AutoRoute(
page: TabsRoute.page, page: AccountShellRoute.page,
path: '/', path: '/account',
initial: true,
children: [ children: [
AutoRoute(page: ExploreRoute.page, path: 'explore'), AutoRoute(page: WalletRoute.page, path: 'wallet'),
AutoRoute(page: AccountRoute.page, path: 'account'), AutoRoute(page: RelationshipRoute.page, path: 'relationships'),
AutoRoute(page: RealmListRoute.page, path: 'realms'), AutoRoute(page: AccountRoute.page, path: ''),
AutoRoute( AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
page: ChatShellRoute.page, AutoRoute(page: AccountProfileRoute.page, path: ':name'),
path: 'chat', AutoRoute(page: PublisherProfileRoute.page, path: ':name/calendar'),
children: [ AutoRoute(page: MyselfEventCalendarRoute.page, path: 'me/calendar'),
AutoRoute(page: ChatListRoute.page, path: ''), AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
AutoRoute(page: ChatRoomRoute.page, path: ':id'), AutoRoute(page: ManagedPublisherRoute.page, path: 'me/publishers'),
AutoRoute(page: NewChatRoute.page, path: 'new'), ],
AutoRoute(page: EditChatRoute.page, path: ':id/edit'), ),
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'), 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: NewChatRoute.page, path: 'new'),
AutoRoute(page: EditChatRoute.page, path: ':id/edit'),
AutoRoute(page: ChatDetailRoute.page, path: ':id/detail'),
], ],
), ),
AutoRoute(page: WalletRoute.page, path: '/wallet'),
AutoRoute(page: RelationshipRoute.page, path: '/relationships'),
AutoRoute(page: SettingsRoute.page, path: '/settings'), AutoRoute(page: SettingsRoute.page, path: '/settings'),
AutoRoute(page: LoginRoute.page, path: '/auth/login'), AutoRoute(page: LoginRoute.page, path: '/auth/login'),
AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'), AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
AutoRoute(page: AccountSettingsRoute.page, path: '/account/settings'), AutoRoute(page: AccountSettingsRoute.page, path: '/account/settings'),
AutoRoute(
page: MyselfEventCalendarRoute.page,
path: '/account/me/calendar',
),
AutoRoute(page: UpdateProfileRoute.page, path: '/account/me/update'),
AutoRoute(page: ManagedPublisherRoute.page, path: '/account/me/publishers'),
AutoRoute(page: NewPublisherRoute.page, path: '/account/me/publishers/new'),
AutoRoute(
page: EditPublisherRoute.page,
path: '/account/me/publishers/:id/edit',
),
AutoRoute(page: AccountProfileRoute.page, path: '/account/:name'),
AutoRoute(
page: PublisherProfileRoute.page,
path: '/account/:name/calendar',
),
AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'), AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'),
AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'), AutoRoute(page: PostDetailRoute.page, path: '/posts/:id'),
AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'), AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),

View File

@ -10,8 +10,10 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i25; import 'package:auto_route/auto_route.dart' as _i25;
import 'package:flutter/foundation.dart' as _i27;
import 'package:flutter/material.dart' as _i26; import 'package:flutter/material.dart' as _i26;
import 'package:island/models/post.dart' as _i27; import 'package:island/models/post.dart' as _i28;
import 'package:island/route.dart' as _i29;
import 'package:island/screens/account.dart' as _i2; import 'package:island/screens/account.dart' as _i2;
import 'package:island/screens/account/me/event_calendar.dart' as _i15; import 'package:island/screens/account/me/event_calendar.dart' as _i15;
import 'package:island/screens/account/me/publishers.dart' as _i9; import 'package:island/screens/account/me/publishers.dart' as _i9;
@ -81,20 +83,43 @@ class AccountProfileRouteArgs {
/// generated route for /// generated route for
/// [_i2.AccountScreen] /// [_i2.AccountScreen]
class AccountRoute extends _i25.PageRouteInfo<void> { class AccountRoute extends _i25.PageRouteInfo<AccountRouteArgs> {
const AccountRoute({List<_i25.PageRouteInfo>? children}) AccountRoute({
: super(AccountRoute.name, initialChildren: children); _i27.Key? key,
bool isAside = false,
List<_i25.PageRouteInfo>? children,
}) : super(
AccountRoute.name,
args: AccountRouteArgs(key: key, isAside: isAside),
initialChildren: children,
);
static const String name = 'AccountRoute'; static const String name = 'AccountRoute';
static _i25.PageInfo page = _i25.PageInfo( static _i25.PageInfo page = _i25.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i2.AccountScreen(); final args = data.argsAs<AccountRouteArgs>(
orElse: () => const AccountRouteArgs(),
);
return _i2.AccountScreen(key: args.key, isAside: args.isAside);
}, },
); );
} }
class AccountRouteArgs {
const AccountRouteArgs({this.key, this.isAside = false});
final _i27.Key? key;
final bool isAside;
@override
String toString() {
return 'AccountRouteArgs{key: $key, isAside: $isAside}';
}
}
/// generated route for /// generated route for
/// [_i3.AccountSettingsScreen] /// [_i3.AccountSettingsScreen]
class AccountSettingsRoute extends _i25.PageRouteInfo<void> { class AccountSettingsRoute extends _i25.PageRouteInfo<void> {
@ -111,6 +136,22 @@ class AccountSettingsRoute extends _i25.PageRouteInfo<void> {
); );
} }
/// generated route for
/// [_i2.AccountShellScreen]
class AccountShellRoute extends _i25.PageRouteInfo<void> {
const AccountShellRoute({List<_i25.PageRouteInfo>? children})
: super(AccountShellRoute.name, initialChildren: children);
static const String name = 'AccountShellRoute';
static _i25.PageInfo page = _i25.PageInfo(
name,
builder: (data) {
return const _i2.AccountShellScreen();
},
);
}
/// generated route for /// generated route for
/// [_i4.ChatDetailScreen] /// [_i4.ChatDetailScreen]
class ChatDetailRoute extends _i25.PageRouteInfo<ChatDetailRouteArgs> { class ChatDetailRoute extends _i25.PageRouteInfo<ChatDetailRouteArgs> {
@ -154,20 +195,43 @@ class ChatDetailRouteArgs {
/// generated route for /// generated route for
/// [_i5.ChatListScreen] /// [_i5.ChatListScreen]
class ChatListRoute extends _i25.PageRouteInfo<void> { class ChatListRoute extends _i25.PageRouteInfo<ChatListRouteArgs> {
const ChatListRoute({List<_i25.PageRouteInfo>? children}) ChatListRoute({
: super(ChatListRoute.name, initialChildren: children); _i26.Key? key,
bool isAside = false,
List<_i25.PageRouteInfo>? children,
}) : super(
ChatListRoute.name,
args: ChatListRouteArgs(key: key, isAside: isAside),
initialChildren: children,
);
static const String name = 'ChatListRoute'; static const String name = 'ChatListRoute';
static _i25.PageInfo page = _i25.PageInfo( static _i25.PageInfo page = _i25.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i5.ChatListScreen(); final args = data.argsAs<ChatListRouteArgs>(
orElse: () => const ChatListRouteArgs(),
);
return _i5.ChatListScreen(key: args.key, isAside: args.isAside);
}, },
); );
} }
class ChatListRouteArgs {
const ChatListRouteArgs({this.key, this.isAside = false});
final _i26.Key? key;
final bool isAside;
@override
String toString() {
return 'ChatListRouteArgs{key: $key, isAside: $isAside}';
}
}
/// generated route for /// generated route for
/// [_i6.ChatRoomScreen] /// [_i6.ChatRoomScreen]
class ChatRoomRoute extends _i25.PageRouteInfo<ChatRoomRouteArgs> { class ChatRoomRoute extends _i25.PageRouteInfo<ChatRoomRouteArgs> {
@ -697,7 +761,7 @@ class NewStickersRouteArgs {
class PostComposeRoute extends _i25.PageRouteInfo<PostComposeRouteArgs> { class PostComposeRoute extends _i25.PageRouteInfo<PostComposeRouteArgs> {
PostComposeRoute({ PostComposeRoute({
_i26.Key? key, _i26.Key? key,
_i27.SnPost? originalPost, _i28.SnPost? originalPost,
List<_i25.PageRouteInfo>? children, List<_i25.PageRouteInfo>? children,
}) : super( }) : super(
PostComposeRoute.name, PostComposeRoute.name,
@ -726,7 +790,7 @@ class PostComposeRouteArgs {
final _i26.Key? key; final _i26.Key? key;
final _i27.SnPost? originalPost; final _i28.SnPost? originalPost;
@override @override
String toString() { String toString() {
@ -1047,21 +1111,54 @@ class StickersRouteArgs {
} }
/// generated route for /// generated route for
/// [_i22.TabsScreen] /// [_i22.TabsNavigationWidget]
class TabsRoute extends _i25.PageRouteInfo<void> { class TabsNavigationWidget
const TabsRoute({List<_i25.PageRouteInfo>? children}) extends _i25.PageRouteInfo<TabsNavigationWidgetArgs> {
: super(TabsRoute.name, initialChildren: children); TabsNavigationWidget({
_i26.Key? key,
required _i26.Widget child,
required _i29.AppRouter router,
List<_i25.PageRouteInfo>? children,
}) : super(
TabsNavigationWidget.name,
args: TabsNavigationWidgetArgs(key: key, child: child, router: router),
initialChildren: children,
);
static const String name = 'TabsRoute'; static const String name = 'TabsNavigationWidget';
static _i25.PageInfo page = _i25.PageInfo( static _i25.PageInfo page = _i25.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i22.TabsScreen(); final args = data.argsAs<TabsNavigationWidgetArgs>();
return _i22.TabsNavigationWidget(
key: args.key,
child: args.child,
router: args.router,
);
}, },
); );
} }
class TabsNavigationWidgetArgs {
const TabsNavigationWidgetArgs({
this.key,
required this.child,
required this.router,
});
final _i26.Key? key;
final _i26.Widget child;
final _i29.AppRouter router;
@override
String toString() {
return 'TabsNavigationWidgetArgs{key: $key, child: $child, router: $router}';
}
}
/// generated route for /// generated route for
/// [_i23.UpdateProfileScreen] /// [_i23.UpdateProfileScreen]
class UpdateProfileRoute extends _i25.PageRouteInfo<void> { class UpdateProfileRoute extends _i25.PageRouteInfo<void> {

View File

@ -9,6 +9,7 @@ import 'package:island/pods/message.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart'; import 'package:island/pods/userinfo.dart';
import 'package:island/route.gr.dart'; import 'package:island/route.gr.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
import 'package:island/widgets/account/leveling_progress.dart'; import 'package:island/widgets/account/leveling_progress.dart';
import 'package:island/widgets/app_scaffold.dart'; import 'package:island/widgets/app_scaffold.dart';
@ -17,11 +18,39 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@RoutePage() @RoutePage()
class AccountScreen extends HookConsumerWidget { class AccountShellScreen extends HookConsumerWidget {
const AccountScreen({super.key}); const AccountShellScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
if (isWide) {
return Row(
children: [
SizedBox(width: 360, child: AccountScreen(isAside: true)),
VerticalDivider(width: 1),
Expanded(child: AutoRouter()),
],
);
}
return AutoRouter();
}
}
@RoutePage()
class AccountScreen extends HookConsumerWidget {
final bool isAside;
const AccountScreen({super.key, this.isAside = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return Container(color: Theme.of(context).scaffoldBackgroundColor);
}
final user = ref.watch(userInfoProvider); final user = ref.watch(userInfoProvider);
if (!user.hasValue || user.value == null) { if (!user.hasValue || user.value == null) {

View File

@ -2,18 +2,53 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gap/gap.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/route.gr.dart';
import 'package:island/services/responsive.dart'; import 'package:island/services/responsive.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
@RoutePage() final currentRouteProvider = StateProvider<String?>((ref) => null);
class TabsScreen extends StatelessWidget {
const TabsScreen({super.key}); class TabNavigationObserver extends AutoRouterObserver {
Function(String?) onChange;
TabNavigationObserver({required this.onChange});
@override @override
Widget build(BuildContext context) { void didPush(Route route, Route? previousRoute) {
Future(() {
print('didPush: ${route.settings.name}');
onChange(route.settings.name);
});
}
@override
void didPop(Route route, Route? previousRoute) {
Future(() {
print('didPop: ${previousRoute?.settings.name}');
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 useHorizontalLayout = isWideScreen(context);
final useExpandableLayout = isWidestScreen(context); final useExpandableLayout = isWidestScreen(context);
final currentRoute = ref.watch(currentRouteProvider);
print('currentRoute: $currentRoute');
int activeIndex = 0;
final destinations = [ final destinations = [
NavigationDestination( NavigationDestination(
@ -31,90 +66,93 @@ class TabsScreen extends StatelessWidget {
), ),
]; ];
final routes = [ final routes = <PageRouteInfo>[
ExploreRoute(), ExploreRoute(),
ChatListRoute(), ChatListRoute(),
RealmListRoute(), RealmListRoute(),
AccountRoute(), AccountRoute(),
]; ];
final routeNames = [
ExploreRoute.name,
ChatListRoute.name,
RealmListRoute.name,
AccountRoute.name,
ChatShellRoute.name,
AccountShellRoute.name,
];
return AutoTabsRouter.tabBar( activeIndex = routes.indexWhere((route) => route.routeName == currentRoute);
routes: routes, if (activeIndex == -1) {
scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal, activeIndex = 0;
physics: const NeverScrollableScrollPhysics(), }
builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context);
// Check if current route is a tab route final isTabRoute = routeNames.any((route) {
final currentRoute = context.router.topRoute; return route == currentRoute;
final isTabRoute = routes.any( });
(route) => route.routeName == currentRoute.name,
);
return Scaffold( return Scaffold(
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: body:
useHorizontalLayout useHorizontalLayout
? Row( ? Row(
children: [
Column(
children: [ children: [
if (isTabRoute) Gap(MediaQuery.of(context).padding.top + 8),
Column( if (useExpandableLayout)
children: [ Expanded(
Gap(MediaQuery.of(context).padding.top + 8), child: NavigationDrawer(
if (useExpandableLayout) backgroundColor: Colors.transparent,
Expanded( children: [
child: NavigationDrawer( for (final destination in destinations)
backgroundColor: Colors.transparent, NavigationDrawerDestination(
children: [ label: Text(destination.label),
for (final destination in destinations) icon: destination.icon,
NavigationDrawerDestination( ),
label: Text(destination.label), ],
icon: destination.icon, ),
)
else
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(),
) ),
else
Expanded(
child: NavigationRail(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected:
tabsRouter.setActiveIndex,
labelType: NavigationRailLabelType.all,
destinations:
destinations
.map(
(d) => NavigationRailDestination(
icon: d.icon,
label: Text(d.label),
),
)
.toList(),
),
),
Gap(MediaQuery.of(context).padding.bottom + 8),
],
), ),
if (isTabRoute) Gap(MediaQuery.of(context).padding.bottom + 8),
VerticalDivider(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
Expanded(child: child),
], ],
) ),
: child, VerticalDivider(
bottomNavigationBar: color: Theme.of(context).dividerColor,
!useHorizontalLayout && isTabRoute width: 1 / MediaQuery.of(context).devicePixelRatio,
? NavigationBar( ),
selectedIndex: tabsRouter.activeIndex, Expanded(child: child),
onDestinationSelected: tabsRouter.setActiveIndex, ],
destinations: destinations, )
) : child,
: null, bottomNavigationBar:
); !useHorizontalLayout && isTabRoute
}, ? NavigationBar(
selectedIndex: activeIndex,
onDestinationSelected: (index) {
router.replace(routes[index]);
},
destinations: destinations,
)
: null,
); );
} }
} }

View File

@ -115,6 +115,11 @@ class ChatListScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return Container(color: Theme.of(context).scaffoldBackgroundColor);
}
final chats = ref.watch(chatroomsJoinedProvider); final chats = ref.watch(chatroomsJoinedProvider);
final chatInvites = ref.watch(chatroomInvitesProvider); final chatInvites = ref.watch(chatroomInvitesProvider);
@ -133,12 +138,6 @@ class ChatListScreen extends HookConsumerWidget {
} }
} }
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return SizedBox.shrink();
}
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: Text('chat').tr(), title: Text('chat').tr(),
@ -222,7 +221,7 @@ class ChatListScreen extends HookConsumerWidget {
room: item, room: item,
isDirect: item.type == 1, isDirect: item.type == 1,
onTap: () { onTap: () {
if (isWide) { if (context.router.topRoute.name == ChatRoomRoute.name) {
context.router.replace(ChatRoomRoute(id: item.id)); context.router.replace(ChatRoomRoute(id: item.id));
} else { } else {
context.router.push(ChatRoomRoute(id: item.id)); context.router.push(ChatRoomRoute(id: item.id));