import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/api_client.dart'; import '../features/uploads/engine_scope.dart'; import '../l10n/app_localizations.dart'; import '../pages/home_page.dart'; import '../pages/login_page.dart'; import '../pages/password_change_page.dart'; import '../pages/server_history_page.dart'; import 'locale_controller.dart'; import 'theme.dart'; /// 根导航键:[ApiClient.onUnauthorized](非 UI 上下文触发)跳转 /login 用。 final GlobalKey rootNavigatorKey = GlobalKey(); /// App 根组件:主题 + l10n 委托 + 命名路由 + 401 驱逐接线 + 引擎生命周期。 /// /// 基线路由为命名路由(/login → /home → /password-change);随页面增长再 /// 评估是否迁移 go_router,当前保持零额外依赖。 /// initState 注册 [ApiClient.onUnauthorized]:任意业务端点 401(会话过期) /// 即清空栈回到 /login(登录/修改密码端点自身的 401 不触发,见 ApiClient), /// 并同步刷新会话账号(引擎 stop,任务保留库内)。 /// build watch [uploadEngineScopeProvider]——App 存续期间引擎随登录态 /// start/stop(Task 9 接线,见 engine_scope.dart)。 class PhotoLiveApp extends ConsumerStatefulWidget { const PhotoLiveApp({super.key}); @override ConsumerState createState() => _PhotoLiveAppState(); } class _PhotoLiveAppState extends ConsumerState { @override void initState() { super.initState(); ref.read(apiClientProvider).onUnauthorized = () { rootNavigatorKey.currentState?.pushNamedAndRemoveUntil( '/login', (route) => false, ); // 401 已清会话(ApiClient):刷新账号观察 → 引擎 stop;启动门 // (home 路由仍在栈上时)随会话离场重建为登录页。 ref.read(sessionAccountProvider.notifier).refresh(); }; } /// 语言解析(P3 Task 10): /// - 显式选择(locale 非 null)优先,按 languageCode 匹配; /// - system 档位下用系统 locale 匹配(zh_* → zh、en_* → en); /// - 均不匹配(如 fr)回退模板语言 zh——不依赖生成代码 supportedLocales /// 的排序(gen-l10n 按字母序 en 在前,默认解析会回退 en,与模板不符)。 static Locale _resolveLocale( Locale? deviceLocale, Iterable supportedLocales, ) { Locale? requested; for (final supported in supportedLocales) { if (supported.languageCode == deviceLocale?.languageCode) { requested = supported; break; } } // 语言码不匹配 → 模板语言(l10n.yaml template-arb-file=app_zh.arb)。 return requested ?? const Locale('zh'); } @override Widget build(BuildContext context) { // select 只取 Locale?:未引起 locale 变化的状态更新不重建 MaterialApp。 final locale = ref.watch(localeSettingsProvider.select((c) => c.locale)); // 引擎生命周期绑定(常驻 watch;账号在场 start / 离场 stop)。 ref.watch(uploadEngineScopeProvider); return MaterialApp( navigatorKey: rootNavigatorKey, onGenerateTitle: (context) => AppLocalizations.of(context).appName, theme: AppTheme.light, darkTheme: AppTheme.dark, locale: locale, localeResolutionCallback: _resolveLocale, supportedLocales: AppLocalizations.supportedLocales, localizationsDelegates: AppLocalizations.localizationsDelegates, routes: { '/login': (context) => const LoginPage(), '/home': (context) => const HomePage(), '/password-change': (context) => const PasswordChangePage(), '/server-history': (context) => const ServerHistoryPage(), }, // 启动门(UI06 用户反馈 2026-09-23:重启 App 不应强制重登):有未过期 // 会话直接进主页,否则登录页。 home: const _StartupGate(), ); } } /// 启动门:读本地会话——未过期直接进 HomePage,否则 LoginPage。 /// /// 会话已持久化(SecureSessionStore,服务端 TTL 默认 30 天);此前的缺口 /// 是启动固定落在登录页。token 实际失效(服务端已撤销)时由 ApiClient 401 /// 兜底:清会话并整栈回登录页(onUnauthorized),过期本地判断只用于避免 /// 「必 401 的闪跳」。 class _StartupGate extends ConsumerStatefulWidget { const _StartupGate(); @override ConsumerState<_StartupGate> createState() => _StartupGateState(); } class _StartupGateState extends ConsumerState<_StartupGate> { @override Widget build(BuildContext context) { // 状态驱动(声明式):watch 会话账号——在场进 HomePage,离场回 // LoginPage。401/登出清会话后 gate 自动重建,不依赖导航时序;home // 路由已被显式导航替换(登录成功 pushReplacement '/home' 等)时, // 401 兜底走 onUnauthorized 的 push。 final account = ref.watch(sessionAccountProvider); return account.maybeWhen( skipLoadingOnReload: false, orElse: () => const Scaffold(body: Center(child: CircularProgressIndicator())), data: (account) => account != null ? const HomePage() : const LoginPage(), ); } }