主要变更: 1. 新增桌面端托盘图标支持深色/浅色主题切换 2. 重构应用锁、动画配置、小组件导航服务职责 3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题 4. 优化诗词服务、阅读进度、搜索结果空状态体验 5. 完善macOS打包配置与错误静默处理逻辑 6. 新增快速卡片多语言适配与动画退出队列管理
252 lines
8.5 KiB
Dart
252 lines
8.5 KiB
Dart
// ============================================================
|
||
// 闲言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);
|
||
}
|
||
}
|