Files
xianyan/lib/app/app.dart
Developer 6119918185 release: bump version to 6.6.25+2606241
主要变更:
1. 新增桌面端托盘图标支持深色/浅色主题切换
2. 重构应用锁、动画配置、小组件导航服务职责
3. 修复Riverpod初始化断言、防重复点击、工作台模式残留选中态问题
4. 优化诗词服务、阅读进度、搜索结果空状态体验
5. 完善macOS打包配置与错误静默处理逻辑
6. 新增快速卡片多语言适配与动画退出队列管理
2026-06-24 04:26:50 +08:00

574 lines
18 KiB
Dart
Raw Permalink 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-22
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay
/// 上次更新: 深度职责分离,提取 QuickActionsHandler/WidgetNavigationService/AppLockLifecycleServiceThemeSettingsState.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,
);
}
}