mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-01 16:48:16 +08:00
refa: logfile (#1764)
* refa: logfile * opt: log page * opt: raf log file * remove old log * update Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me> --------- Co-authored-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
committed by
GitHub
parent
255e39b709
commit
0d273f6909
@@ -10,12 +10,12 @@ import 'package:PiliPlus/plugin/pl_player/controller.dart';
|
|||||||
import 'package:PiliPlus/router/app_pages.dart';
|
import 'package:PiliPlus/router/app_pages.dart';
|
||||||
import 'package:PiliPlus/services/account_service.dart';
|
import 'package:PiliPlus/services/account_service.dart';
|
||||||
import 'package:PiliPlus/services/download/download_service.dart';
|
import 'package:PiliPlus/services/download/download_service.dart';
|
||||||
import 'package:PiliPlus/services/logger.dart';
|
|
||||||
import 'package:PiliPlus/services/service_locator.dart';
|
import 'package:PiliPlus/services/service_locator.dart';
|
||||||
import 'package:PiliPlus/utils/app_scheme.dart';
|
import 'package:PiliPlus/utils/app_scheme.dart';
|
||||||
import 'package:PiliPlus/utils/cache_manager.dart';
|
import 'package:PiliPlus/utils/cache_manager.dart';
|
||||||
import 'package:PiliPlus/utils/calc_window_position.dart';
|
import 'package:PiliPlus/utils/calc_window_position.dart';
|
||||||
import 'package:PiliPlus/utils/date_utils.dart';
|
import 'package:PiliPlus/utils/date_utils.dart';
|
||||||
|
import 'package:PiliPlus/utils/json_file_handler.dart';
|
||||||
import 'package:PiliPlus/utils/page_utils.dart';
|
import 'package:PiliPlus/utils/page_utils.dart';
|
||||||
import 'package:PiliPlus/utils/path_utils.dart';
|
import 'package:PiliPlus/utils/path_utils.dart';
|
||||||
import 'package:PiliPlus/utils/request_utils.dart';
|
import 'package:PiliPlus/utils/request_utils.dart';
|
||||||
@@ -175,15 +175,14 @@ void main() async {
|
|||||||
// 异常捕获 logo记录
|
// 异常捕获 logo记录
|
||||||
final customParameters = {
|
final customParameters = {
|
||||||
'BuildConfig':
|
'BuildConfig':
|
||||||
'''\n
|
'\nBuild Time: ${DateFormatUtils.format(BuildConfig.buildTime, format: DateFormatUtils.longFormatDs)}\n'
|
||||||
Build Time: ${DateFormatUtils.format(BuildConfig.buildTime, format: DateFormatUtils.longFormatDs)}
|
'Commit Hash: ${BuildConfig.commitHash}',
|
||||||
Commit Hash: ${BuildConfig.commitHash}''',
|
|
||||||
};
|
};
|
||||||
final fileHandler = FileHandler(await LoggerUtils.getLogsPath());
|
final fileHandler = await JsonFileHandler.init();
|
||||||
final Catcher2Options debugConfig = Catcher2Options(
|
final Catcher2Options debugConfig = Catcher2Options(
|
||||||
SilentReportMode(),
|
SilentReportMode(),
|
||||||
[
|
[
|
||||||
fileHandler,
|
?fileHandler,
|
||||||
ConsoleHandler(
|
ConsoleHandler(
|
||||||
enableDeviceParameters: false,
|
enableDeviceParameters: false,
|
||||||
enableApplicationParameters: false,
|
enableApplicationParameters: false,
|
||||||
@@ -196,7 +195,7 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
|||||||
final Catcher2Options releaseConfig = Catcher2Options(
|
final Catcher2Options releaseConfig = Catcher2Options(
|
||||||
SilentReportMode(),
|
SilentReportMode(),
|
||||||
[
|
[
|
||||||
fileHandler,
|
?fileHandler,
|
||||||
ConsoleHandler(enableCustomParameters: true),
|
ConsoleHandler(enableCustomParameters: true),
|
||||||
],
|
],
|
||||||
customParameters: customParameters,
|
customParameters: customParameters,
|
||||||
@@ -205,7 +204,7 @@ Commit Hash: ${BuildConfig.commitHash}''',
|
|||||||
Catcher2(
|
Catcher2(
|
||||||
debugConfig: debugConfig,
|
debugConfig: debugConfig,
|
||||||
releaseConfig: releaseConfig,
|
releaseConfig: releaseConfig,
|
||||||
runAppFunction: () => runApp(const MyApp()),
|
rootWidget: const MyApp(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import 'dart:io';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||||
import 'package:PiliPlus/services/logger.dart';
|
import 'package:PiliPlus/services/logger.dart';
|
||||||
|
import 'package:PiliPlus/utils/date_utils.dart';
|
||||||
import 'package:PiliPlus/utils/page_utils.dart';
|
import 'package:PiliPlus/utils/page_utils.dart';
|
||||||
import 'package:PiliPlus/utils/storage.dart';
|
import 'package:PiliPlus/utils/storage.dart';
|
||||||
import 'package:PiliPlus/utils/storage_key.dart';
|
import 'package:PiliPlus/utils/storage_key.dart';
|
||||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:flutter/foundation.dart' show kDebugMode;
|
import 'package:catcher_2/model/platform_type.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
|
||||||
@@ -19,13 +22,9 @@ class LogsPage extends StatefulWidget {
|
|||||||
State<LogsPage> createState() => _LogsPageState();
|
State<LogsPage> createState() => _LogsPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _LogInfo = ({Object? date, String body});
|
|
||||||
|
|
||||||
class _LogsPageState extends State<LogsPage> {
|
class _LogsPageState extends State<LogsPage> {
|
||||||
late File logsPath;
|
List<Report> logsContent = [];
|
||||||
late String fileContent;
|
Report? latestLog;
|
||||||
List<_LogInfo> logsContent = [];
|
|
||||||
DateTime? latestLog;
|
|
||||||
late bool enableLog = Pref.enableLog;
|
late bool enableLog = Pref.enableLog;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -37,7 +36,8 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (latestLog != null) {
|
if (latestLog != null) {
|
||||||
if (DateTime.now().difference(latestLog!) >= const Duration(days: 14)) {
|
final time = latestLog!.dateTime;
|
||||||
|
if (DateTime.now().difference(time) >= const Duration(days: 14)) {
|
||||||
LoggerUtils.clearLogs();
|
LoggerUtils.clearLogs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,56 +45,35 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getLog() async {
|
Future<void> getLog() async {
|
||||||
logsPath = await LoggerUtils.getLogsPath();
|
final logsPath = await LoggerUtils.getLogsPath();
|
||||||
fileContent = await logsPath.readAsString();
|
logsContent = (await logsPath.readAsLines()).reversed.map((i) {
|
||||||
logsContent = parseLogs(fileContent);
|
try {
|
||||||
setState(() {});
|
final log = Report.fromJson(jsonDecode(i));
|
||||||
}
|
latestLog ??= log.copyWith();
|
||||||
|
return log;
|
||||||
List<_LogInfo> parseLogs(String fileContent) {
|
} catch (e, s) {
|
||||||
const String splitToken =
|
return Report(
|
||||||
'======================================================================';
|
'Parse log failed: $e\n\n\n$i',
|
||||||
List contentList = fileContent.split(splitToken).map((item) {
|
s,
|
||||||
return item
|
DateTime.now(),
|
||||||
.replaceAll(
|
const {},
|
||||||
'============================== CATCHER 2 LOG ==============================',
|
const {},
|
||||||
'${Constants.appName}错误日志\n********************',
|
const {},
|
||||||
)
|
null,
|
||||||
.replaceAll('DEVICE INFO', '设备信息')
|
PlatformType.unknown,
|
||||||
.replaceAll('APP INFO', '应用信息')
|
);
|
||||||
.replaceAll('ERROR', '错误信息')
|
|
||||||
.replaceAll('STACK TRACE', '错误堆栈');
|
|
||||||
}).toList();
|
|
||||||
List<_LogInfo> result = [];
|
|
||||||
for (String i in contentList) {
|
|
||||||
Object? date;
|
|
||||||
String body = i
|
|
||||||
.split("\n")
|
|
||||||
.map((l) {
|
|
||||||
if (l.startsWith("Crash occurred on")) {
|
|
||||||
try {
|
|
||||||
date = DateTime.parse(
|
|
||||||
l.split("Crash occurred on")[1].trim(),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (kDebugMode) debugPrint(e.toString());
|
|
||||||
date = l.toString();
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return l;
|
|
||||||
})
|
|
||||||
.where((l) => l.replaceAll("\n", "").trim().isNotEmpty)
|
|
||||||
.join("\n");
|
|
||||||
if (date != null || body != '') {
|
|
||||||
result.add((date: date, body: body));
|
|
||||||
}
|
}
|
||||||
|
}).toList();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
return result.reversed.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void copyLogs() {
|
void copyLogs() {
|
||||||
Utils.copyText('```\n$fileContent\n```', needToast: false);
|
Utils.copyText(
|
||||||
|
'```\n${logsContent.join('\n\n')}```',
|
||||||
|
needToast: false,
|
||||||
|
);
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('复制成功')),
|
const SnackBar(content: Text('复制成功')),
|
||||||
@@ -140,9 +119,23 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
clearLogsHandle();
|
clearLogsHandle();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (kDebugMode) {
|
||||||
|
Timer.periodic(const Duration(milliseconds: 3500), (timer) {
|
||||||
|
Utils.reportError('Manual');
|
||||||
|
if (timer.tick > 3) {
|
||||||
|
timer.cancel();
|
||||||
|
if (mounted) getLog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||||
|
if (kDebugMode)
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'assert',
|
||||||
|
child: Text('引发错误'),
|
||||||
|
),
|
||||||
PopupMenuItem<String>(
|
PopupMenuItem<String>(
|
||||||
value: 'log',
|
value: 'log',
|
||||||
child: Text('${enableLog ? '关闭' : '开启'}日志'),
|
child: Text('${enableLog ? '关闭' : '开启'}日志'),
|
||||||
@@ -165,78 +158,370 @@ class _LogsPageState extends State<LogsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: logsContent.isNotEmpty
|
body: logsContent.isNotEmpty
|
||||||
? ListView.separated(
|
? Padding(
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: padding.left,
|
left: padding.left + 12,
|
||||||
right: padding.right,
|
right: padding.right + 12,
|
||||||
bottom: padding.bottom + 100,
|
|
||||||
),
|
),
|
||||||
itemCount: logsContent.length,
|
child: CustomScrollView(
|
||||||
itemBuilder: (context, index) {
|
slivers: [
|
||||||
final log = logsContent[index];
|
if (latestLog != null)
|
||||||
if (log.date case DateTime date) {
|
SliverToBoxAdapter(
|
||||||
latestLog ??= date;
|
child: Padding(
|
||||||
}
|
padding: const .only(bottom: 12),
|
||||||
return Padding(
|
child: InfoCard(report: latestLog!),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
spacing: 5,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
spacing: 10,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
log.date.toString(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.titleMedium!.fontSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton.icon(
|
|
||||||
style: TextButton.styleFrom(
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Utils.copyText(
|
|
||||||
'```\n${log.body}\n```',
|
|
||||||
needToast: false,
|
|
||||||
);
|
|
||||||
if (context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'已将 ${log.date} 复制至剪贴板',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.copy_outlined, size: 16),
|
|
||||||
label: const Text('复制'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Card(
|
),
|
||||||
child: Padding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: EdgeInsets.only(bottom: padding.bottom + 100),
|
||||||
child: SelectableText(log.body),
|
sliver: SliverList.separated(
|
||||||
),
|
itemCount: logsContent.length,
|
||||||
),
|
itemBuilder: (context, index) =>
|
||||||
],
|
ReportCard(report: logsContent[index]),
|
||||||
|
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
],
|
||||||
},
|
|
||||||
separatorBuilder: (context, index) => const Divider(
|
|
||||||
indent: 12,
|
|
||||||
endIndent: 12,
|
|
||||||
height: 24,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: scrollErrorWidget(),
|
: scrollErrorWidget(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class InfoCard extends StatelessWidget {
|
||||||
|
final Report report;
|
||||||
|
|
||||||
|
const InfoCard({super.key, required this.report});
|
||||||
|
|
||||||
|
Widget _buildMapSection(
|
||||||
|
Color color,
|
||||||
|
String title,
|
||||||
|
Map<String, dynamic> map,
|
||||||
|
) {
|
||||||
|
if (map.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
spacing: 4,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...map.entries.map(
|
||||||
|
(entry) => Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '• ${entry.key}: ',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: entry.value.toString(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = ColorScheme.of(context);
|
||||||
|
return _card([
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 22,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'相关信息',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
report.isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
report.isExpanded = !report.isExpanded;
|
||||||
|
(context as Element).markNeedsBuild();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (report.isExpanded) ...[
|
||||||
|
_buildMapSection(
|
||||||
|
colorScheme.primary,
|
||||||
|
'设备信息',
|
||||||
|
report.deviceParameters,
|
||||||
|
),
|
||||||
|
_buildMapSection(
|
||||||
|
colorScheme.primary,
|
||||||
|
'应用信息',
|
||||||
|
report.applicationParameters,
|
||||||
|
),
|
||||||
|
_buildMapSection(
|
||||||
|
colorScheme.primary,
|
||||||
|
'编译信息',
|
||||||
|
report.customParameters,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReportCard extends StatelessWidget {
|
||||||
|
final Report report;
|
||||||
|
|
||||||
|
const ReportCard({super.key, required this.report});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = ColorScheme.of(context);
|
||||||
|
late final stackTrace = report.stackTrace.toString().trim();
|
||||||
|
final dateTime = DateFormatUtils.longFormatDs.format(report.dateTime);
|
||||||
|
return _card([
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: colorScheme.error, size: 22),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
report.error.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
foregroundColor: colorScheme.secondary,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Utils.copyText('```\n$report```', needToast: false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('已将 $dateTime 复制至剪贴板')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.copy_outlined,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
label: const Text('复制'),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
report.isExpanded ? Icons.expand_less : Icons.expand_more,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
report.isExpanded = !report.isExpanded;
|
||||||
|
(context as Element).markNeedsBuild();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
spacing: 4,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.access_time,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.outline,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
dateTime,
|
||||||
|
style: TextStyle(
|
||||||
|
height: 1.2,
|
||||||
|
color: colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (report.isExpanded) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'错误详情',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.error,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outline.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SelectableText(
|
||||||
|
report.error.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Monospace',
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// stackTrace may be null or String("null") or blank
|
||||||
|
if (stackTrace.isNotEmpty && stackTrace != 'null') ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'堆栈跟踪',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.error,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outline.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SelectableText(
|
||||||
|
stackTrace,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'Monospace',
|
||||||
|
fontSize: 13,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _card(List<Widget> contents) {
|
||||||
|
return Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const .all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: .stretch,
|
||||||
|
children: contents,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Report {
|
||||||
|
Report(
|
||||||
|
this.error,
|
||||||
|
this.stackTrace,
|
||||||
|
this.dateTime,
|
||||||
|
this.deviceParameters,
|
||||||
|
this.applicationParameters,
|
||||||
|
this.customParameters,
|
||||||
|
this.errorDetails,
|
||||||
|
this.platformType,
|
||||||
|
);
|
||||||
|
|
||||||
|
final dynamic error;
|
||||||
|
|
||||||
|
final dynamic stackTrace;
|
||||||
|
|
||||||
|
final DateTime dateTime;
|
||||||
|
|
||||||
|
final Map<String, dynamic> deviceParameters;
|
||||||
|
|
||||||
|
final Map<String, dynamic> applicationParameters;
|
||||||
|
|
||||||
|
final Map<String, dynamic> customParameters;
|
||||||
|
|
||||||
|
final FlutterErrorDetails? errorDetails;
|
||||||
|
|
||||||
|
final PlatformType platformType;
|
||||||
|
|
||||||
|
bool isExpanded = false;
|
||||||
|
|
||||||
|
factory Report.fromJson(Map<String, dynamic> json) => Report(
|
||||||
|
json['error'],
|
||||||
|
json['stackTrace'],
|
||||||
|
DateTime.tryParse(json['dateTime'] ?? '') ?? DateTime(1970),
|
||||||
|
json['deviceParameters'] ?? const {},
|
||||||
|
json['applicationParameters'] ?? const {},
|
||||||
|
json['customParameters'] ?? const {},
|
||||||
|
null,
|
||||||
|
PlatformType.values.byName(json['platformType']),
|
||||||
|
);
|
||||||
|
|
||||||
|
Report copyWith({
|
||||||
|
dynamic error,
|
||||||
|
dynamic stackTrace,
|
||||||
|
DateTime? dateTime,
|
||||||
|
Map<String, dynamic>? deviceParameters,
|
||||||
|
Map<String, dynamic>? applicationParameters,
|
||||||
|
Map<String, dynamic>? customParameters,
|
||||||
|
FlutterErrorDetails? errorDetails,
|
||||||
|
PlatformType? platformType,
|
||||||
|
}) {
|
||||||
|
return Report(
|
||||||
|
error ?? this.error,
|
||||||
|
stackTrace ?? this.stackTrace,
|
||||||
|
dateTime ?? this.dateTime,
|
||||||
|
deviceParameters ?? this.deviceParameters,
|
||||||
|
applicationParameters ?? this.applicationParameters,
|
||||||
|
customParameters ?? this.customParameters,
|
||||||
|
errorDetails ?? this.errorDetails,
|
||||||
|
platformType ?? this.platformType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _params2String(Map<String, dynamic> params) {
|
||||||
|
return params.entries
|
||||||
|
.map((entry) => '${entry.key}: ${entry.value}\n')
|
||||||
|
.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '------- DEVICE INFO -------\n${_params2String(deviceParameters)}'
|
||||||
|
'------- APP INFO -------\n${_params2String(applicationParameters)}'
|
||||||
|
'------- ERROR -------\n$error\n'
|
||||||
|
'------- STACK TRACE -------\n${stackTrace.toString().trim()}\n'
|
||||||
|
'------- CUSTOM INFO -------\n${_params2String(customParameters)}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1084,7 +1084,7 @@ class PlPlayerController {
|
|||||||
if (kDebugMode)
|
if (kDebugMode)
|
||||||
videoPlayerController!.stream.log.listen(((PlayerLog log) {
|
videoPlayerController!.stream.log.listen(((PlayerLog log) {
|
||||||
if (log.level == 'error' || log.level == 'fatal') {
|
if (log.level == 'error' || log.level == 'fatal') {
|
||||||
Utils.reportError(log.text, null, log.prefix);
|
Utils.reportError('${log.prefix}: ${log.text}', null);
|
||||||
} else {
|
} else {
|
||||||
debugPrint(log.toString());
|
debugPrint(log.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:PiliPlus/utils/extension.dart';
|
||||||
|
import 'package:PiliPlus/utils/json_file_handler.dart';
|
||||||
|
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -10,44 +14,48 @@ class PiliLogger extends Logger {
|
|||||||
PiliLogger() : super();
|
PiliLogger() : super();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> log(
|
void log(
|
||||||
Level level,
|
Level level,
|
||||||
dynamic message, {
|
dynamic message, {
|
||||||
Object? error,
|
Object? error,
|
||||||
StackTrace? stackTrace,
|
StackTrace? stackTrace,
|
||||||
DateTime? time,
|
DateTime? time,
|
||||||
}) async {
|
}) {
|
||||||
if (level == Level.error || level == Level.fatal) {
|
if (level == Level.error || level == Level.fatal) {
|
||||||
// 添加至文件末尾
|
Catcher2.reportCheckedError(error, stackTrace);
|
||||||
File logFile = await LoggerUtils.getLogsPath();
|
|
||||||
logFile.writeAsString(
|
|
||||||
"**${DateTime.now()}** \n $message \n $stackTrace",
|
|
||||||
mode: FileMode.writeOnlyAppend,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
super.log(level, "$message", error: error, stackTrace: stackTrace);
|
super.log(level, message, error: error, stackTrace: stackTrace, time: time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoggerUtils {
|
abstract final class LoggerUtils {
|
||||||
static File? _logFile;
|
static File? _logFile;
|
||||||
|
|
||||||
static Future<File> getLogsPath() async {
|
static Future<File> getLogsPath() async {
|
||||||
if (_logFile != null) return _logFile!;
|
if (_logFile != null) return _logFile!;
|
||||||
|
|
||||||
String dir = (await getApplicationDocumentsDirectory()).path;
|
String dir = (await getApplicationDocumentsDirectory()).path;
|
||||||
final String filename = p.join(dir, ".pili_logs");
|
final String filename = p.join(dir, '.pili_logs.json');
|
||||||
final File file = File(filename);
|
final File file = File(filename);
|
||||||
if (!file.existsSync()) {
|
if (!file.existsSync()) {
|
||||||
await file.create(recursive: true);
|
await file.create(recursive: true);
|
||||||
|
// TODO: remove after next two versions
|
||||||
|
final oldFile = File(p.join(dir, '.pili_logs'));
|
||||||
|
if (oldFile.existsSync()) oldFile.tryDel();
|
||||||
}
|
}
|
||||||
return _logFile = file;
|
return _logFile = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> clearLogs() async {
|
static Future<bool> clearLogs() async {
|
||||||
final file = await getLogsPath();
|
|
||||||
try {
|
try {
|
||||||
await file.writeAsBytes(const [], flush: true);
|
if (Pref.enableLog) {
|
||||||
|
await JsonFileHandler.add(
|
||||||
|
(raf) => raf.setPosition(0).then((raf) => raf.truncate(0)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final file = await getLogsPath();
|
||||||
|
await file.writeAsBytes(const [], flush: true);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if (kDebugMode) debugPrint('Error clearing file: $e');
|
// if (kDebugMode) debugPrint('Error clearing file: $e');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
103
lib/utils/json_file_handler.dart
Normal file
103
lib/utils/json_file_handler.dart
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:PiliPlus/services/logger.dart' show LoggerUtils;
|
||||||
|
import 'package:catcher_2/model/platform_type.dart';
|
||||||
|
import 'package:catcher_2/model/report.dart';
|
||||||
|
import 'package:catcher_2/model/report_handler.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class JsonFileHandler extends ReportHandler {
|
||||||
|
final bool enableDeviceParameters;
|
||||||
|
final bool enableApplicationParameters;
|
||||||
|
final bool enableStackTrace;
|
||||||
|
final bool enableCustomParameters;
|
||||||
|
final bool printLogs;
|
||||||
|
final bool handleWhenRejected;
|
||||||
|
|
||||||
|
static late Future<RandomAccessFile> _future;
|
||||||
|
|
||||||
|
JsonFileHandler._({
|
||||||
|
this.enableDeviceParameters = true,
|
||||||
|
this.enableApplicationParameters = true,
|
||||||
|
this.enableStackTrace = true,
|
||||||
|
this.enableCustomParameters = true,
|
||||||
|
this.printLogs = false,
|
||||||
|
this.handleWhenRejected = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<JsonFileHandler?> init({
|
||||||
|
bool enableDeviceParameters = true,
|
||||||
|
bool enableApplicationParameters = true,
|
||||||
|
bool enableStackTrace = true,
|
||||||
|
bool enableCustomParameters = true,
|
||||||
|
bool printLogs = false,
|
||||||
|
bool handleWhenRejected = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final raf = await (await LoggerUtils.getLogsPath()).open(
|
||||||
|
mode: FileMode.writeOnlyAppend,
|
||||||
|
);
|
||||||
|
await raf.writeFrom(const []);
|
||||||
|
await raf.flush();
|
||||||
|
_future = Future.syncValue(raf);
|
||||||
|
return JsonFileHandler._(
|
||||||
|
enableDeviceParameters: enableDeviceParameters,
|
||||||
|
enableApplicationParameters: enableApplicationParameters,
|
||||||
|
enableStackTrace: enableStackTrace,
|
||||||
|
enableCustomParameters: enableCustomParameters,
|
||||||
|
printLogs: printLogs,
|
||||||
|
handleWhenRejected: handleWhenRejected,
|
||||||
|
);
|
||||||
|
} catch (e, s) {
|
||||||
|
debugPrintStack(stackTrace: s, label: e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<RandomAccessFile> add(
|
||||||
|
Future<RandomAccessFile> Function(RandomAccessFile) onValue,
|
||||||
|
) {
|
||||||
|
return _future = _future.then(onValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> handle(Report report, BuildContext? context) async {
|
||||||
|
try {
|
||||||
|
await _processReport(report);
|
||||||
|
return true;
|
||||||
|
} catch (exc, stackTrace) {
|
||||||
|
_printLog('Exception occurred: $exc stack: $stackTrace');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _processReport(Report report) {
|
||||||
|
_printLog('Writing report to file');
|
||||||
|
final json = report.toJson(
|
||||||
|
enableDeviceParameters: enableDeviceParameters,
|
||||||
|
enableApplicationParameters: enableApplicationParameters,
|
||||||
|
enableStackTrace: enableStackTrace,
|
||||||
|
enableCustomParameters: enableCustomParameters,
|
||||||
|
);
|
||||||
|
return add((raf) => raf.writeString('${jsonEncode(json)}\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _printLog(String log) {
|
||||||
|
if (printLogs) {
|
||||||
|
logger.info(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<PlatformType> getSupportedPlatforms() => const [
|
||||||
|
PlatformType.android,
|
||||||
|
PlatformType.iOS,
|
||||||
|
PlatformType.linux,
|
||||||
|
PlatformType.macOS,
|
||||||
|
PlatformType.windows,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldHandleWhenRejected() => handleWhenRejected;
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ import 'dart:io';
|
|||||||
import 'dart:math' show Random;
|
import 'dart:math' show Random;
|
||||||
|
|
||||||
import 'package:PiliPlus/common/constants.dart';
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
|
import 'package:catcher_2/catcher_2.dart';
|
||||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -163,19 +163,7 @@ abstract class Utils {
|
|||||||
/// containing the `catch` block with
|
/// containing the `catch` block with
|
||||||
/// `@pragma('vm:notify-debugger-on-exception')` to allow an attached debugger
|
/// `@pragma('vm:notify-debugger-on-exception')` to allow an attached debugger
|
||||||
/// to treat the exception as unhandled.
|
/// to treat the exception as unhandled.
|
||||||
static void reportError(
|
static void reportError(Object exception, [StackTrace? stack]) {
|
||||||
Object exception, [
|
Catcher2.reportCheckedError(exception, stack);
|
||||||
StackTrace? stack,
|
|
||||||
String? library = Constants.appName,
|
|
||||||
bool silent = false,
|
|
||||||
]) {
|
|
||||||
FlutterError.reportError(
|
|
||||||
FlutterErrorDetails(
|
|
||||||
exception: exception,
|
|
||||||
stack: stack,
|
|
||||||
library: library,
|
|
||||||
silent: silent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user