Initial project import
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
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/recycle_bin_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/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<NavigatorState>();
|
||||
|
||||
final router = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: '/splash',
|
||||
refreshListenable: SessionStore.instance,
|
||||
redirect: (_, state) {
|
||||
final aiOnly =
|
||||
state.matchedLocation == '/chat' ||
|
||||
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: '/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: (_, __) => const ReportPage()),
|
||||
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(),
|
||||
),
|
||||
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<MiaoJiApp> createState() => _MiaoJiAppState();
|
||||
}
|
||||
|
||||
class _MiaoJiAppState extends State<MiaoJiApp> 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);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
ApiClient.sessionExpired.removeListener(_handleSessionExpired);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_resumeServices();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _bootstrap() async {
|
||||
await _runSafely(ScreenshotChannel.cleanupStaleScreenshots);
|
||||
if (SessionStore.instance.hasSession) {
|
||||
await _runSafely(CurrentLedgerStore.instance.loadCached);
|
||||
}
|
||||
await _runSafely(RecognitionImportService.configureNativeContext);
|
||||
await _runSafely(RecognitionImportService.importAutomatic);
|
||||
if (mounted) setState(() {});
|
||||
unawaited(_refreshRemoteState());
|
||||
}
|
||||
|
||||
Future<void> _resumeServices() async {
|
||||
await _runSafely(RecognitionImportService.configureNativeContext);
|
||||
await _runSafely(RecognitionImportService.importAutomatic);
|
||||
unawaited(_refreshRemoteState());
|
||||
}
|
||||
|
||||
Future<void> _refreshRemoteState() async {
|
||||
await Future.wait([
|
||||
_runSafely(PublicConfigApi.init),
|
||||
_refreshAccountData(),
|
||||
_runSafely(SyncService.instance.run),
|
||||
]);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _refreshAccountData() async {
|
||||
final session = SessionStore.instance;
|
||||
if (!session.isAccount || session.shouldUseLocalOnly) return;
|
||||
await _runSafely(AuthApi.me);
|
||||
await _runSafely(
|
||||
() => CurrentLedgerStore.instance.ensureLoaded(force: true),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runSafely(Future<void> Function() operation) async {
|
||||
try {
|
||||
await operation();
|
||||
} catch (_) {
|
||||
// Optional startup and refresh work is isolated from the local app shell.
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRecognitionAction(Map<String, dynamic> action) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 180));
|
||||
final context = _rootNavigatorKey.currentContext;
|
||||
if (!mounted || context == null || !context.mounted) return;
|
||||
await RecognitionImportService.handleAction(context, action);
|
||||
}
|
||||
|
||||
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<void> _handleScreenshotReady(String path) async {
|
||||
if (_showingScreenshotSheet) return;
|
||||
await Future<void>.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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user