Files
xianyan/lib/core/router/app_router.dart
Developer 6119918185 release: bump version to 6.6.25+2606241
主要变更:
1. 新增桌面端托盘图标支持深色/浅色主题切换
2. 重构应用锁、动画配置、小组件导航服务职责
3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题
4. 优化诗词服务、阅读进度、搜索结果空状态体验
5. 完善macOS打包配置与错误静默处理逻辑
6. 新增快速卡片多语言适配与动画退出队列管理
2026-06-24 04:26:50 +08:00

252 lines
8.5 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 闲言APP — 路由配置(主入口)
// 创建时间: 2026-04-20
// 更新时间: 2026-06-09
// 作用: go_router 路由表组装 + ShellRoute 布局壳 + iOS 风格转场 + 深链接重定向
// 上次更新: 深度链接解析重构为配置驱动删除5个硬编码switch方法
// ============================================================
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import '../utils/logger.dart' show Log, LogCategory;
import '../utils/platform/platform_utils.dart' as pu;
import '../../features/onboarding/presentation/onboarding_page.dart';
import '../../features/home/presentation/home_page.dart';
import '../../features/discover/presentation/pages/home/discover_page.dart';
import '../../features/profile/presentation/profile_page.dart';
import '../../app/layout/app_shell.dart';
import '../storage/kv_storage.dart';
import '../utils/ui/page_transitions.dart';
import 'app_routes.dart';
import 'deep_link_resolver.dart';
import 'settings_routes.dart';
import 'tool_routes.dart';
import 'editor_router.dart';
import 'user_routes.dart';
import 'content_routes.dart';
import 'feature_routes.dart';
export 'app_routes.dart';
final rootNavigatorKey = GlobalKey<NavigatorState>();
final shellNavigatorKey = GlobalKey<NavigatorState>();
final _homeNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'home');
final _discoverNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'discover');
final _profileNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'profile');
/// 按主 Tab 索引返回对应的 ShellBranch navigatorKey
///
/// 0 = 首页, 1 = 发现, 2 = 我的
/// 用于在脱离 widget 树上下文时(如托盘菜单回调),仍能拿到当前活动 Tab
/// 的 NavigatorState从而保证导航进入正确的 Tab 栈。
/// 索引越界或未匹配时返回 null由调用方回退到 rootNavigatorKey 或 ref.context。
GlobalKey<NavigatorState>? tabNavigatorKeyFor(int tabIndex) {
switch (tabIndex) {
case 0:
return _homeNavigatorKey;
case 1:
return _discoverNavigatorKey;
case 2:
return _profileNavigatorKey;
default:
return null;
}
}
final GoRouter appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
// 初始路由始终为home由redirect动态判断是否需要跳转引导页
// KvStorage在GoRouter构造时可能尚未初始化不能在initialLocation中依赖它
initialLocation: AppRoutes.home,
debugLogDiagnostics: true,
observers: pu.isOhos
? [_OhosRouteObserver()]
: [BotToastNavigatorObserver()],
redirect: _handleRedirect,
routes: [
GoRoute(
path: AppRoutes.onboarding,
name: 'onboarding',
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) {
final skipAgreement = state.uri.queryParameters['skip_agreement'] == 'true';
return iosSlideTransition(
state: state,
child: OnboardingPage(skipAgreement: skipAgreement),
);
},
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
AppShell(child: navigationShell),
branches: [
StatefulShellBranch(
navigatorKey: _homeNavigatorKey,
routes: [
GoRoute(
path: AppRoutes.home,
name: 'home',
pageBuilder: (context, state) =>
iosFadeTransition(state: state, child: const HomePage()),
),
],
),
StatefulShellBranch(
navigatorKey: _discoverNavigatorKey,
routes: [
GoRoute(
path: AppRoutes.discover,
name: 'discover',
pageBuilder: (context, state) =>
iosFadeTransition(state: state, child: const DiscoverPage()),
),
],
),
StatefulShellBranch(
navigatorKey: _profileNavigatorKey,
routes: [
GoRoute(
path: AppRoutes.profile,
name: 'profile',
pageBuilder: (context, state) =>
iosFadeTransition(state: state, child: const ProfilePage()),
),
],
),
],
),
...buildEditorRoutes(rootNavigatorKey),
...buildSettingsRoutes(rootNavigatorKey),
...buildToolRoutes(rootNavigatorKey),
...buildUserRoutes(rootNavigatorKey),
...buildContentRoutes(rootNavigatorKey),
...buildFeatureRoutes(rootNavigatorKey),
],
errorBuilder: (context, state) => const NotFoundPage(),
);
/// 统一重定向:引导页判断 + 深度链接解析
/// 在每次导航时执行此时KvStorage已初始化完成
String? _handleRedirect(BuildContext context, GoRouterState state) {
final currentPath = state.matchedLocation;
// 1. 引导页判断仅在KvStorage已初始化时生效
if (KvStorage.isReady) {
final onboardingDone = KvStorage.isOnboardingCompleted;
final shouldShow = KvStorage.shouldShowOnboarding;
// 需要显示引导页且当前不在引导页 → 跳转引导页
if ((!onboardingDone || shouldShow) && currentPath != AppRoutes.onboarding) {
if (pu.isOhos) {
Log.d('🟢 [OHOS] redirect: onboardingDone=$onboardingDone, shouldShow=$shouldShow → /onboarding', null, null, LogCategory.router);
}
return AppRoutes.onboarding;
}
// 引导页已完成但当前在引导页 → 跳转首页
// 例外:如果带有 review=true 参数,说明是用户主动查看,不拦截
if (onboardingDone && !shouldShow && currentPath == AppRoutes.onboarding) {
final isReview = state.uri.queryParameters['review'] == 'true';
if (!isReview) {
final savedPage = KvStorage.getString('general_startup_page');
final location = switch (savedPage) {
'inspiration' => AppRoutes.inspiration,
'discover' => AppRoutes.discover,
'profile' => AppRoutes.profile,
_ => AppRoutes.home,
};
return location;
}
}
}
// 2. 深度链接解析
final uri = state.uri;
final resolved = AppRouter.resolveDeepLinkUri(uri);
if (resolved != null && resolved != currentPath) {
final scheme = uri.scheme.toLowerCase();
if (scheme == 'xianyan') {
Log.d('🔗 [DeepLink] xianyan://${uri.host}${uri.path}$resolved', null, null, LogCategory.router);
} else if (scheme == 'https' || scheme == 'http') {
Log.d('🔗 [DeepLink] ${uri.scheme}://${uri.host}${uri.path}$resolved', null, null, LogCategory.router);
}
return resolved;
}
return null;
}
/// 统一深度链接URI解析入口
/// 支持 xianyan:// scheme 和 https://s2ss.com 通用链接
/// 供 GoRouter redirect 和 DeepLinkService 共用
/// 解析逻辑委托给 DeepLinkResolver配置驱动从 route_registry 自动构建映射表)
class AppRouter {
AppRouter._();
/// 解析 URI 为内部路由路径
/// 返回 null 表示不是深度链接,不需要重定向
static String? resolveDeepLinkUri(Uri uri) {
final scheme = uri.scheme.toLowerCase();
if (scheme == 'xianyan') {
return DeepLinkResolver.resolveCustomScheme(uri);
}
if (scheme == 'https' || scheme == 'http') {
final host = uri.host.toLowerCase();
// CTC 子域名专用解析: https://ctc.s2ss.com/<key> → /ctc/notes/edit?key=<key>
if (host == 'ctc.s2ss.com') {
return DeepLinkResolver.resolveCtcDomain(uri);
}
if (host == 's2ss.com' || host == 'www.s2ss.com') {
return DeepLinkResolver.resolveHttps(uri);
}
}
return null;
}
}
class _OhosRouteObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
Log.d(
'🟢 [OHOS] Route didPush: ${route.settings.name} (${route.runtimeType})',
null,
null,
LogCategory.router,
);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
Log.d('🟢 [OHOS] Route didPop: ${route.settings.name}', null, null, LogCategory.router);
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
Log.d(
'🟢 [OHOS] Route didReplace: ${oldRoute?.settings.name}${newRoute?.settings.name}',
null,
null,
LogCategory.router,
);
}
@override
void didStartUserGesture(
Route<dynamic> route,
Route<dynamic>? previousRoute,
) {
Log.d('🟢 [OHOS] Route didStartUserGesture: ${route.settings.name}', null, null, LogCategory.router);
}
}