import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:miaoji_zhang/features/add/screenshot_parse_sheet.dart'; import 'package:miaoji_zhang/features/ai_mode/pages/ai_mode_page.dart'; import 'package:miaoji_zhang/features/auth/pages/login_page.dart'; import 'package:miaoji_zhang/features/auth/pages/register_page.dart'; import 'package:miaoji_zhang/features/auth/pages/splash_page.dart'; import 'package:miaoji_zhang/features/chat/chat_page.dart'; import 'package:miaoji_zhang/features/home/pages/home_page.dart'; import 'package:miaoji_zhang/features/home/pages/main_shell.dart'; import 'package:miaoji_zhang/features/home/pages/search_page.dart'; import 'package:miaoji_zhang/features/onboarding/pages/onboarding_page.dart'; import 'package:miaoji_zhang/features/settings/account_data_page.dart'; import 'package:miaoji_zhang/features/settings/appearance_page.dart'; import 'package:miaoji_zhang/features/settings/budget_page.dart'; import 'package:miaoji_zhang/features/settings/category_manage_page.dart'; import 'package:miaoji_zhang/features/settings/companion_page.dart'; import 'package:miaoji_zhang/features/settings/me_page.dart'; import 'package:miaoji_zhang/features/settings/push_settings_page.dart'; import 'package:miaoji_zhang/features/settings/recycle_bin_page.dart'; import 'package:miaoji_zhang/features/settings/recognition_batch_page.dart'; import 'package:miaoji_zhang/features/settings/legal_document_page.dart'; import 'package:miaoji_zhang/features/settings/screenshot_settings_page.dart'; import 'package:miaoji_zhang/features/settings/sync_conflicts_page.dart'; import 'package:miaoji_zhang/features/stats/report_page.dart'; import 'package:miaoji_zhang/features/stats/stats_page.dart'; import 'package:miaoji_zhang/features/add/add_page.dart'; import 'package:miaoji_zhang/shared/api/api_client.dart'; import 'package:miaoji_zhang/shared/api/auth_api.dart'; import 'package:miaoji_zhang/shared/api/config_api.dart'; import 'package:miaoji_zhang/shared/services/current_ledger_store.dart'; import 'package:miaoji_zhang/shared/services/recognition_import_service.dart'; import 'package:miaoji_zhang/shared/services/screenshot_channel.dart'; import 'package:miaoji_zhang/shared/services/sync_service.dart'; import 'package:miaoji_zhang/shared/services/session_store.dart'; import 'package:miaoji_zhang/shared/services/push_service.dart'; import 'package:miaoji_zhang/shared/theme/theme_store.dart'; import 'package:miaoji_zhang/shared/update/update_coordinator.dart'; import 'package:provider/provider.dart'; import 'package:miaoji_zhang/shared/theme/app_theme.dart'; final _rootNavigatorKey = GlobalKey(); final router = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/splash', refreshListenable: SessionStore.instance, redirect: (_, state) { final aiOnly = state.matchedLocation == '/ai-mode' || state.matchedLocation == '/companion'; if (aiOnly && SessionStore.instance.isAccount && !SessionStore.instance.aiEnabled) { return '/home'; } return null; }, routes: [ GoRoute(path: '/splash', builder: (_, __) => const SplashPage()), GoRoute( path: '/login', builder: (_, state) => LoginPage(notice: state.uri.queryParameters['notice']), ), GoRoute(path: '/register', builder: (_, __) => const RegisterPage()), GoRoute(path: '/onboarding', builder: (_, __) => const OnboardingPage()), GoRoute(path: '/add', builder: (_, __) => const AddPage()), GoRoute(path: '/budget', builder: (_, __) => const BudgetPage()), GoRoute(path: '/account-data', builder: (_, __) => const AccountDataPage()), GoRoute(path: '/appearance', builder: (_, __) => const AppearancePage()), GoRoute( path: '/notification-settings', builder: (_, __) => const PushSettingsPage(), ), GoRoute(path: '/recycle-bin', builder: (_, __) => const RecycleBinPage()), GoRoute( path: '/sync-conflicts', builder: (_, __) => const SyncConflictsPage(), ), GoRoute(path: '/companion', builder: (_, __) => const CompanionPage()), GoRoute( path: '/categories', builder: (_, __) => const CategoryManagePage(), ), GoRoute( path: '/report', builder: (_, state) => ReportPage( initialPeriod: state.uri.queryParameters['period'], initialAnchor: DateTime.tryParse( state.uri.queryParameters['date'] ?? '', ), ), ), GoRoute( path: '/legal/:kind', builder: (_, state) => LegalDocumentPage( kind: LegalDocumentKind.fromKey(state.pathParameters['kind']), ), ), GoRoute(path: '/search', builder: (_, __) => const SearchPage()), GoRoute( path: '/screenshot-settings', builder: (_, __) => const ScreenshotSettingsPage(), ), GoRoute( path: '/recognition-batches', builder: (_, state) => RecognitionBatchPage( initialBatchId: state.uri.queryParameters['batchId'], ), ), StatefulShellRoute.indexedStack( builder: (_, __, shell) => MainShell(shell: shell), branches: [ StatefulShellBranch( routes: [ GoRoute(path: '/home', builder: (_, __) => const HomePage()), ], ), StatefulShellBranch( routes: [ GoRoute(path: '/stats', builder: (_, __) => const StatsPage()), ], ), StatefulShellBranch( routes: [ GoRoute(path: '/chat', builder: (_, __) => const ChatTabPage()), ], ), StatefulShellBranch( routes: [GoRoute(path: '/me', builder: (_, __) => const MePage())], ), ], ), GoRoute(path: '/ai-mode', builder: (_, __) => const AiModePage()), ], ); class MiaoJiApp extends StatefulWidget { const MiaoJiApp({super.key}); @override State createState() => _MiaoJiAppState(); } class _MiaoJiAppState extends State with WidgetsBindingObserver { bool _showingScreenshotSheet = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); UpdateCoordinator.instance.bindNavigator(_rootNavigatorKey); ApiClient.sessionExpired.addListener(_handleSessionExpired); unawaited(_bootstrap()); ScreenshotChannel.onScreenshotReady( _handleScreenshotReady, onError: _handleScreenshotError, ); ScreenshotChannel.onRecognitionAction(_handleRecognitionAction); PushService.instance.setOpenHandler(_handlePushOpen); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); ApiClient.sessionExpired.removeListener(_handleSessionExpired); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _resumeServices(); } } Future _bootstrap() async { await _runSafely(ScreenshotChannel.cleanupStaleScreenshots); if (SessionStore.instance.hasSession) { await _runSafely(CurrentLedgerStore.instance.loadCached); } await _runSafely(_restoreRecognitionServices); await _runSafely(PushService.instance.initialize); if (mounted) setState(() {}); unawaited(_refreshRemoteState()); } Future _resumeServices() async { unawaited(ApiClient.instance.probe()); await _runSafely(_restoreRecognitionServices); await _runSafely(PushService.instance.refresh); unawaited(_refreshRemoteState()); } Future _restoreRecognitionServices() async { await RecognitionImportService.configureNativeContext(); await ScreenshotChannel.ensureRecognitionKeepAlive(); await RecognitionImportService.importAutomatic(); unawaited(_runSafely(_importAfterAccessibilityReconnect)); } Future _importAfterAccessibilityReconnect() async { final status = await ScreenshotChannel.waitForAccessibilityConnection(); if (status.accessibilityConnected) { await RecognitionImportService.importAutomatic(); } } Future _refreshRemoteState() async { await Future.wait([ _runSafely(PublicConfigApi.init), _refreshAccountData(), _runSafely(SyncService.instance.run), ]); if (mounted) setState(() {}); } Future _refreshAccountData() async { final session = SessionStore.instance; if (!session.isAccount || session.shouldUseLocalOnly) return; await _runSafely(AuthApi.me); await _runSafely( () => CurrentLedgerStore.instance.ensureLoaded(force: true), ); } Future _runSafely(Future Function() operation) async { try { await operation(); } catch (_) { // Optional startup and refresh work is isolated from the local app shell. } } Future _handleRecognitionAction(Map action) async { await Future.delayed(const Duration(milliseconds: 180)); final context = _rootNavigatorKey.currentContext; if (!mounted || context == null || !context.mounted) return; if (action['action'] == 'open_recognition_settings') { router.push('/screenshot-settings'); return; } await RecognitionImportService.handleAction(context, action); } Future _handlePushOpen(PushOpen open) async { await Future.delayed(const Duration(milliseconds: 150)); final context = _rootNavigatorKey.currentContext; if (!mounted || context == null || !context.mounted) return; if (!SessionStore.instance.isAccount && open.action == 'budget') { router.go('/login', extra: null); return; } switch (open.action) { case 'home': router.go('/home'); case 'budget': router.push('/budget'); case 'update': await UpdateCoordinator.instance.checkManually(context); case 'none': break; } } void _handleSessionExpired() { final context = _rootNavigatorKey.currentContext; if (context != null && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('云同步登录已过期,本地记账仍可使用;重新登录后可继续同步')), ); } } void _handleScreenshotError(String message) { final context = _rootNavigatorKey.currentContext; if (!mounted || context == null || !context.mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); if (message.contains('无障碍服务')) { router.push('/screenshot-settings'); } } Future _handleScreenshotReady(String path) async { if (_showingScreenshotSheet) return; await Future.delayed(const Duration(milliseconds: 150)); final context = _rootNavigatorKey.currentContext; if (!mounted || context == null || !context.mounted) return; _showingScreenshotSheet = true; try { await ScreenshotParseSheet.show(context, path); } finally { _showingScreenshotSheet = false; } } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: ThemeStore.instance, builder: (context, _) => ChangeNotifierProvider.value( value: CurrentLedgerStore.instance, child: MaterialApp.router( title: PublicConfigApi.appName, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: ThemeStore.instance.themeMode, routerConfig: router, debugShowCheckedModeBanner: false, ), ), ); } }