主要变更: 1. 新增桌面端托盘图标支持深色/浅色主题切换 2. 重构应用锁、动画配置、小组件导航服务职责 3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题 4. 优化诗词服务、阅读进度、搜索结果空状态体验 5. 完善macOS打包配置与错误静默处理逻辑 6. 新增快速卡片多语言适配与动画退出队列管理
574 lines
18 KiB
Dart
574 lines
18 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 应用根组件
|
||
/// 创建时间: 2026-04-20
|
||
/// 更新时间: 2026-06-22
|
||
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay
|
||
/// 上次更新: 深度职责分离,提取 QuickActionsHandler/WidgetNavigationService/AppLockLifecycleService,ThemeSettingsState.toThemeData()
|
||
/// ============================================================
|
||
|
||
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_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 'services/theme_sync_service.dart';
|
||
import 'services/animation_config_service.dart';
|
||
import 'services/data_management_service.dart';
|
||
import 'services/desktop_services_manager.dart';
|
||
import 'services/quick_actions_handler.dart';
|
||
import 'services/widget_navigation_service.dart';
|
||
import 'services/app_lock_lifecycle_service.dart';
|
||
import '../core/services/accessibility_service.dart';
|
||
import '../core/services/ui/status_bar_service.dart';
|
||
import '../core/router/app_router.dart' show appRouter, rootNavigatorKey;
|
||
import 'layout/ohos_app_shell.dart';
|
||
import '../core/services/performance/app_lifecycle_gate.dart';
|
||
import '../core/services/device/app_lock_service.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/desktop/macos_menu_bar_wrapper.dart';
|
||
import '../features/template/services/wallpaper_preload_service.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 '../shared/widgets/adaptive/keyboard_back_handler.dart';
|
||
import '../shared/widgets/feedback/app_toast.dart';
|
||
import '../shared/widgets/display/app_icon.dart';
|
||
import '../core/services/device/quick_actions_service.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 {
|
||
// ============================================================
|
||
// 服务实例
|
||
// ============================================================
|
||
|
||
late final ThemeSyncService _themeSyncService;
|
||
late final AnimationConfigService _animationConfigService;
|
||
late final DataManagementService _dataManagementService;
|
||
late final DesktopServicesManager _desktopServicesManager;
|
||
late final QuickActionsHandler _quickActionsHandler;
|
||
late final WidgetNavigationService _widgetNavigationService;
|
||
late final AppLockLifecycleService _appLockLifecycleService;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
initLifecycleGate();
|
||
|
||
// 初始化服务
|
||
_themeSyncService = ThemeSyncService();
|
||
_animationConfigService = AnimationConfigService();
|
||
_dataManagementService = DataManagementService(ref);
|
||
_desktopServicesManager = DesktopServicesManager(ref);
|
||
_quickActionsHandler = QuickActionsHandler(ref);
|
||
_widgetNavigationService = WidgetNavigationService();
|
||
_appLockLifecycleService = AppLockLifecycleService(ref);
|
||
|
||
// 初始化各服务
|
||
_quickActionsHandler.initialize();
|
||
_initWallpaperPreload();
|
||
_dataManagementService.initialize();
|
||
_initHttpCache();
|
||
_initAccessibility();
|
||
_desktopServicesManager.initialize();
|
||
}
|
||
|
||
// ============================================================
|
||
// 初始化方法
|
||
// ============================================================
|
||
|
||
/// WiFi环境下预加载壁纸缩略图
|
||
void _initWallpaperPreload() {
|
||
Future.microtask(() async {
|
||
try {
|
||
await WallpaperPreloadService.preloadIfNeeded();
|
||
} catch (e) {
|
||
Log.e('壁纸预加载初始化失败', e);
|
||
}
|
||
}).catchError((_) {});
|
||
}
|
||
|
||
/// 初始化HTTP缓存
|
||
Future<void> _initHttpCache() async {
|
||
try {
|
||
await ApiClient.instance.initCache();
|
||
} catch (e) {
|
||
Log.e('HTTP缓存初始化失败', e);
|
||
}
|
||
}
|
||
|
||
/// 初始化无障碍服务
|
||
void _initAccessibility() {
|
||
final generalSettings = ref.read(generalSettingsProvider);
|
||
AccessibilityService.instance.initSemanticsDebug(
|
||
generalSettings.semanticsDebugEnabled,
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 生命周期
|
||
// ============================================================
|
||
|
||
@override
|
||
void dispose() {
|
||
_desktopServicesManager.dispose().catchError((Object e) {
|
||
Log.e('桌面端服务销毁失败', e);
|
||
});
|
||
disposeLifecycleGate();
|
||
disposeReadlaterRefreshController();
|
||
disposeFavoriteRefreshController();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
super.didChangeAppLifecycleState(state);
|
||
switch (state) {
|
||
case AppLifecycleState.paused:
|
||
_appLockLifecycleService.onAppPaused();
|
||
case AppLifecycleState.resumed:
|
||
Future.microtask(() {
|
||
_appLockLifecycleService.onAppResumed();
|
||
_widgetNavigationService.handlePendingNavigation();
|
||
_dataManagementService.handlePendingNavigation();
|
||
QuickActionsService.handlePendingAction();
|
||
});
|
||
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;
|
||
_themeSyncService.syncFromSystemBrightness(
|
||
isDark: isDark,
|
||
trayController: _desktopServicesManager.trayController,
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void didChangeLocales(List<Locale>? locales) {
|
||
super.didChangeLocales(locales);
|
||
if (locales != null && locales.isNotEmpty) {
|
||
ref.read(systemLocaleVersionProvider.notifier).increment();
|
||
Log.i('系统语言变化: ${locales.first.toLanguageTag()}');
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 构建
|
||
// ============================================================
|
||
|
||
@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.instance.updateFromContext(context);
|
||
|
||
// 更新全局动画配置(去重)
|
||
_animationConfigService.updateSettings(
|
||
enabled: settings.animationEnabled,
|
||
intensity: settings.animationIntensity,
|
||
);
|
||
|
||
// 计算有效字体
|
||
final builtInFontFamilies = fontStyleOptions
|
||
.map((opt) => opt.fontFamily)
|
||
.toSet();
|
||
final effectiveFontFamily = settings.customFontFamily.isNotEmpty
|
||
? settings.customFontFamily
|
||
: (!builtInFontFamilies.contains(fontState.activeFontFamily)
|
||
? fontState.activeFontFamily
|
||
: settings.fontStyle.fontFamily);
|
||
|
||
// 使用 ThemeSettingsState.toThemeData() 构建主题
|
||
final theme = settings.toThemeData(
|
||
fontFamily: effectiveFontFamily,
|
||
highContrast: generalSettings.highContrastEnabled,
|
||
colorWeakTypeId: generalSettings.colorWeakTypeId,
|
||
);
|
||
final darkTheme = settings.toThemeData(
|
||
fontFamily: effectiveFontFamily,
|
||
highContrast: generalSettings.highContrastEnabled,
|
||
colorWeakTypeId: generalSettings.colorWeakTypeId,
|
||
isDark: true,
|
||
);
|
||
|
||
final themeMode = _resolveThemeMode(settings.themeMode);
|
||
|
||
// 计算有效暗色状态并同步到桌面端(去重)
|
||
final effectiveIsDark = switch (themeMode) {
|
||
ThemeMode.dark => true,
|
||
ThemeMode.light => false,
|
||
ThemeMode.system =>
|
||
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||
Brightness.dark,
|
||
};
|
||
_themeSyncService.syncThemeToDesktop(
|
||
isDark: effectiveIsDark,
|
||
isAmoled: settings.isAmoled,
|
||
trayController: _desktopServicesManager.trayController,
|
||
);
|
||
|
||
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) {
|
||
return _buildOhosApp(
|
||
settings: settings,
|
||
appLocale: appLocale,
|
||
supportedLocales: supportedLocales,
|
||
theme: theme,
|
||
darkTheme: darkTheme,
|
||
themeMode: themeMode,
|
||
);
|
||
}
|
||
|
||
return _buildStandardApp(
|
||
settings: settings,
|
||
appLocale: appLocale,
|
||
supportedLocales: supportedLocales,
|
||
theme: theme,
|
||
darkTheme: darkTheme,
|
||
themeMode: themeMode,
|
||
);
|
||
}
|
||
|
||
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)
|
||
_buildOfflineBanner(),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
// ============================================================
|
||
// 平台应用构建
|
||
// ============================================================
|
||
|
||
/// 构建鸿蒙端应用
|
||
Widget _buildOhosApp({
|
||
required ThemeSettingsState settings,
|
||
required Locale appLocale,
|
||
required Iterable<Locale> supportedLocales,
|
||
required ThemeData theme,
|
||
required ThemeData darkTheme,
|
||
required ThemeMode themeMode,
|
||
}) {
|
||
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(),
|
||
],
|
||
builder: (context, widget) {
|
||
final botToastBuilder = BotToastInit();
|
||
AppToast.markInitialized();
|
||
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: _buildGlassThemeData(settings),
|
||
child: ohosMaterialApp,
|
||
)
|
||
: ohosMaterialApp,
|
||
);
|
||
}
|
||
|
||
/// 构建标准端应用(非鸿蒙)
|
||
Widget _buildStandardApp({
|
||
required ThemeSettingsState settings,
|
||
required Locale appLocale,
|
||
required Iterable<Locale> supportedLocales,
|
||
required ThemeData theme,
|
||
required ThemeData darkTheme,
|
||
required ThemeMode themeMode,
|
||
}) {
|
||
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();
|
||
AppToast.markInitialized();
|
||
final botWidget = botToastBuilder(context, widget);
|
||
|
||
return DefaultTextStyle(
|
||
style: const TextStyle(decoration: TextDecoration.none),
|
||
child: StatusBarStyleRegion(
|
||
isDark: settings.isDark,
|
||
child: botWidget,
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
// macOS 原生菜单栏包裹
|
||
final materialAppWithMenuBar = MacosMenuBarWrapper(
|
||
child: materialApp,
|
||
);
|
||
|
||
final useGlass = PlatformCapabilities.supports(
|
||
CapabilityKey.liquidGlass,
|
||
);
|
||
return useGlass
|
||
? GlassTheme(
|
||
data: _buildGlassThemeData(settings),
|
||
child: materialAppWithMenuBar,
|
||
)
|
||
: materialAppWithMenuBar;
|
||
}
|
||
|
||
/// 构建毛玻璃主题数据
|
||
GlassThemeData _buildGlassThemeData(ThemeSettingsState settings) {
|
||
return 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,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 构建离线提示横幅
|
||
Widget _buildOfflineBanner() {
|
||
return 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,
|
||
);
|
||
}
|
||
}
|