feat: 错误日志记录

This commit is contained in:
guozhigq
2024-01-28 15:52:30 +08:00
parent 1014c26d29
commit a78ce4f6d4
9 changed files with 367 additions and 4 deletions

View File

@ -58,11 +58,10 @@ android {
applicationId "com.guozhigq.pilipala"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
// minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
minSdkVersion 19
minSdkVersion 21
multiDexEnabled true
}

View File

@ -13,8 +13,13 @@ PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_mailer (0.0.1):
- Flutter
- flutter_volume_controller (0.0.1):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
@ -49,6 +54,7 @@ PODS:
- Flutter
- system_proxy (0.0.1):
- Flutter
- Toast (4.1.0)
- url_launcher_ios (0.0.1):
- Flutter
- volume_controller (0.0.1):
@ -68,7 +74,9 @@ DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
@ -93,6 +101,7 @@ SPEC REPOS:
- FMDB
- GT3Captcha-iOS
- ReachabilitySwift
- Toast
EXTERNAL SOURCES:
appscheme:
@ -109,8 +118,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_mailer:
:path: ".symlinks/plugins/flutter_mailer/ios"
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
media_kit_libs_ios_video:
@ -156,7 +169,9 @@ SPEC CHECKSUMS:
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
@ -173,6 +188,7 @@ SPEC CHECKSUMS:
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47

View File

@ -21,6 +21,8 @@ import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
import 'package:catcher_2/catcher_2.dart';
import './services/loggeer.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -32,7 +34,32 @@ void main() async {
await setupServiceLocator();
Request();
await Request.setCookie();
// 异常捕获 logo记录
final Catcher2Options debugConfig = Catcher2Options(
SilentReportMode(),
[
FileHandler(await getLogsPath()),
ConsoleHandler(
enableDeviceParameters: false,
enableApplicationParameters: false,
)
],
);
final Catcher2Options releaseConfig = Catcher2Options(
SilentReportMode(),
[FileHandler(await getLogsPath())],
);
Catcher2(
debugConfig: debugConfig,
releaseConfig: releaseConfig,
runAppFunction: () {
runApp(const MyApp());
},
);
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(

View File

@ -133,6 +133,11 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
ListTile(
onTap: () => _aboutController.logs(),
title: const Text('错误日志'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
],
),
),
@ -260,4 +265,9 @@ class AboutController extends GetxController {
mode: LaunchMode.externalApplication,
);
}
// 日志
logs() {
Get.toNamed('/logs');
}
}

View File

@ -0,0 +1,201 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../services/loggeer.dart';
class LogsPage extends StatefulWidget {
const LogsPage({super.key});
@override
State<LogsPage> createState() => _LogsPageState();
}
class _LogsPageState extends State<LogsPage> {
late File logsPath;
late String fileContent;
List logsContent = [];
@override
void initState() {
getPath();
super.initState();
}
void getPath() async {
logsPath = await getLogsPath();
fileContent = await logsPath.readAsString();
logsContent = await parseLogs(fileContent);
setState(() {});
}
Future<List<Map<String, dynamic>>> parseLogs(String fileContent) async {
const String splitToken =
'======================================================================';
List contentList = fileContent.split(splitToken).map((item) {
return item
.replaceAll(
'============================== CATCHER 2 LOG ==============================',
'Pilipala错误日志 \n ********************')
.replaceAll('DEVICE INFO', '设备信息')
.replaceAll('APP INFO', '应用信息')
.replaceAll('ERROR', '错误信息')
.replaceAll('STACK TRACE', '错误堆栈');
}).toList();
List<Map<String, dynamic>> result = [];
for (String i in contentList) {
DateTime? date;
String body = i
.split("\n")
.map((l) {
if (l.startsWith("Crash occurred on")) {
date = DateTime.parse(
l.split("Crash occurred on")[1].trim().split('.')[0],
);
return "";
}
return l;
})
.where((dynamic l) => l.replaceAll("\n", "").trim().isNotEmpty)
.join("\n");
if (date != null || body != '') {
result.add({'date': date, 'body': body, 'expand': false});
}
}
return result.reversed.toList();
}
void copyLogs() async {
await Clipboard.setData(ClipboardData(text: fileContent));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('复制成功')),
);
}
}
void feedback() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/issues'),
// 系统自带浏览器打开
mode: LaunchMode.externalApplication,
);
}
void clearLogsHandle() async {
if (await clearLogs()) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已清空')),
);
logsContent = [];
setState(() {});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text('日志', style: Theme.of(context).textTheme.titleMedium),
actions: [
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'copy':
copyLogs();
break;
case 'feedback':
feedback();
break;
case 'clear':
clearLogsHandle();
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'copy',
child: Text('复制日志'),
),
const PopupMenuItem<String>(
value: 'feedback',
child: Text('错误反馈'),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空日志'),
),
],
),
const SizedBox(width: 6),
],
),
body: logsContent.isNotEmpty
? ListView.builder(
itemCount: logsContent.length,
itemBuilder: (context, index) {
final log = logsContent[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
log['date'].toString(),
style: Theme.of(context).textTheme.titleMedium,
),
),
TextButton.icon(
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: log['body']),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'已将 ${log['date'].toString()} 复制至剪贴板',
),
),
);
}
},
icon: const Icon(Icons.copy_outlined, size: 16),
label: const Text('复制'),
)
],
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
elevation: 1,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: SelectableText(log['body']),
),
),
),
const Divider(indent: 12, endIndent: 12),
],
);
},
)
: const CustomScrollView(
slivers: <Widget>[
NoData(),
],
),
);
}
}

View File

@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart';
import '../pages/blacklist/index.dart';
@ -151,6 +152,8 @@ class Routes {
// 用户专栏
CustomGetPage(
name: '/memberSeasons', page: () => const MemberSeasonsPage()),
// 日志
CustomGetPage(name: '/logs', page: () => const LogsPage()),
];
}

56
lib/services/loggeer.dart Normal file
View File

@ -0,0 +1,56 @@
// final _loggerFactory =
import 'dart:io';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
final _loggerFactory = PiliLogger();
PiliLogger getLogger<T>() {
return _loggerFactory;
}
class PiliLogger extends Logger {
PiliLogger() : super();
@override
void log(Level level, dynamic message,
{Object? error, StackTrace? stackTrace, DateTime? time}) async {
if (level == Level.error) {
String dir = (await getApplicationDocumentsDirectory()).path;
// 创建logo文件
final String filename = p.join(dir, ".pili_logs");
// 添加至文件末尾
await File(filename).writeAsString(
"**${DateTime.now()}** \n $message \n $stackTrace",
mode: FileMode.writeOnlyAppend,
);
}
super.log(level, "$message", error: error, stackTrace: stackTrace);
}
}
Future<File> getLogsPath() async {
String dir = (await getApplicationDocumentsDirectory()).path;
final String filename = p.join(dir, ".pili_logs");
final file = File(filename);
if (!await file.exists()) {
await file.create();
}
return file;
}
Future<bool> clearLogs() async {
String dir = (await getApplicationDocumentsDirectory()).path;
final String filename = p.join(dir, ".pili_logs");
final file = File(filename);
try {
await file.writeAsString('');
} catch (e) {
print('Error clearing file: $e');
return false;
}
return true;
}

View File

@ -209,6 +209,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
catcher_2:
dependency: "direct main"
description:
name: catcher_2
sha256: ca94d45ffb52bf4b16a425cdff6734ae8443d36d5f06c276f1c2a593120b11ed
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
characters:
dependency: transitive
description:
@ -547,6 +555,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_mailer:
dependency: transitive
description:
name: flutter_mailer
sha256: "4fffaa35e911ff5ec2e5a4ebbca62c372e99a154eb3bb2c0bf79f09adf6ecf4c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -589,6 +605,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: transitive
description:
name: fluttertoast
sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.2.4"
font_awesome_flutter:
dependency: "direct main"
description:
@ -781,6 +805,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
logger:
dependency: "direct main"
description:
name: logger
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.2+1"
logging:
dependency: transitive
description:
@ -789,6 +821,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.0"
mailer:
dependency: transitive
description:
name: mailer
sha256: "57f6dd1496699999a7bfd0aa6be0645384f477f4823e16d4321c40a434346382"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.1"
matcher:
dependency: transitive
description:
@ -951,7 +991,7 @@ packages:
source: hosted
version: "2.0.1"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
@ -1214,6 +1254,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.8"
sentry:
dependency: transitive
description:
name: sentry
sha256: "5686ed515bb620dc52b4ae99a6586fe720d443591183cf1f620ec5d1f0eec100"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.15.0"
share_plus:
dependency: "direct main"
description:

View File

@ -134,6 +134,9 @@ dependencies:
uuid: ^3.0.7
scrollable_positioned_list: ^0.3.8
nil: ^1.1.1
catcher_2: ^1.1.0
logger: ^2.0.2+1
path: 1.8.3
dev_dependencies: