Merge branch 'feature-logger'
This commit is contained in:
@ -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();
|
||||
runApp(const MyApp());
|
||||
|
||||
// 异常捕获 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(
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
201
lib/pages/setting/pages/logs.dart
Normal file
201
lib/pages/setting/pages/logs.dart
Normal 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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
56
lib/services/loggeer.dart
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user