575 lines
19 KiB
Dart
575 lines
19 KiB
Dart
/// ============================================================
|
||
/// 闲言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,
|
||
);
|
||
}
|
||
}
|