diff --git a/android/app/build.gradle b/android/app/build.gradle index 0e87d789..3dc4f82a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5f855b9d..8db59815 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/lib/main.dart b/lib/main.dart index 2c7ef1ee..bdf3f6b7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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( diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index 997adbfe..5df05d31 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -133,6 +133,11 @@ class _AboutPageState extends State { 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'); + } } diff --git a/lib/pages/setting/pages/logs.dart b/lib/pages/setting/pages/logs.dart new file mode 100644 index 00000000..0958edb8 --- /dev/null +++ b/lib/pages/setting/pages/logs.dart @@ -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 createState() => _LogsPageState(); +} + +class _LogsPageState extends State { + 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>> 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> 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( + onSelected: (String type) { + // 处理菜单项选择的逻辑 + switch (type) { + case 'copy': + copyLogs(); + break; + case 'feedback': + feedback(); + break; + case 'clear': + clearLogsHandle(); + break; + default: + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: 'copy', + child: Text('复制日志'), + ), + const PopupMenuItem( + value: 'feedback', + child: Text('错误反馈'), + ), + const PopupMenuItem( + 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: [ + NoData(), + ], + ), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 64d8da31..23172d3c 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -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()), ]; } diff --git a/lib/services/loggeer.dart b/lib/services/loggeer.dart new file mode 100644 index 00000000..5555432c --- /dev/null +++ b/lib/services/loggeer.dart @@ -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() { + 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 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 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; +} diff --git a/pubspec.lock b/pubspec.lock index b8f7aff8..d9b6a01d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index 5f22a7de..d81781c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: