♻️ 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/websocket.dart';
import 'package:island/route.dart';
import 'package:island/screens/auth/tabs.dart';
import 'package:island/services/notify.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:relative_time/relative_time.dart';
@ -102,7 +103,16 @@ class IslandApp extends HookConsumerWidget {
theme: theme?.light,
darkTheme: theme?.dark,
themeMode: ThemeMode.system,
routerConfig: _appRouter.config(),
routerConfig: _appRouter.config(
navigatorObservers:
() => [
TabNavigationObserver(
onChange: (route) {
ref.read(currentRouteProvider.notifier).state = route;
},
),
],
),
supportedLocales: context.supportedLocales,
localizationsDelegates: [
...context.localizationDelegates,
@ -117,7 +127,10 @@ class IslandApp extends HookConsumerWidget {
builder:
(_) => WindowScaffold(
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
List<AutoRoute> get routes => [
RedirectRoute(path: '/', redirectTo: '/explore'),
AutoRoute(page: ExploreRoute.page, path: '/explore'),
AutoRoute(
page: TabsRoute.page,
path: '/',
initial: true,
page: AccountShellRoute.page,
path: '/account',
children: [
AutoRoute(page: ExploreRoute.page, path: 'explore'),
AutoRoute(page: AccountRoute.page, path: 'account'),
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: AccountRoute.page, path: ''),
AutoRoute(page: AccountSettingsRoute.page, path: 'settings'),
AutoRoute(page: AccountProfileRoute.page, path: ':name'),
AutoRoute(page: PublisherProfileRoute.page, path: ':name/calendar'),
AutoRoute(page: MyselfEventCalendarRoute.page, path: 'me/calendar'),
AutoRoute(page: UpdateProfileRoute.page, path: 'me/update'),
AutoRoute(page: ManagedPublisherRoute.page, path: 'me/publishers'),
],
),
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: LoginRoute.page, path: '/auth/login'),
AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
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: PostDetailRoute.page, path: '/posts/:id'),
AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),

View File

@ -10,8 +10,10 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
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: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/me/event_calendar.dart' as _i15;
import 'package:island/screens/account/me/publishers.dart' as _i9;
@ -81,20 +83,43 @@ class AccountProfileRouteArgs {
/// generated route for
/// [_i2.AccountScreen]
class AccountRoute extends _i25.PageRouteInfo<void> {
const AccountRoute({List<_i25.PageRouteInfo>? children})
: super(AccountRoute.name, initialChildren: children);
class AccountRoute extends _i25.PageRouteInfo<AccountRouteArgs> {
AccountRoute({
_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 _i25.PageInfo page = _i25.PageInfo(
name,
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
/// [_i3.AccountSettingsScreen]
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
/// [_i4.ChatDetailScreen]
class ChatDetailRoute extends _i25.PageRouteInfo<ChatDetailRouteArgs> {
@ -154,20 +195,43 @@ class ChatDetailRouteArgs {
/// generated route for
/// [_i5.ChatListScreen]
class ChatListRoute extends _i25.PageRouteInfo<void> {
const ChatListRoute({List<_i25.PageRouteInfo>? children})
: super(ChatListRoute.name, initialChildren: children);
class ChatListRoute extends _i25.PageRouteInfo<ChatListRouteArgs> {
ChatListRoute({
_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 _i25.PageInfo page = _i25.PageInfo(
name,
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
/// [_i6.ChatRoomScreen]
class ChatRoomRoute extends _i25.PageRouteInfo<ChatRoomRouteArgs> {
@ -697,7 +761,7 @@ class NewStickersRouteArgs {
class PostComposeRoute extends _i25.PageRouteInfo<PostComposeRouteArgs> {
PostComposeRoute({
_i26.Key? key,
_i27.SnPost? originalPost,
_i28.SnPost? originalPost,
List<_i25.PageRouteInfo>? children,
}) : super(
PostComposeRoute.name,
@ -726,7 +790,7 @@ class PostComposeRouteArgs {
final _i26.Key? key;
final _i27.SnPost? originalPost;
final _i28.SnPost? originalPost;
@override
String toString() {
@ -1047,21 +1111,54 @@ class StickersRouteArgs {
}
/// generated route for
/// [_i22.TabsScreen]
class TabsRoute extends _i25.PageRouteInfo<void> {
const TabsRoute({List<_i25.PageRouteInfo>? children})
: super(TabsRoute.name, initialChildren: children);
/// [_i22.TabsNavigationWidget]
class TabsNavigationWidget
extends _i25.PageRouteInfo<TabsNavigationWidgetArgs> {
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(
name,
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
/// [_i23.UpdateProfileScreen]
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/userinfo.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/leveling_progress.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';
@RoutePage()
class AccountScreen extends HookConsumerWidget {
const AccountScreen({super.key});
class AccountShellScreen extends HookConsumerWidget {
const AccountShellScreen({super.key});
@override
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);
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: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/services/responsive.dart';
import 'package:material_symbols_icons/symbols.dart';
@RoutePage()
class TabsScreen extends StatelessWidget {
const TabsScreen({super.key});
final currentRouteProvider = StateProvider<String?>((ref) => null);
class TabNavigationObserver extends AutoRouterObserver {
Function(String?) onChange;
TabNavigationObserver({required this.onChange});
@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 useExpandableLayout = isWidestScreen(context);
final currentRoute = ref.watch(currentRouteProvider);
print('currentRoute: $currentRoute');
int activeIndex = 0;
final destinations = [
NavigationDestination(
@ -31,90 +66,93 @@ class TabsScreen extends StatelessWidget {
),
];
final routes = [
final routes = <PageRouteInfo>[
ExploreRoute(),
ChatListRoute(),
RealmListRoute(),
AccountRoute(),
];
final routeNames = [
ExploreRoute.name,
ChatListRoute.name,
RealmListRoute.name,
AccountRoute.name,
ChatShellRoute.name,
AccountShellRoute.name,
];
return AutoTabsRouter.tabBar(
routes: routes,
scrollDirection: useHorizontalLayout ? Axis.vertical : Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
builder: (context, child, _) {
final tabsRouter = AutoTabsRouter.of(context);
activeIndex = routes.indexWhere((route) => route.routeName == currentRoute);
if (activeIndex == -1) {
activeIndex = 0;
}
// Check if current route is a tab route
final currentRoute = context.router.topRoute;
final isTabRoute = routes.any(
(route) => route.routeName == currentRoute.name,
);
final isTabRoute = routeNames.any((route) {
return route == currentRoute;
});
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
body:
useHorizontalLayout
? Row(
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Colors.transparent,
body:
useHorizontalLayout
? Row(
children: [
Column(
children: [
if (isTabRoute)
Column(
children: [
Gap(MediaQuery.of(context).padding.top + 8),
if (useExpandableLayout)
Expanded(
child: NavigationDrawer(
backgroundColor: Colors.transparent,
children: [
for (final destination in destinations)
NavigationDrawerDestination(
label: Text(destination.label),
icon: destination.icon,
Gap(MediaQuery.of(context).padding.top + 8),
if (useExpandableLayout)
Expanded(
child: NavigationDrawer(
backgroundColor: Colors.transparent,
children: [
for (final destination in destinations)
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),
),
],
),
)
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),
],
)
.toList(),
),
),
if (isTabRoute)
VerticalDivider(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
Expanded(child: child),
Gap(MediaQuery.of(context).padding.bottom + 8),
],
)
: child,
bottomNavigationBar:
!useHorizontalLayout && isTabRoute
? NavigationBar(
selectedIndex: tabsRouter.activeIndex,
onDestinationSelected: tabsRouter.setActiveIndex,
destinations: destinations,
)
: null,
);
},
),
VerticalDivider(
color: Theme.of(context).dividerColor,
width: 1 / MediaQuery.of(context).devicePixelRatio,
),
Expanded(child: child),
],
)
: child,
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
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 chatInvites = ref.watch(chatroomInvitesProvider);
@ -133,12 +138,6 @@ class ChatListScreen extends HookConsumerWidget {
}
}
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return SizedBox.shrink();
}
return AppScaffold(
appBar: AppBar(
title: Text('chat').tr(),
@ -222,7 +221,7 @@ class ChatListScreen extends HookConsumerWidget {
room: item,
isDirect: item.type == 1,
onTap: () {
if (isWide) {
if (context.router.topRoute.name == ChatRoomRoute.name) {
context.router.replace(ChatRoomRoute(id: item.id));
} else {
context.router.push(ChatRoomRoute(id: item.id));