Files
xianyan/lib/core/services/catcher2_config_service.dart
Developer a5d8483797 chore: 完成多平台兼容性优化与资源更新
本次提交包含多项改进:
1. 新增Android启动页资源与配色配置,完善多版本主题适配
2. 全量替换Platform.pathSeparator为硬编码斜杠,修复Web平台路径兼容问题
3. 为大量文件系统操作添加kIsWeb守卫,优化Web端表现
4. 替换硬编码平台判断为platform_utils封装,统一平台检测逻辑
5. 移除冗余代码与默认参数,优化小部件性能
6. 新增Web端适配逻辑,处理不支持的原生功能
7. 更新鸿蒙兼容性工具,完善平台识别与路径处理
8. 优化设备注册错误捕获,避免非致命崩溃
9. 添加启动页图标与背景配置,优化首屏体验
2026-06-05 02:31:34 +08:00

289 lines
8.5 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 — Catcher2 配置服务
/// 创建时间: 2026-05-21
/// 更新时间: 2026-06-05
/// 作用: 统一管理 Catcher2 异常捕获开关与动态配置更新
/// 上次更新: 修复Zone mismatch不使用runAppFunction手动调用runApp
/// ============================================================
import 'package:catcher_2/catcher_2.dart';
import 'package:catcher_2/model/platform_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show SelectableText;
import 'package:flutter/services.dart';
import '../router/app_router.dart' show rootNavigatorKey;
import '../storage/kv_storage.dart';
import '../utils/logger.dart';
import 'crash_log_service.dart';
class Catcher2ConfigService {
Catcher2ConfigService._();
static final Catcher2ConfigService instance = Catcher2ConfigService._();
Catcher2? _catcher2;
bool _initialized = false;
bool get isInitialized => _initialized;
bool get isEnabled {
return KvStorage.getBool('general_catcher_enabled') ?? true;
}
/// 初始化 Catcher2使用 rootWidget 而非 runAppFunction避免 Zone mismatch
void init({required Widget rootWidget}) {
final enabled = isEnabled;
final debugConfig = enabled
? Catcher2Options(
CopyableDialogReportMode(),
[_ConsoleLogHandler()],
localizationOptions: [
LocalizationOptions.buildDefaultChineseOptions(),
],
)
: Catcher2Options(SilentReportMode(), []);
final releaseConfig = enabled
? Catcher2Options(SilentReportMode(), [_ConsoleLogHandler()])
: Catcher2Options(SilentReportMode(), []);
// 使用 rootWidget 而非 runAppFunctionCatcher2 会在内部调用 runApp
// 但不会创建新的 Zone避免 Zone mismatch 警告
_catcher2 = Catcher2(
rootWidget: rootWidget,
debugConfig: debugConfig,
releaseConfig: releaseConfig,
profileConfig: Catcher2Options(SilentReportMode(), []),
navigatorKey: rootNavigatorKey,
);
_initialized = true;
Log.i(
'Catcher2ConfigService: 初始化完成 (enabled=$enabled, mode=${kDebugMode ? "debug" : "release"})',
);
}
void updateFromSettings(bool enabled) {
if (_catcher2 == null) {
Log.w('Catcher2ConfigService: Catcher2 未初始化,跳过更新');
return;
}
final debugConfig = enabled
? Catcher2Options(
CopyableDialogReportMode(),
[_ConsoleLogHandler()],
localizationOptions: [
LocalizationOptions.buildDefaultChineseOptions(),
],
)
: Catcher2Options(SilentReportMode(), []);
final releaseConfig = enabled
? Catcher2Options(SilentReportMode(), [_ConsoleLogHandler()])
: Catcher2Options(SilentReportMode(), []);
_catcher2!.updateConfig(
debugConfig: debugConfig,
releaseConfig: releaseConfig,
);
Log.i('Catcher2ConfigService: 配置已更新 (enabled=$enabled)');
}
}
class CopyableDialogReportMode extends ReportMode {
@override
void requestAction(Report report, BuildContext? context) {
_showDialog(report, context);
}
Future<void> _showDialog(Report report, BuildContext? context) async {
await Future<void>.delayed(Duration.zero);
if (context != null && context.mounted) {
showCupertinoDialog<void>(
context: context,
builder: (ctx) => _CopyableErrorDialog(
report: report,
onAccept: () {
onActionConfirmed(report);
Navigator.of(ctx).pop();
},
onReject: () {
onActionRejected(report);
Navigator.of(ctx).pop();
},
),
);
}
}
@override
bool isContextRequired() => true;
@override
List<PlatformType> getSupportedPlatforms() => PlatformType.values.toList();
}
class _CopyableErrorDialog extends StatelessWidget {
const _CopyableErrorDialog({
required this.report,
required this.onAccept,
required this.onReject,
});
final Report report;
final VoidCallback onAccept;
final VoidCallback onReject;
String get _errorId {
final ts = report.dateTime.millisecondsSinceEpoch
.toRadixString(36)
.toUpperCase();
return 'ERR-$ts';
}
String get _errorText {
final buffer = StringBuffer();
buffer.writeln('[$_errorId] ${report.dateTime.toIso8601String()}');
buffer.writeln();
buffer.writeln('Error: ${report.error}');
buffer.writeln();
buffer.writeln('Stack Trace:');
buffer.writeln(report.stackTrace);
return buffer.toString();
}
void _copyToClipboard(BuildContext context) {
Clipboard.setData(ClipboardData(text: _errorText));
HapticFeedback.lightImpact();
showCupertinoDialog<void>(
context: context,
barrierDismissible: true,
builder: (ctx) => CupertinoAlertDialog(
content: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
'✅ 已复制到剪贴板\n标识: $_errorId',
style: const TextStyle(fontSize: 14),
),
),
actions: [
CupertinoDialogAction(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('好的'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final errorStr = report.error.toString();
final displayError = errorStr.length > 300
? '${errorStr.substring(0, 300)}...'
: errorStr;
return CupertinoAlertDialog(
title: Column(
children: [
const Text('⚠️ 应用异常'),
const SizedBox(height: 4),
Text(
_errorId,
style: TextStyle(
fontSize: 11,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: SelectableText(
displayError,
style: TextStyle(
fontSize: 12,
color: CupertinoColors.systemRed.resolveFrom(context),
),
),
),
),
const SizedBox(height: 8),
Text(
'时间: ${report.dateTime.toIso8601String()}',
style: TextStyle(
fontSize: 11,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
),
],
),
actions: [
CupertinoDialogAction(
onPressed: () => _copyToClipboard(context),
child: const Text('📋 复制详情'),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: onReject,
child: const Text('忽略'),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: onAccept,
child: const Text('确认'),
),
],
);
}
}
class _ConsoleLogHandler extends ReportHandler {
DateTime? _lastLog;
static const _cooldown = Duration(seconds: 3);
@override
Future<bool> handle(Report report, BuildContext? context) async {
final now = DateTime.now();
final ts = report.dateTime.millisecondsSinceEpoch
.toRadixString(36)
.toUpperCase();
final errorId = 'ERR-$ts';
if (_lastLog == null || now.difference(_lastLog!) > _cooldown) {
_lastLog = now;
Log.e('[$errorId] ${report.error}');
if (kDebugMode) {
debugPrint('──────────────────────────────────────');
debugPrint('[$errorId] Catcher2 异常详情:');
debugPrint('Error: ${report.error}');
debugPrint('Stack: ${report.stackTrace}');
debugPrint('──────────────────────────────────────');
}
}
CrashLogService.instance.addLog(
error: report.error.toString(),
stackTrace: report.stackTrace.toString(),
);
return true;
}
@override
List<PlatformType> getSupportedPlatforms() => PlatformType.values.toList();
@override
bool isContextRequired() => false;
}