本次更新包含多项功能优化与兼容性修复: 1. iOS/鸿蒙端添加加密出口合规配置,跳过App Store审核问卷 2. 新增学习计划设置页路由与国际化支持 3. 修复鸿蒙端剪贴板粘贴不工作问题,安装标准剪贴板拦截器 4. 优化收藏功能:兼容复合ID、添加状态同步与触觉反馈 5. 修复鸿蒙端相册保存兼容性,统一使用系统分享降级方案 6. 优化搜索快捷方式跳转逻辑,避免白屏问题 7. 更新本地化资源,新增闲情逸致、学习计划等模块翻译 8. 修复节气日期表排序与跨年边界问题 9. 优化设备信息页面显示,新增系统版本号展示 10. 重构文件传输二维码逻辑,使用纯URL提升兼容性 11. 优化设置项布局,避免文本溢出问题 12. 修复登录页记住账户功能,新增隐私协议守卫 13. 更新macOS依赖库,替换flutter_secure_storage为darwin版本
762 lines
26 KiB
Dart
762 lines
26 KiB
Dart
/// ============================================================
|
||
/// 闲言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 Tab(go 而非 push,避免在 shell 外创建新页面)
|
||
// 2. 延迟弹出搜索浮层,等待 Tab 切换动画完成
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
try {
|
||
// 使用 go 而非 push:go 会切换到对应的 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,
|
||
);
|
||
}
|
||
}
|