D.3.2 三态保存指示器: - 未保存 (灰色) → 保存中 (琥珀色脉冲点) → 已保存 (绿色点) - _PulsingDot 动画组件,800ms 呼吸效果 - 点击'完成'时显示保存中状态 D.3.3 工具栏触摸目标: - BoxConstraints 36x36 → 44x44,符合 WCAG 标准 D.3.4 主题偏好持久化: - SettingsBloc 接受 SharedPreferences,保存/恢复 themeMode - NuanjiApp 改为 StatefulWidget,异步初始化 SharedPreferences - 启动时显示 loading,初始化完成后渲染 app
157 lines
5.0 KiB
Dart
157 lines
5.0 KiB
Dart
// 暖记 App 根组件 — MaterialApp + BLoC Provider 注入
|
|
//
|
|
// 依赖注入结构:
|
|
// MultiRepositoryProvider
|
|
// ├─ ApiClient
|
|
// ├─ AuthRepository
|
|
// ├─ JournalRepository (IsarJournalRepository — 离线优先)
|
|
// ├─ RemoteJournalRepository (供 SyncEngine 使用)
|
|
// └─ ClassRepository
|
|
// └─ BlocProvider<AuthBloc>
|
|
// └─ MaterialApp.router
|
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:provider/provider.dart' show ListenableProvider;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
import 'config/app_config.dart';
|
|
import 'core/theme/app_theme.dart';
|
|
import 'core/routing/app_router.dart';
|
|
import 'data/local/secure_token_store_factory.dart';
|
|
import 'data/remote/api_client.dart';
|
|
import 'data/repositories/auth_repository.dart';
|
|
import 'data/repositories/journal_repository.dart';
|
|
import 'data/repositories/isar_journal_repository.dart';
|
|
import 'data/repositories/remote_journal_repository.dart';
|
|
import 'data/repositories/class_repository.dart';
|
|
import 'data/services/sync_engine.dart';
|
|
import 'features/auth/bloc/auth_bloc.dart';
|
|
import 'features/profile/bloc/settings_bloc.dart';
|
|
|
|
/// 暖记 App — 根组件
|
|
class NuanjiApp extends StatefulWidget {
|
|
const NuanjiApp({super.key});
|
|
|
|
@override
|
|
State<NuanjiApp> createState() => _NuanjiAppState();
|
|
}
|
|
|
|
class _NuanjiAppState extends State<NuanjiApp> {
|
|
late final ApiClient _apiClient;
|
|
late final AuthRepository _authRepository;
|
|
late final JournalRepository _journalRepository;
|
|
late final RemoteJournalRepository _remoteJournalRepository;
|
|
late final SyncEngine _syncEngine;
|
|
late final ClassRepository _classRepository;
|
|
late final SettingsBloc _settingsBloc;
|
|
late final AuthBloc _authBloc;
|
|
|
|
bool _initialized = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_initApp();
|
|
}
|
|
|
|
Future<void> _initApp() async {
|
|
final config = kDebugMode ? AppConfig.dev : AppConfig.fromEnvironment();
|
|
_apiClient = ApiClient(baseUrl: config.apiBaseUrl);
|
|
final tokenStore = createSecureTokenStore();
|
|
_authRepository = AuthRepository(apiClient: _apiClient, tokenStore: tokenStore);
|
|
_journalRepository = kIsWeb
|
|
? RemoteJournalRepository(api: _apiClient)
|
|
: IsarJournalRepository();
|
|
_remoteJournalRepository = RemoteJournalRepository(api: _apiClient);
|
|
_syncEngine = SyncEngine(apiClient: _apiClient);
|
|
_classRepository = ClassRepository(api: _apiClient);
|
|
_settingsBloc = SettingsBloc(
|
|
prefs: await SharedPreferences.getInstance(),
|
|
);
|
|
_authBloc = AuthBloc(
|
|
authRepository: _authRepository,
|
|
classRepository: _classRepository,
|
|
);
|
|
|
|
// 启动时检查认证状态
|
|
_authBloc.add(const AppStarted());
|
|
|
|
// 异步恢复 SyncEngine 持久化队列
|
|
_syncEngine.restorePendingQueue();
|
|
_syncEngine.startAutoSync();
|
|
|
|
// 认证状态监听:登出时清除 token
|
|
_authBloc.stream.listen((state) {
|
|
if (state is! Authenticated) {
|
|
_apiClient.clearToken();
|
|
}
|
|
});
|
|
|
|
// Token 刷新彻底失败时 → 派发 AuthExpired
|
|
_apiClient.onAuthFailed = () {
|
|
_authBloc.add(const AuthExpired());
|
|
};
|
|
|
|
setState(() => _initialized = true);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!_initialized) {
|
|
return const MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
home: Scaffold(body: Center(child: CircularProgressIndicator())),
|
|
);
|
|
}
|
|
|
|
return MultiRepositoryProvider(
|
|
providers: [
|
|
RepositoryProvider<ApiClient>.value(value: _apiClient),
|
|
RepositoryProvider<AuthRepository>.value(value: _authRepository),
|
|
RepositoryProvider<JournalRepository>.value(value: _journalRepository),
|
|
RepositoryProvider<RemoteJournalRepository>.value(value: _remoteJournalRepository),
|
|
RepositoryProvider<SyncEngine>.value(value: _syncEngine),
|
|
RepositoryProvider<ClassRepository>.value(value: _classRepository),
|
|
],
|
|
child: ListenableProvider<SettingsBloc>.value(
|
|
value: _settingsBloc,
|
|
child: Builder(
|
|
builder: (context) {
|
|
final settings = context.watch<SettingsBloc>();
|
|
return BlocProvider<AuthBloc>.value(
|
|
value: _authBloc,
|
|
child: _AppView(
|
|
router: createAppRouter(_authBloc),
|
|
themeMode: settings.state.themeMode,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// App 视图 — MaterialApp.router 包装
|
|
class _AppView extends StatelessWidget {
|
|
final GoRouter router;
|
|
final ThemeMode themeMode;
|
|
|
|
const _AppView({required this.router, this.themeMode = ThemeMode.system});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp.router(
|
|
title: '暖记',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: AppTheme.light(),
|
|
darkTheme: AppTheme.dark(),
|
|
themeMode: themeMode,
|
|
routerConfig: router,
|
|
);
|
|
}
|
|
}
|