Files
xianyan/lib/app/app.dart
Developer 544f77c0ce chore: 完成v2.4.7版本迭代更新
本次更新包含多项功能优化与兼容性修复:
1. iOS/鸿蒙端添加加密出口合规配置,跳过App Store审核问卷
2. 新增学习计划设置页路由与国际化支持
3. 修复鸿蒙端剪贴板粘贴不工作问题,安装标准剪贴板拦截器
4. 优化收藏功能:兼容复合ID、添加状态同步与触觉反馈
5. 修复鸿蒙端相册保存兼容性,统一使用系统分享降级方案
6. 优化搜索快捷方式跳转逻辑,避免白屏问题
7. 更新本地化资源,新增闲情逸致、学习计划等模块翻译
8. 修复节气日期表排序与跨年边界问题
9. 优化设备信息页面显示,新增系统版本号展示
10. 重构文件传输二维码逻辑,使用纯URL提升兼容性
11. 优化设置项布局,避免文本溢出问题
12. 修复登录页记住账户功能,新增隐私协议守卫
13. 更新macOS依赖库,替换flutter_secure_storage为darwin版本
2026-06-17 08:45:34 +08:00

762 lines
26 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-17
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay
/// 上次更新: 修复安卓桌面搜索快捷方式白屏问题,改用 go 切换 Tab + 直接弹 SpotlightSearchOverlay
/// ============================================================
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/device/windows_platform_service.dart';
import '../core/services/data/home_widget_service.dart';
import '../core/storage/database/app_database.dart';
import '../core/services/ui/status_bar_service.dart';
import '../core/services/accessibility_service.dart';
import '../core/router/app_router.dart' show appRouter, rootNavigatorKey;
import '../core/router/app_routes.dart';
import '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_capability.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 '../shared/widgets/display/app_icon.dart';
import '../features/settings/providers/theme_settings_provider.dart';
import '../features/settings/providers/general_settings_provider.dart';
import '../features/settings/presentation/font_management_notifier.dart';
import '../features/settings/presentation/lock/app_lock_overlay.dart';
import '../features/profile/presentation/spotlight_search/spotlight_search_overlay.dart';
import '../core/sync/data_sync_compat.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) {
try {
final context = rootNavigatorKey.currentContext;
if (context == null) {
Log.w('🚀 [QuickActions] context不可用延迟导航');
return;
}
// 搜索快捷方式:特殊标记 'action:search'
// 不再 push /profile 路由会导致白屏shell 外创建新页面 + initState 不触发)
// 改为:切换到 profile Tab + 直接弹出 SpotlightSearchOverlay
if (route == 'action:search') {
_handleSearchShortcut();
return;
}
final cleanRoute = route.split('?').first;
if (pu.isOhos) {
OhosNavBridge.push(context, cleanRoute);
} else {
// 安卓端确保路由系统已就绪再导航防止冷启动时GoRouter未初始化导致闪退
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
appRouter.push(cleanRoute);
} catch (e) {
Log.e('🚀 [QuickActions] Android端快捷方式导航失败', e);
}
});
}
} catch (e) {
Log.e('🚀 [QuickActions] 快捷方式回调异常', e);
}
},
);
}
/// 处理搜索快捷方式
///
/// 修复白屏问题:
/// - 旧逻辑push('/profile?action=search') → shell 外创建新页面 → 白屏只显示底栏
/// - 新逻辑go('/profile') 切换到 profile Tab + 直接弹出 SpotlightSearchOverlay
///
/// 同时兼容冷启动和热启动场景:
/// - 冷启动ProfilePage 首次构建,通过 pendingSearch 机制弹出
/// - 热启动ProfilePage 已构建,直接在 root context 弹出搜索浮层
void _handleSearchShortcut() {
final context = rootNavigatorKey.currentContext;
if (context == null) {
Log.w('🚀 [QuickActions] 搜索快捷方式: context不可用');
return;
}
if (pu.isOhos) {
// 鸿蒙端:切换到 profile 页面后弹出搜索
OhosNavBridge.push(context, AppRoutes.profile);
Future.delayed(const Duration(milliseconds: 600), () {
final ctx = rootNavigatorKey.currentContext;
if (ctx != null && ctx.mounted) {
SpotlightSearchOverlay.show(ctx, ref);
}
});
return;
}
// 安卓/iOS端
// 1. 切换到 profile Tabgo 而非 push避免在 shell 外创建新页面)
// 2. 延迟弹出搜索浮层,等待 Tab 切换动画完成
WidgetsBinding.instance.addPostFrameCallback((_) {
try {
// 使用 go 而非 pushgo 会切换到对应的 StatefulShellBranch
appRouter.go(AppRoutes.profile);
// 延迟弹出搜索浮层,确保 Tab 切换完成
Future.delayed(const Duration(milliseconds: 500), () {
final ctx = rootNavigatorKey.currentContext;
if (ctx != null && ctx.mounted) {
SpotlightSearchOverlay.show(ctx, ref);
}
});
} catch (e) {
Log.e('🚀 [QuickActions] 搜索快捷方式处理失败', e);
}
});
}
Future<void> _initHttpCache() async {
try {
await ApiClient.instance.initCache();
} catch (e) {
Log.e('HTTP缓存初始化失败', e);
}
}
void _initDataManagementChannel() {
if (pu.isWeb) return;
// 鸿蒙端暂不支持数据管理通道,跳过初始化避免 MissingPluginException
if (pu.isOhos) return;
_dataManagementChannel.setMethodCallHandler((call) async {
switch (call.method) {
case 'open_data_management':
_navigateToDataManagement();
case 'navigate_to_cache_management':
_navigateToCacheManagement();
case 'navigate_to_data_management':
_navigateToDataManagement();
case 'clear_all_data':
await _clearAllAppData();
}
});
_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 _navigateToCacheManagement() {
Future.delayed(const Duration(milliseconds: 300), () {
final context = rootNavigatorKey.currentContext;
if (context == null) {
Log.w('📦 [DataManagement] context不可用延迟导航到缓存管理');
return;
}
Log.i('📦 [DataManagement] 导航到缓存管理页面');
// ignore: use_build_context_synchronously
if (!context.mounted) return;
if (pu.isOhos) {
OhosNavBridge.push(context, AppRoutes.cacheManagement);
} else {
appRouter.push(AppRoutes.cacheManagement);
}
}).catchError((_) {});
}
/// 一键清理所有应用数据(从原生管理空间对话框触发)
Future<void> _clearAllAppData() async {
try {
Log.i('📦 [DataManagement] 开始一键清理所有应用数据');
final db = AppDatabase.instance;
await db.clearAllFavorites();
await db.clearAllReadHistory();
await db.clearAllNotes();
await db.clearAllShareHistory();
await db.clearFeedCache();
await db.clearAllHanziCache();
await db.clearOfflineQueue();
ref.read(generalSettingsProvider.notifier).clearCache();
Log.i('📦 [DataManagement] 一键清理完成');
// 清理完成后显示提示
Future.delayed(const Duration(milliseconds: 300), () {
final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted) {
AppToast.showSuccess('所有数据已清理完成');
}
});
} catch (e) {
Log.e('📦 [DataManagement] 一键清理失败', e);
Future.delayed(const Duration(milliseconds: 300), () {
final context = rootNavigatorKey.currentContext;
if (context != null && context.mounted) {
AppToast.showError('清理失败,请重试');
}
});
}
}
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);
WindowsPlatformService.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);
WindowsPlatformService.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,
),
);
},
);
final useGlass = PlatformCapabilities.supports(
CapabilityKey.liquidGlass,
);
return useGlass
? 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,
)
: materialApp;
}
final iconMode = generalSettings.iconMode;
final isLocked = ref.watch(
appLockProvider.select((s) => s.isLocked),
);
return AppIconModeScope(
mode: iconMode,
child: 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,
);
}
}