407 lines
14 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|