Files
xianyan/lib/app/app.dart
Developer f281e465bb chore: v6.6.6 版本迭代更新
主要变更:
1. 重构"国学"相关模块为"经典名句",统一命名规范
2. 重命名"阅读报告"为"使用报告",调整相关文案与配置
3. 修复iOS模拟器图片缓存兼容问题,优化图表渲染逻辑
4. 新增设备活跃状态前端兜底判断,修复在线计数异常
5. 完善登录/注册流程,新增忘记密码路由与账户编辑提示
6. 优化文件传输与字体导入逻辑,废弃过时的bytes属性使用
7. 添加Spotlight全局快捷键支持,更新隐私权限与通知配置
8. 补充数据库迁移脚本与部署文档,修复后端接口兼容问题
9. 调整部分UI交互细节,优化内存占用与应用稳定性
2026-06-07 06:56:52 +08:00

613 lines
21 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 应用根组件
/// 创建时间: 2026-04-20
/// 更新时间: 2026-06-05
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay
/// 上次更新: 初始化AccessibilityServicebuild时同步系统无障碍状态
/// ============================================================
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter_quill/flutter_quill.dart'
show FlutterQuillLocalizations;
import 'package:flutter/services.dart';
import '../core/services/device/quick_actions_service.dart';
import '../core/services/device/macos_platform_service.dart';
import '../core/services/data/home_widget_service.dart';
import '../core/services/ui/status_bar_service.dart';
import '../core/services/accessibility/accessibility_service.dart';
import '../core/router/app_router.dart' show appRouter, rootNavigatorKey;
import '../core/router/app_routes.dart';
import '../core/layout/ohos_app_shell.dart';
import '../core/router/ohos_nav_bridge.dart';
import '../core/services/device/app_lock_service.dart';
import '../core/services/performance/app_lifecycle_gate.dart';
import '../core/theme/app_theme.dart';
import '../core/utils/logger.dart';
import '../core/utils/platform/platform_utils.dart' as pu;
import '../core/network/api_client.dart';
import '../core/providers/connectivity_provider.dart';
import '../core/theme/app_typography.dart';
import '../features/template/services/wallpaper_preload_service.dart';
import '../shared/widgets/adaptive/keyboard_back_handler.dart';
import '../shared/widgets/feedback/app_toast.dart';
import '../features/mine/settings/providers/theme_settings_provider.dart';
import '../features/mine/settings/providers/general_settings_provider.dart';
import '../features/mine/settings/presentation/font_management_notifier.dart';
import '../features/mine/settings/presentation/lock/app_lock_overlay.dart';
import '../core/sync/sync.dart'
show disposeReadlaterRefreshController, disposeFavoriteRefreshController;
import '../l10n/app_locale.dart';
import '../main.dart' show liquidGlassReady;
const _localizationsDelegates = [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
DefaultCupertinoLocalizations.delegate,
FlutterQuillLocalizations.delegate,
];
class AppScrollBehavior extends MaterialScrollBehavior {
const AppScrollBehavior();
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
PointerDeviceKind.trackpad,
};
}
class XianyanApp extends ConsumerStatefulWidget {
const XianyanApp({super.key});
@override
ConsumerState<XianyanApp> createState() => _XianyanAppState();
}
class _XianyanAppState extends ConsumerState<XianyanApp>
with WidgetsBindingObserver, AppLifecycleGate {
static const _dataManagementChannel = MethodChannel(
'apps.xy.xianyan/data_management',
);
Duration? _lastAnimateDuration;
Curve? _lastAnimateCurve;
bool _pendingDataManagement = false;
@override
void initState() {
super.initState();
initLifecycleGate();
_initQuickActions();
_initWallpaperPreload();
_initDataManagementChannel();
_initHttpCache();
_initAccessibility();
}
/// 初始化无障碍服务
void _initAccessibility() {
// 从设置中恢复语义调试状态
final generalSettings = ref.read(generalSettingsProvider);
AccessibilityService.instance.initSemanticsDebug(
generalSettings.semanticsDebugEnabled,
);
}
void _initQuickActions() {
QuickActionsService.init(
onActionCallback: (String route) {
final context = rootNavigatorKey.currentContext;
if (context == null) {
Log.w('🚀 [QuickActions] context不可用延迟导航');
return;
}
if (pu.isOhos) {
OhosNavBridge.push(context, route);
} else {
// 使用push而非go保留导航栈以便返回
appRouter.push(route);
}
},
);
}
Future<void> _initHttpCache() async {
try {
await ApiClient.instance.initCache();
} catch (e) {
Log.e('HTTP缓存初始化失败', e);
}
}
void _initDataManagementChannel() {
if (pu.isWeb || !pu.isOhos) return;
_dataManagementChannel.setMethodCallHandler((call) async {
if (call.method == 'open_data_management') {
_navigateToDataManagement();
}
});
_checkPendingManageStorage();
}
Future<void> _checkPendingManageStorage() async {
if (pu.isWeb) return;
try {
final isPending = await _dataManagementChannel.invokeMethod<bool>(
'checkPendingManageStorage',
);
if (isPending == true) {
_pendingDataManagement = true;
Log.i('📦 [DataManagement] 检测到系统清除数据意图,等待导航');
}
} catch (e) {
Log.e('数据管理通道检查失败', e);
}
}
void _navigateToDataManagement() {
_pendingDataManagement = false;
Future.delayed(const Duration(milliseconds: 300), () {
final context = rootNavigatorKey.currentContext;
if (context == null) {
_pendingDataManagement = true;
Log.w('📦 [DataManagement] context不可用延迟导航');
return;
}
Log.i('📦 [DataManagement] 导航到数据管理页面');
// ignore: use_build_context_synchronously
if (!context.mounted) return;
if (pu.isOhos) {
OhosNavBridge.push(context, AppRoutes.dataManagement);
} else {
appRouter.push(AppRoutes.dataManagement);
}
}).catchError((_) {});
}
void _handlePendingDataManagement() {
if (!_pendingDataManagement) return;
_navigateToDataManagement();
}
/// WiFi环境下预加载壁纸缩略图
void _initWallpaperPreload() {
Future.microtask(() async {
try {
await WallpaperPreloadService.preloadIfNeeded();
} catch (e) {
Log.e('壁纸预加载初始化失败', e);
}
}).catchError((_) {});
}
@override
void dispose() {
disposeLifecycleGate();
disposeReadlaterRefreshController();
disposeFavoriteRefreshController();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
try {
ref.read(appLockProvider.notifier).onAppPaused();
} catch (e) {
Log.e('应用锁暂停处理失败', e);
}
break;
case AppLifecycleState.resumed:
Future.microtask(() {
try {
ref.read(appLockProvider.notifier).onAppResumed();
} catch (e) {
Log.e('应用锁恢复处理失败', e);
}
_handlePendingWidgetNavigation();
_handlePendingDataManagement();
QuickActionsService.handlePendingAction();
});
break;
default:
break;
}
}
@override
void didChangePlatformBrightness() {
super.didChangePlatformBrightness();
final settings = ref.read(themeSettingsProvider);
if (settings.themeMode == AppThemeMode.system) {
final isDark =
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark;
MacosPlatformService.syncTheme(isDark);
}
}
@override
void didChangeLocales(List<Locale>? locales) {
super.didChangeLocales(locales);
if (locales != null && locales.isNotEmpty) {
ref.read(systemLocaleVersionProvider.notifier).increment();
Log.i('系统语言变化: ${locales.first.toLanguageTag()}');
}
}
void _handlePendingWidgetNavigation() {
final route = HomeWidgetService.consumePendingNavigation();
if (route == null) return;
final context = rootNavigatorKey.currentContext;
if (context == null) {
Log.w('WidgetNavigation: rootNavigatorKey context 不可用');
return;
}
Log.i('WidgetNavigation: 从小组件导航到 $route');
if (pu.isOhos) {
OhosNavBridge.go(context, route);
} else {
appRouter.push(route);
}
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(themeSettingsProvider);
final generalSettings = ref.watch(generalSettingsProvider);
final fontState = ref.watch(fontManagementProvider);
final appLocale = ref.watch(appLocaleProvider);
final supportedLocales = ref.watch(supportedLocalesProvider);
final textDirection = ref.watch(appTextDirectionProvider);
final connectivity = ref.watch(connectivityProvider);
// 同步系统无障碍状态到AccessibilityService
AccessibilityService.instance.updateFromContext(context);
// 修复9: 仅在设置变化时更新Animate全局配置避免每次build重复设置
final targetDuration = settings.animationEnabled
? Duration(
milliseconds: (300 * settings.animationIntensity.durationMultiplier)
.round(),
)
: Duration.zero;
final targetCurve = settings.animationIntensity.curve;
if (_lastAnimateDuration != targetDuration ||
_lastAnimateCurve != targetCurve) {
_lastAnimateDuration = targetDuration;
_lastAnimateCurve = targetCurve;
Animate.defaultDuration = targetDuration;
Animate.defaultCurve = targetCurve;
}
final builtInFontFamilies = fontStyleOptions
.map((opt) => opt.fontFamily)
.toSet();
final effectiveFontFamily = settings.customFontFamily.isNotEmpty
? settings.customFontFamily
: (!builtInFontFamilies.contains(fontState.activeFontFamily)
? fontState.activeFontFamily
: settings.fontStyle.fontFamily);
final theme = AppTheme.buildFromSettings(
isDark: settings.isDark,
isAmoled: settings.isAmoled,
accent: settings.accentColor.color,
accentLight: settings.accentColor.lightPrimary,
fontScale: settings.fontSize.scale,
fontWeight: settings.fontWeight.weight,
fontFamily: effectiveFontFamily,
glassBlurMultiplier: settings.glassIntensity.blurMultiplier,
cardStyleId: settings.cardStyleId,
cornerRadiusId: settings.cornerRadiusId,
highContrast: generalSettings.highContrastEnabled,
colorWeakTypeId: generalSettings.colorWeakTypeId,
);
final darkTheme = AppTheme.buildFromSettings(
isDark: true,
isAmoled: settings.isAmoled,
accent: settings.accentColor.darkPrimary,
accentLight: settings.accentColor.lightPrimary,
fontScale: settings.fontSize.scale,
fontWeight: settings.fontWeight.weight,
fontFamily: effectiveFontFamily,
glassBlurMultiplier: settings.glassIntensity.blurMultiplier,
cardStyleId: settings.cardStyleId,
cornerRadiusId: settings.cornerRadiusId,
highContrast: generalSettings.highContrastEnabled,
colorWeakTypeId: generalSettings.colorWeakTypeId,
);
final themeMode = _resolveThemeMode(settings.themeMode);
final effectiveIsDark = switch (themeMode) {
ThemeMode.dark => true,
ThemeMode.light => false,
ThemeMode.system =>
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
Brightness.dark,
};
MacosPlatformService.syncTheme(effectiveIsDark);
return Directionality(
textDirection: textDirection,
child: _LocaleTransitionWrapper(
locale: appLocale,
animationEnabled: settings.animationEnabled,
child: ScreenUtilInit(
designSize: const Size(390, 844),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
Widget buildApp() {
if (pu.isOhos) {
Log.i(
'🟢 [OHOS] 使用 MaterialApp(home:) + OhosAppShell (liquidGlass=$liquidGlassReady)',
);
final ohosMaterialApp = MaterialApp(
navigatorKey: rootNavigatorKey,
title: '闲言',
debugShowCheckedModeBanner: false,
scrollBehavior: const AppScrollBehavior(),
locale: appLocale,
supportedLocales: supportedLocales,
localizationsDelegates: _localizationsDelegates,
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
home: const OhosAppShell(),
navigatorObservers: [
BotToastNavigatorObserver(),
], // [BotToast] 步骤3: 注册路由观察者
builder: (context, widget) {
final botToastBuilder =
BotToastInit(); // [BotToast] 步骤1: 创建Toast builder
AppToast.markInitialized(); // [BotToast] 步骤2: 标记初始化完成
final botWidget = botToastBuilder(context, widget);
return DefaultTextStyle(
style: const TextStyle(decoration: TextDecoration.none),
child: StatusBarStyleRegion(
isDark: settings.isDark,
child: botWidget,
),
);
},
);
return InheritedGoRouter(
goRouter: appRouter,
child: liquidGlassReady
? GlassTheme(
data: GlassThemeData(
light: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 20.0,
blur: settings.glassEnabled ? 2.0 : 0.0,
refractiveIndex: 1.4,
lightIntensity: 0.8,
ambientStrength: 0.4,
saturation: 1.0,
),
),
dark: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 28.0,
blur: settings.glassEnabled ? 3.0 : 0.0,
lightIntensity: 1.0,
refractiveIndex: 1.2,
saturation: 1.0,
),
),
),
child: ohosMaterialApp,
)
: ohosMaterialApp,
);
}
final materialApp = MaterialApp.router(
title: '闲言',
debugShowCheckedModeBanner: false,
scrollBehavior: const AppScrollBehavior(),
locale: appLocale,
supportedLocales: supportedLocales,
localizationsDelegates: _localizationsDelegates,
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
routerConfig: appRouter,
builder: (context, widget) {
final botToastBuilder =
BotToastInit(); // [BotToast] 步骤1: 创建Toast builder
AppToast.markInitialized(); // [BotToast] 步骤2: 标记初始化完成
final botWidget = botToastBuilder(context, widget);
return DefaultTextStyle(
style: const TextStyle(decoration: TextDecoration.none),
child: StatusBarStyleRegion(
isDark: settings.isDark,
child: botWidget,
),
);
},
);
return GlassTheme(
data: GlassThemeData(
light: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 20.0,
blur: settings.glassEnabled ? 2.0 : 0.0,
refractiveIndex: 1.4,
lightIntensity: 0.8,
ambientStrength: 0.4,
saturation: 1.0,
),
),
dark: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 28.0,
blur: settings.glassEnabled ? 3.0 : 0.0,
lightIntensity: 1.0,
refractiveIndex: 1.2,
saturation: 1.0,
),
),
),
child: materialApp,
);
}
final isLocked = ref.watch(
appLockProvider.select((s) => s.isLocked),
);
return KeyboardBackHandler(
child: Stack(
children: [
buildApp(),
if (isLocked) const Positioned.fill(child: AppLockOverlay()),
if (!connectivity.isConnected)
Positioned(
top: 0,
left: 0,
right: 0,
child: SafeArea(
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
color: CupertinoColors.systemGrey6.withValues(
alpha: 0.95,
),
child: Row(
children: [
const Icon(
CupertinoIcons.wifi_exclamationmark,
size: 16,
color: CupertinoColors.systemGrey,
),
const SizedBox(width: 8),
Text(
'网络已断开,部分功能可能不可用',
style: AppTypography.footnote.copyWith(
color: CupertinoColors.systemGrey,
),
),
],
),
),
),
),
],
),
);
},
),
),
);
}
ThemeMode _resolveThemeMode(AppThemeMode mode) {
return switch (mode) {
AppThemeMode.light => ThemeMode.light,
AppThemeMode.dark => ThemeMode.dark,
AppThemeMode.amoled => ThemeMode.dark,
AppThemeMode.system => ThemeMode.system,
};
}
}
class _LocaleTransitionWrapper extends StatefulWidget {
const _LocaleTransitionWrapper({
required this.locale,
required this.animationEnabled,
required this.child,
});
final Locale locale;
final bool animationEnabled;
final Widget child;
@override
State<_LocaleTransitionWrapper> createState() =>
_LocaleTransitionWrapperState();
}
class _LocaleTransitionWrapperState extends State<_LocaleTransitionWrapper>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late Animation<double> _opacity;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_opacity = Tween<double>(begin: 1.0, end: 1.0).animate(_controller);
_controller.value = 1.0;
}
@override
void didUpdateWidget(_LocaleTransitionWrapper oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.locale != widget.locale) {
if (widget.animationEnabled) {
_triggerTransition();
}
}
}
void _triggerTransition() {
_opacity = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_controller.forward(from: 0.0);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(opacity: _opacity.value, child: child);
},
child: widget.child,
);
}
}