Files
xianyan/lib/app/app.dart
2026-05-22 02:04:46 +08:00

407 lines
14 KiB
Dart

/// ============================================================
/// 闲言APP — 应用根组件
/// 创建时间: 2026-04-20
/// 更新时间: 2026-05-22
/// 作用: MaterialApp.router + Riverpod 主题管理 + GlassTheme + flutter_animate
/// 上次更新: 集成AppLifecycleGate前后台统一管理
/// ============================================================
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:flutter/services.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter_quill/flutter_quill.dart'
show FlutterQuillLocalizations;
import '../core/router/app_router.dart' show appRouter, rootNavigatorKey;
import '../core/layout/ohos_app_shell.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_utils.dart' as pu;
import '../features/mine/settings/providers/theme_settings_provider.dart';
import '../features/mine/settings/presentation/font_management_notifier.dart';
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 {
@override
void initState() {
super.initState();
initLifecycleGate();
}
@override
void dispose() {
disposeLifecycleGate();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.paused:
try {
AppLockService.onAppPaused();
} catch (e) {
Log.e('应用锁暂停处理失败', e);
}
break;
case AppLifecycleState.resumed:
Future.microtask(() {
try {
AppLockService.onAppResumed();
} catch (e) {
Log.e('应用锁恢复处理失败', e);
}
});
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()}');
}
}
@override
Widget build(BuildContext context) {
final settings = ref.watch(themeSettingsProvider);
final fontState = ref.watch(fontManagementProvider);
final appLocale = ref.watch(appLocaleProvider);
final supportedLocales = ref.watch(supportedLocalesProvider);
final textDirection = ref.watch(appTextDirectionProvider);
Animate.defaultDuration = settings.animationEnabled
? Duration(
milliseconds: (300 * settings.animationIntensity.durationMultiplier)
.round(),
)
: Duration.zero;
Animate.defaultCurve = settings.animationIntensity.curve;
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,
);
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,
);
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) {
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(),
builder: (context, widget) {
final botToastBuilder = BotToastInit();
final botWidget = botToastBuilder(context, widget);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: settings.isDark
? Brightness.light
: Brightness.dark,
statusBarBrightness: settings.isDark
? Brightness.dark
: Brightness.light,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: settings.isDark
? Brightness.light
: Brightness.dark,
systemNavigationBarDividerColor: Colors.transparent,
),
child: MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.noScaling),
child: DefaultTextStyle(
style: const TextStyle(),
child: botWidget,
),
),
);
},
);
return InheritedGoRouter(
goRouter: appRouter,
child: liquidGlassReady
? GlassTheme(
data: GlassThemeData(
light: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 30.0,
blur: settings.glassEnabled ? 3.0 : 0.0,
refractiveIndex: 1.65,
lightIntensity: 1.2,
ambientStrength: 0.6,
saturation: 1.2,
),
),
dark: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 40.0,
blur: settings.glassEnabled ? 5.0 : 0.0,
lightIntensity: 1.5,
refractiveIndex: 1.2,
saturation: 1.1,
),
),
),
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();
final botWidget = botToastBuilder(context, widget);
final isDark = settings.isDark;
final wrappedWidget = AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: isDark
? Brightness.light
: Brightness.dark,
statusBarBrightness: isDark
? Brightness.dark
: Brightness.light,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarIconBrightness: isDark
? Brightness.light
: Brightness.dark,
systemNavigationBarDividerColor: Colors.transparent,
),
child: MediaQuery(
data: MediaQuery.of(
context,
).copyWith(textScaler: TextScaler.noScaling),
child: DefaultTextStyle(
style: const TextStyle(),
child: botWidget,
),
),
);
return wrappedWidget;
},
);
return GlassTheme(
data: GlassThemeData(
light: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 30.0,
blur: settings.glassEnabled ? 3.0 : 0.0,
refractiveIndex: 1.65,
lightIntensity: 1.2,
ambientStrength: 0.6,
saturation: 1.2,
),
),
dark: GlassThemeVariant(
settings: GlassThemeSettings(
thickness: 40.0,
blur: settings.glassEnabled ? 5.0 : 0.0,
lightIntensity: 1.5,
refractiveIndex: 1.2,
saturation: 1.1,
),
),
),
child: materialApp,
);
},
),
),
);
}
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,
);
}
}