Files
xianyan/lib/app/app.dart

575 lines
19 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-05-31
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate + AppLockOverlay
/// 上次更新: 移除DataSyncEventBus.instance.dispose()避免Hot Restart后单例失效
/// ============================================================
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/data/home_widget_service.dart';
import '../core/services/ui/status_bar_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();
}
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 {
appRouter.go(route);
}
},
);
}
Future<void> _initHttpCache() async {
try {
await ApiClient.instance.initCache();
} catch (e) {
Log.e('HTTP缓存初始化失败', e);
}
}
void _initDataManagementChannel() {
if (pu.isWeb) 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 (pu.isOhos) {
OhosNavBridge.push(context, AppRoutes.dataManagement);
} else {
appRouter.go(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 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.go(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);
// 修复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);
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,
);
}
}