389 lines
13 KiB
Dart
389 lines
13 KiB
Dart
/// ============================================================
|
||
// 闲言APP — 应用入口
|
||
// 创建时间: 2026-04-20
|
||
/// 更新时间: 2026-06-19
|
||
/// 作用: main 函数,初始化存储 + 液态玻璃 + 异常捕获 + 启动 App
|
||
/// 上次更新: 移除 main.dart 中的 flutter_acrylic 初始化,统一由 WindowsAcrylicService 管理
|
||
// ============================================================
|
||
|
||
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:isolate'
|
||
if (dart.library.js) 'core/utils/platform/isolate_stub.dart';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||
import 'package:logging/logging.dart';
|
||
import 'package:flutter/services.dart';
|
||
import 'package:window_manager/window_manager.dart';
|
||
|
||
import 'app/app.dart';
|
||
import 'core/constants/app_constants.dart';
|
||
import 'core/services/network/deep_link_service.dart';
|
||
import 'core/router/deep_link_resolver.dart';
|
||
import 'core/services/performance/performance_orchestrator.dart';
|
||
import 'core/services/error/crash_monitor.dart';
|
||
import 'core/storage/kv_storage.dart';
|
||
import 'core/services/error/global_error_handler.dart';
|
||
import 'core/services/post_agreement_initializer.dart';
|
||
import 'core/utils/logger.dart' show Log, LogCategory;
|
||
import 'core/utils/platform/platform_utils.dart' as pu;
|
||
import 'core/utils/platform/platform_capability.dart';
|
||
import 'core/utils/platform/clipboard_bridge.dart';
|
||
import 'core/registry/page_registry.dart';
|
||
import 'core/router/app_router.dart';
|
||
import 'core/services/catcher2_config_service.dart';
|
||
import 'editor/services/3d/platform_3d_service.dart';
|
||
import 'features/template/services/wallpaper_favorite_service.dart';
|
||
import 'features/template/services/wallpaper_health_service.dart';
|
||
import 'features/settings/services/settings_change_logger.dart';
|
||
import 'features/auth/providers/auth_provider.dart';
|
||
import 'features/discover/services/rss_service.dart';
|
||
import 'features/discover/services/exchange_rate_service.dart';
|
||
import 'package:path_provider/path_provider.dart';
|
||
|
||
bool kvStorageReady = false;
|
||
bool _liquidGlassReady = false;
|
||
|
||
bool get liquidGlassReady => _liquidGlassReady;
|
||
|
||
void main() async {
|
||
// Catcher2 内部会调用 runZonedGuarded 并在其中执行 runApp,
|
||
// 因此 binding 必须在 Catcher2 之前初始化(在同一个 root zone),
|
||
// 否则会出现 Zone mismatch 警告。
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
// 移除外层 runZonedGuarded,由 Catcher2 内部管理 zone
|
||
// 异常捕获由 Catcher2 + GlobalErrorHandler 共同处理
|
||
_appMain().catchError((Object e, StackTrace st) {
|
||
Log.e('main初始化失败', e, st, LogCategory.general);
|
||
});
|
||
}
|
||
|
||
/// 应用主入口逻辑
|
||
Future<void> _appMain() async {
|
||
if (pu.isDesktop) {
|
||
await windowManager.ensureInitialized();
|
||
|
||
const windowOptions = WindowOptions(
|
||
minimumSize: Size(400, 600),
|
||
title: '闲言',
|
||
center: true,
|
||
// 隐藏系统标题栏,使用自定义软件样式标题栏(DesktopWindowTitleBar)
|
||
titleBarStyle: TitleBarStyle.hidden,
|
||
// macOS: 隐藏原生红黄绿按钮,由 Flutter 侧自绘
|
||
skipTaskbar: false,
|
||
);
|
||
|
||
// Windows: flutter_acrylic 初始化统一由 WindowsAcrylicService 管理
|
||
// Window.initialize() 和 Window.setEffect() 均在 app.dart 的 _initWindowEffect 中调用
|
||
// 不在此处调用 Window.initialize(),避免重复初始化和布局异常
|
||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||
await windowManager.show();
|
||
});
|
||
}
|
||
|
||
if (pu.isOhos)
|
||
Log.i('🟢 [OHOS] main() 开始执行', null, null, LogCategory.general);
|
||
|
||
// 初始化平台能力注册表(必须在其他服务初始化之前)
|
||
PlatformCapabilities.init();
|
||
Log.i('平台能力注册表初始化完成', null, null, LogCategory.general);
|
||
|
||
// 鸿蒙端安装标准剪贴板拦截器,修复输入框粘贴不工作
|
||
// 原生端EntryAbility.ets拦截flutter/platform通道的Clipboard方法
|
||
ClipboardBridge.installOhosClipboardInterceptor();
|
||
|
||
FlutterError.onError = (FlutterErrorDetails details) {
|
||
final msg = details.exceptionAsString();
|
||
// 布局溢出等非致命错误:仅打印日志,不弹窗/不显示溢出指示器
|
||
if (msg.contains('overflowed')) {
|
||
Log.w(
|
||
'⚠️ FlutterError: RenderFlex overflow (已静默)',
|
||
msg,
|
||
details.stack,
|
||
LogCategory.general,
|
||
);
|
||
return;
|
||
}
|
||
// IOSScrollViewFlingVelocityTracker 时间戳乱序:Flutter框架已知bug,非致命
|
||
// iOS上触摸事件时间戳偶尔乱序导致velocity tracker断言失败,不影响功能
|
||
if (msg.contains('smaller timestamp') && msg.contains('predecessor')) {
|
||
Log.w(
|
||
'⚠️ FlutterError: VelocityTracker timestamp out-of-order (已静默)',
|
||
msg,
|
||
details.stack,
|
||
LogCategory.ui,
|
||
);
|
||
return;
|
||
}
|
||
FlutterError.presentError(details);
|
||
Log.e(
|
||
'🔥 FlutterError.onError',
|
||
details.exceptionAsString(),
|
||
details.stack,
|
||
LogCategory.general,
|
||
);
|
||
};
|
||
|
||
GlobalErrorHandler.init();
|
||
|
||
if (!pu.isWeb) {
|
||
Isolate.current.addErrorListener(
|
||
RawReceivePort((Object? pair) {
|
||
final List<dynamic> errorAndStacktrace = pair as List<dynamic>;
|
||
Log.e(
|
||
'🔥 Isolate uncaught error',
|
||
errorAndStacktrace.first,
|
||
StackTrace.fromString(errorAndStacktrace.last.toString()),
|
||
LogCategory.general,
|
||
);
|
||
}).sendPort,
|
||
);
|
||
}
|
||
|
||
if (!pu.isWeb) {
|
||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||
}
|
||
if (pu.isOhos)
|
||
Log.i('🟢 [OHOS] SystemChrome 配置完成', null, null, LogCategory.general);
|
||
|
||
try {
|
||
await AppVersion.init();
|
||
Log.i('AppVersion 初始化完成', null, null, LogCategory.general);
|
||
} catch (e, st) {
|
||
Log.e('AppVersion 初始化失败', e, st, LogCategory.general);
|
||
}
|
||
|
||
try {
|
||
await KvStorage.init();
|
||
kvStorageReady = true;
|
||
if (pu.isOhos)
|
||
Log.i('🟢 [OHOS] KvStorage 初始化完成', null, null, LogCategory.storage);
|
||
} catch (e, st) {
|
||
Log.e('KV 存储初始化失败', e, st, LogCategory.storage);
|
||
}
|
||
|
||
try {
|
||
// HapticService.init() 已移至 PostAgreementInitializer
|
||
// Vibrate.canVibrate 会触发 flutter_vibrate 原生插件初始化,
|
||
// 未同意隐私协议前不应调用
|
||
Log.i('触觉反馈服务延迟到协议同意后初始化', null, null, LogCategory.haptic);
|
||
} catch (e, st) {
|
||
Log.e('触觉反馈服务初始化失败', e, st, LogCategory.haptic);
|
||
}
|
||
|
||
try {
|
||
await WallpaperFavoriteService.init();
|
||
Log.i('壁纸收藏服务初始化完成', null, null, LogCategory.general);
|
||
} catch (e, st) {
|
||
Log.e('壁纸收藏服务初始化失败', e, st, LogCategory.general);
|
||
}
|
||
|
||
try {
|
||
PerformanceOrchestrator.instance.init();
|
||
Log.i('PerformanceOrchestrator 初始化完成', null, null, LogCategory.service);
|
||
} catch (e, st) {
|
||
Log.e('PerformanceOrchestrator 初始化失败', e, st, LogCategory.service);
|
||
}
|
||
|
||
try {
|
||
await LiquidGlassWidgets.initialize();
|
||
_liquidGlassReady = true;
|
||
Log.i(
|
||
'LiquidGlassWidgets 初始化完成 (ohos=${pu.isOhos})',
|
||
null,
|
||
null,
|
||
LogCategory.ui,
|
||
);
|
||
} catch (e, st) {
|
||
Log.e('LiquidGlassWidgets 初始化失败', e, st, LogCategory.ui);
|
||
}
|
||
|
||
// 抑制 liquid_glass_widgets 内部 lgr 系列日志(FINER 级别高频输出)
|
||
// 仅在 debug 模式下抑制,release 模式本身不会输出这些日志
|
||
_suppressLiquidGlassLogs();
|
||
|
||
try {
|
||
await Platform3DService.instance.detectDeviceCapability();
|
||
if (pu.isOhos)
|
||
Log.i(
|
||
'🟢 [OHOS] 3D设备检测完成 (lowEnd=${Platform3DService.instance.isLowEnd})',
|
||
null,
|
||
null,
|
||
LogCategory.device,
|
||
);
|
||
} catch (e, st) {
|
||
Log.e('3D平台设备检测失败', e, st, LogCategory.device);
|
||
}
|
||
|
||
try {
|
||
_validatePageRegistry();
|
||
} catch (e, st) {
|
||
Log.e('页面注册表验证失败', e, st, LogCategory.general);
|
||
}
|
||
|
||
if (!pu.isWeb) {
|
||
// Debug 模式下验证深度链接配置完整性
|
||
assert(() {
|
||
final errors = DeepLinkResolver.validate();
|
||
if (errors.isNotEmpty) {
|
||
for (final e in errors) {
|
||
Log.e('🔗 [DeepLink] 配置错误: $e', null, null, LogCategory.router);
|
||
}
|
||
} else {
|
||
Log.i('🔗 [DeepLink] 配置验证通过', null, null, LogCategory.router);
|
||
}
|
||
return true;
|
||
}());
|
||
|
||
try {
|
||
await DeepLinkService.init();
|
||
if (pu.isOhos)
|
||
Log.i('🟢 [OHOS] 深度链接服务初始化完成', null, null, LogCategory.router);
|
||
} catch (e, st) {
|
||
Log.e('深度链接服务初始化失败', e, st, LogCategory.router);
|
||
}
|
||
}
|
||
|
||
try {
|
||
await CrashMonitor.instance.init();
|
||
Log.i('崩溃监控服务初始化完成', null, null, LogCategory.service);
|
||
} catch (e, st) {
|
||
Log.e('崩溃监控服务初始化失败', e, st, LogCategory.service);
|
||
}
|
||
|
||
try {
|
||
await WallpaperHealthService.init();
|
||
Log.i('壁纸源健康检测服务初始化完成', null, null, LogCategory.service);
|
||
} catch (e, st) {
|
||
Log.e('壁纸源健康检测服务初始化失败', e, st, LogCategory.service);
|
||
}
|
||
|
||
try {
|
||
await SettingsChangeLogger.init();
|
||
Log.i('设置变更日志服务初始化完成', null, null, LogCategory.service);
|
||
} catch (e, st) {
|
||
Log.e('设置变更日志服务初始化失败', e, st, LogCategory.service);
|
||
}
|
||
|
||
try {
|
||
await RssService.init();
|
||
Log.i('RSS服务初始化完成', null, null, LogCategory.network);
|
||
} catch (e, st) {
|
||
Log.e('RSS服务初始化失败', e, st, LogCategory.network);
|
||
}
|
||
|
||
try {
|
||
await ExchangeRateService.init();
|
||
Log.i('汇率缓存服务初始化完成', null, null, LogCategory.network);
|
||
} catch (e, st) {
|
||
Log.e('汇率缓存服务初始化失败', e, st, LogCategory.network);
|
||
}
|
||
|
||
if (PostAgreementInitializer.shouldInit()) {
|
||
Log.i('检测到老用户已完成引导,立即初始化权限敏感服务', null, null, LogCategory.general);
|
||
// 通知原生端协议已同意(确保老用户升级后原生端也注册敏感插件)
|
||
await _notifyNativeAgreementForExistingUsers();
|
||
try {
|
||
await PostAgreementInitializer.init();
|
||
} catch (e, st) {
|
||
Log.e('老用户权限敏感服务初始化失败', e, st, LogCategory.general);
|
||
}
|
||
} else {
|
||
Log.i('新用户未同意协议,权限敏感服务延迟到协议同意后初始化', null, null, LogCategory.onboarding);
|
||
}
|
||
|
||
// 初始化 Catcher2 异常捕获并启动应用
|
||
// 使用 rootWidget 方式,Catcher2 内部调用 runApp,避免 Zone mismatch
|
||
// 预先获取临时目录作为截图保存路径,避免 Catcher2 输出 "Screenshots path is empty" 警告
|
||
String catcherScreenshotsPath = '';
|
||
try {
|
||
final tempDir = await getTemporaryDirectory();
|
||
catcherScreenshotsPath = '${tempDir.path}/catcher_screenshots';
|
||
} catch (e) {
|
||
// 获取失败时保持空字符串,Catcher2 会输出警告但不影响功能
|
||
}
|
||
|
||
if (_liquidGlassReady) {
|
||
Catcher2ConfigService.instance.init(
|
||
screenshotsPath: catcherScreenshotsPath,
|
||
rootWidget: LiquidGlassWidgets.wrap(
|
||
child: ProviderScope(
|
||
overrides: [
|
||
authStateProvider.overrideWith((ref) => ref.watch(authProvider)),
|
||
logoutProvider.overrideWith(
|
||
(ref) =>
|
||
() => ref.read(authProvider.notifier).logout(),
|
||
),
|
||
],
|
||
child: const XianyanApp(),
|
||
),
|
||
),
|
||
);
|
||
} else {
|
||
Catcher2ConfigService.instance.init(
|
||
screenshotsPath: catcherScreenshotsPath,
|
||
rootWidget: ProviderScope(
|
||
overrides: [
|
||
authStateProvider.overrideWith((ref) => ref.watch(authProvider)),
|
||
logoutProvider.overrideWith(
|
||
(ref) =>
|
||
() => ref.read(authProvider.notifier).logout(),
|
||
),
|
||
],
|
||
child: const XianyanApp(),
|
||
),
|
||
);
|
||
}
|
||
|
||
if (pu.isOhos) {
|
||
Log.i('🟢 [OHOS] runApp() 已调用', null, null, LogCategory.general);
|
||
}
|
||
}
|
||
|
||
void _validatePageRegistry() {
|
||
final errors = PageRegistry.validateAll();
|
||
if (errors.isNotEmpty) {
|
||
for (final e in errors) {
|
||
Log.e('页面注册表验证失败: $e', null, null, LogCategory.general);
|
||
}
|
||
} else {
|
||
Log.i(
|
||
'页面注册表验证通过,共 ${PageRegistry.pageCount} 个页面已注册',
|
||
null,
|
||
null,
|
||
LogCategory.general,
|
||
);
|
||
}
|
||
|
||
const route = AppRoutes.home;
|
||
if (!PageRegistry.isRouteRegistered(route)) {
|
||
Log.e('页面注册表验证: 首页路由 $route 未注册!', null, null, LogCategory.general);
|
||
}
|
||
}
|
||
|
||
/// 通知原生端协议已同意(老用户升级后确保原生端注册敏感插件)
|
||
Future<void> _notifyNativeAgreementForExistingUsers() async {
|
||
if (!Platform.isAndroid) return;
|
||
try {
|
||
const channel = MethodChannel('apps.xy.xianyan/agreement');
|
||
await channel.invokeMethod<bool>('markAgreementAccepted');
|
||
Log.i('老用户原生端协议通知成功', null, null, LogCategory.general);
|
||
} catch (e) {
|
||
Log.e('老用户原生端协议通知失败', e, null, LogCategory.general);
|
||
}
|
||
}
|
||
|
||
/// 抑制 liquid_glass_widgets 内部 lgr 系列高频日志
|
||
/// lgr.geometry / lgr.render / lgr.render.layer 在 debug 模式下
|
||
/// 每帧输出大量 FINER 级别日志,严重影响控制台可读性
|
||
void _suppressLiquidGlassLogs() {
|
||
// 启用层级日志控制,允许单独设置子 logger 的级别
|
||
hierarchicalLoggingEnabled = true;
|
||
|
||
// 将 lgr 根 logger 级别设为 WARNING,FINER/FINE/INFO 级别日志不再输出
|
||
Logger('lgr').level = Level.WARNING;
|
||
}
|