diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90fa6311..78230645 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,84 +1,157 @@ -name: build_apk +name: Pilipala Release # action事件触发 on: push: # push tag时触发 tags: - - 'v*.*.*' + - "v*.*.*" # 可以有多个jobs jobs: - build_apk: - # 运行环境 ubuntu-latest window-latest mac-latest - runs-on: ubuntu-latest + android: + # 运行环境 ubuntu-latest window-latest mac-latest + runs-on: ubuntu-latest - # 每个jobs中可以有多个steps - steps: - - name: 代码迁出 - uses: actions/checkout@v3 + # 每个jobs中可以有多个steps + steps: + - name: 代码迁出 + uses: actions/checkout@v3 - - name: 构建Java环境 - uses: actions/setup-java@v3 - with: - distribution: "zulu" - java-version: "17" - token: ${{secrets.GIT_TOKEN}} + - name: 构建Java环境 + uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "17" + token: ${{secrets.GIT_TOKEN}} - - name: 检查缓存 - uses: actions/cache@v2 - id: cache-flutter - with: - path: /root/flutter-sdk # Flutter SDK 的路径 - key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }} + - name: 检查缓存 + uses: actions/cache@v2 + id: cache-flutter + with: + path: /root/flutter-sdk # Flutter SDK 的路径 + key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }} - - name: 安装Flutter - if: steps.cache-flutter.outputs.cache-hit != 'true' - uses: subosito/flutter-action@v2 - with: - flutter-version: 3.16.5 - channel: any + - name: 安装Flutter + if: steps.cache-flutter.outputs.cache-hit != 'true' + uses: subosito/flutter-action@v2 + with: + flutter-version: 3.16.5 + channel: any - - name: 下载项目依赖 - run: flutter pub get + - name: 下载项目依赖 + run: flutter pub get - - name: 解码生成 jks - run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks - env: - KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} + - name: 解码生成 jks + run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks + env: + KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} - - name: flutter build apk - # 对应 android/app/build.gradle signingConfigs中的配置项 - run: flutter build apk --release --split-per-abi - env: - KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} - KEY_ALIAS: ${{ secrets.KEY_ALIAS }} - KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}} + - name: flutter build apk + run: flutter build apk --release --split-per-abi + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}} - - name: 获取版本号 - id: version - run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT + - name: flutter build apk + run: flutter build apk --release + env: + KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}} - # - name: 获取当前日期 - # id: date - # run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT + - name: 获取版本号 + id: version + run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT - - name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk - run: | - # DATE=${{ steps.date.outputs.date }} - for file in build/app/outputs/flutter-apk/app-*-release.apk; do - if [[ $file =~ app-(.*)-release.apk ]]; then - new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}.apk" - mv "$file" "$new_file_name" - fi - done + # - name: 获取当前日期 + # id: date + # run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT - - name: 构建和发布release - uses: ncipollo/release-action@v1 - with: - # release title - name: v${{ steps.version.outputs.version }} - artifacts: "build/app/outputs/flutter-apk/Pili-*.apk" - bodyFile: "change_log/${{steps.version.outputs.version}}.md" - token: ${{ secrets.GIT_TOKEN }} - allowUpdates: true + - name: 重命名应用 + run: | + # DATE=${{ steps.date.outputs.date }} + for file in build/app/outputs/flutter-apk/app-*.apk; do + if [[ $file =~ app-(.?*)release.apk ]]; then + new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}${{ steps.version.outputs.version }}.apk" + mv "$file" "$new_file_name" + fi + done + + - name: 上传 + uses: actions/upload-artifact@v3 + with: + name: Pilipala-Release + path: | + build/app/outputs/flutter-apk/Pili-*.apk + + iOS: + runs-on: macos-latest + + steps: + - name: 代码迁出 + uses: actions/checkout@v4 + + - name: 安装Flutter + if: steps.cache-flutter.outputs.cache-hit != 'true' + uses: subosito/flutter-action@v2.10.0 + with: + cache: true + flutter-version: 3.16.5 + + - name: flutter build ipa + run: | + flutter build ios --release --no-codesign + ln -sf ./build/ios/iphoneos Payload + zip -r9 app.ipa Payload/runner.app + + - name: 获取版本号 + id: version + run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT + + - name: 重命名应用 + run: | + DATE=${{ steps.date.outputs.date }} + for file in app.ipa; do + new_file_name="build/Pili-${{ steps.version.outputs.version }}.ipa" + mv "$file" "$new_file_name" + done + + - name: 上传 + uses: actions/upload-artifact@v3 + with: + if-no-files-found: error + name: Pilipala-Release + path: | + build/Pili-*.ipa + + upload: + runs-on: ubuntu-latest + + needs: + - android + - iOS + steps: + - uses: actions/download-artifact@v3 + with: + name: Pilipala-Release + path: ./Pilipala-Release + + - name: Install dependencies + run: sudo apt-get install tree -y + + - name: Get version + id: version + run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT + + - name: Upload Release + uses: ncipollo/release-action@v1 + with: + name: v${{ steps.version.outputs.version }} + token: ${{ secrets.GIT_TOKEN }} + omitBodyDuringUpdate: true + omitNameDuringUpdate: true + omitPrereleaseDuringUpdate: true + allowUpdates: true + artifacts: Pilipala-Release/* 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/change_log/1.0.18.0130.md b/change_log/1.0.18.0130.md new file mode 100644 index 00000000..2f0b80ca --- /dev/null +++ b/change_log/1.0.18.0130.md @@ -0,0 +1,16 @@ +## 1.0.18 + + +### 功能 + + +### 修复 + + +### 优化 + + + + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/change_log/1.0.19.0131.md b/change_log/1.0.19.0131.md new file mode 100644 index 00000000..1fd3071b --- /dev/null +++ b/change_log/1.0.19.0131.md @@ -0,0 +1,15 @@ +## 1.0.19 + + +### 修复 ++ 视频404、评论加载错误 ++ bvav转换 + +### 优化 ++ 视频详情页内存占用 + + + + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 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/http/init.dart b/lib/http/init.dart index 6244df04..dcc9f402 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -179,8 +179,14 @@ class Request { ); return response; } on DioException catch (e) { - print('get error: $e'); - return Future.error(await ApiInterceptor.dioError(e)); + Response errResponse = Response( + data: { + 'message': await ApiInterceptor.dioError(e) + }, // 将自定义 Map 数据赋值给 Response 的 data 属性 + statusCode: 200, + requestOptions: RequestOptions(), + ); + return errResponse; } } @@ -201,8 +207,14 @@ class Request { // print('post success: ${response.data}'); return response; } on DioException catch (e) { - print('post error: $e'); - return Future.error(await ApiInterceptor.dioError(e)); + Response errResponse = Response( + data: { + 'message': await ApiInterceptor.dioError(e) + }, // 将自定义 Map 数据赋值给 Response 的 data 属性 + statusCode: 200, + requestOptions: RequestOptions(), + ); + return errResponse; } } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 48ba7e60..362ff17a 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -5,7 +5,6 @@ import 'package:dio/dio.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive/hive.dart'; import '../utils/storage.dart'; -// import 'package:get/get.dart' hide Response; class ApiInterceptor extends Interceptor { @override @@ -71,35 +70,28 @@ class ApiInterceptor extends Interceptor { return '发送请求超时,请检查网络设置'; case DioExceptionType.unknown: final String res = await checkConnect(); - return '$res \n 网络异常,请稍后重试!'; - // default: - // return 'Dio异常'; + return '$res,网络异常!'; } } static Future checkConnect() async { final ConnectivityResult connectivityResult = await Connectivity().checkConnectivity(); - if (connectivityResult == ConnectivityResult.mobile) { - return 'connected with mobile network'; - } else if (connectivityResult == ConnectivityResult.wifi) { - return 'connected with wifi network'; - } else if (connectivityResult == ConnectivityResult.ethernet) { - // I am connected to a ethernet network. - return ''; - } else if (connectivityResult == ConnectivityResult.vpn) { - // I am connected to a vpn network. - // Note for iOS and macOS: - // There is no separate network interface type for [vpn]. - // It returns [other] on any device (also simulator) - return ''; - } else if (connectivityResult == ConnectivityResult.other) { - // I am connected to a network which is not in the above mentioned networks. - return ''; - } else if (connectivityResult == ConnectivityResult.none) { - return 'not connected to any network'; - } else { - return ''; + switch (connectivityResult) { + case ConnectivityResult.mobile: + return '正在使用移动流量'; + case ConnectivityResult.wifi: + return '正在使用wifi'; + case ConnectivityResult.ethernet: + return '正在使用局域网'; + case ConnectivityResult.vpn: + return '正在使用代理网络'; + case ConnectivityResult.other: + return '正在使用其他网络'; + case ConnectivityResult.none: + return '未连接到任何网络'; + default: + return ''; } } } diff --git a/lib/http/video.dart b/lib/http/video.dart index a48dd11b..d0faabaa 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -240,10 +240,11 @@ class VideoHttp { // 获取投币状态 static Future hasCoinVideo({required String bvid}) async { var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid}); + print('res: $res'); if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; } else { - return {'status': true, 'data': []}; + return {'status': false, 'data': []}; } } @@ -377,7 +378,7 @@ class VideoHttp { if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; } else { - return {'status': true, 'data': []}; + return {'status': false, 'data': []}; } } @@ -393,7 +394,7 @@ class VideoHttp { if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; } else { - return {'status': true, 'data': []}; + return {'status': false, 'data': []}; } } @@ -449,6 +450,8 @@ class VideoHttp { }); if (res.data['code'] == 0) { return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'data': null, 'msg': res.data['message']}; } } @@ -463,11 +466,13 @@ class VideoHttp { 'up_mid': upMid, }); var res = await Request().get(Api.aiConclusion, data: params); - if (res.data['code'] == 0) { + if (res.data['code'] == 0 && res.data['data']['code'] == 0) { return { 'status': true, 'data': AiConclusionModel.fromJson(res.data['data']), }; + } else { + return {'status': false, 'data': []}; } } } diff --git a/lib/main.dart b/lib/main.dart index f834d5d2..5c467722 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,9 @@ 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:pilipala/utils/recommend_filter.dart'; +import 'package:catcher_2/catcher_2.dart'; +import './services/loggeer.dart'; + void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -34,7 +37,33 @@ void main() async { Request(); await Request.setCookie(); RecommendFilter(); - 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/models/common/dynamic_badge_mode.dart b/lib/models/common/dynamic_badge_mode.dart new file mode 100644 index 00000000..2609c5e2 --- /dev/null +++ b/lib/models/common/dynamic_badge_mode.dart @@ -0,0 +1,9 @@ +enum DynamicBadgeMode { hidden, point, number } + +extension DynamicBadgeModeDesc on DynamicBadgeMode { + String get description => ['隐藏', '红点', '数字'][index]; +} + +extension DynamicBadgeModeCode on DynamicBadgeMode { + int get code => [0, 1, 2][index]; +} diff --git a/lib/models/github/latest.dart b/lib/models/github/latest.dart index 8730a4ba..c4b88b63 100644 --- a/lib/models/github/latest.dart +++ b/lib/models/github/latest.dart @@ -17,8 +17,9 @@ class LatestDataModel { url = json['url']; tagName = json['tag_name']; createdAt = json['created_at']; - assets = - json['assets'].map((e) => AssetItem.fromJson(e)).toList(); + assets = json['assets'] != null + ? json['assets'].map((e) => AssetItem.fromJson(e)).toList() + : []; body = json['body']; } } diff --git a/lib/models/msg/session.dart b/lib/models/msg/session.dart index 1fa05cb0..ea241249 100644 --- a/lib/models/msg/session.dart +++ b/lib/models/msg/session.dart @@ -166,7 +166,7 @@ class SessionMsgDataModel { int? hasMore; int? minSeqno; int? maxSeqno; - List? eInfos; + List? eInfos; SessionMsgDataModel.fromJson(Map json) { messages = json['messages'] 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/danmaku/view.dart b/lib/pages/danmaku/view.dart index 83d6020e..109f0206 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -35,6 +35,7 @@ class _PlDanmakuState extends State { late double opacityVal; late double fontSizeVal; late double danmakuDurationVal; + late double strokeWidth; int latestAddedPosition = -1; @override @@ -65,6 +66,7 @@ class _PlDanmakuState extends State { showArea = playerController.showArea; opacityVal = playerController.opacityVal; fontSizeVal = playerController.fontSizeVal; + strokeWidth = playerController.strokeWidth; danmakuDurationVal = playerController.danmakuDurationVal; } @@ -136,6 +138,7 @@ class _PlDanmakuState extends State { hideBottom: blockTypes.contains(4), duration: danmakuDurationVal / playerController.playbackSpeed, + strokeWidth: strokeWidth, // initDuration / // (danmakuSpeedVal * widget.playerController.playbackSpeed), ), diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart index eee4255c..d10804d7 100644 --- a/lib/pages/dynamics/widgets/content_panel.dart +++ b/lib/pages/dynamics/widgets/content_panel.dart @@ -1,5 +1,6 @@ // 内容 import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/dynamics/result.dart'; @@ -80,7 +81,7 @@ class _ContentState extends State { height: height, ), ), - height > maxHeight + height > Get.size.height * 0.9 ? const PBadge( text: '长图', right: 8, diff --git a/lib/pages/dynamics/widgets/pic_panel.dart b/lib/pages/dynamics/widgets/pic_panel.dart index 25b22c21..4e94e6fd 100644 --- a/lib/pages/dynamics/widgets/pic_panel.dart +++ b/lib/pages/dynamics/widgets/pic_panel.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; @@ -87,7 +88,7 @@ Widget picWidget(item, context) { childAspectRatio: aspectRatio, children: list, ), - if (len == 1 && origAspectRatio < 0.4) + if (len == 1 && height > Get.size.height * 0.9) const PBadge( text: '长图', top: null, diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index e6fafba6..685ff5c3 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -63,13 +63,16 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { } void setTabConfig() async { - defaultTabs = tabsConfig; + defaultTabs = [...tabsConfig]; tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); + defaultTabs.retainWhere( + (item) => tabbarSort.contains((item['type'] as TabType).id)); + defaultTabs.sort((a, b) => tabbarSort + .indexOf((a['type'] as TabType).id) + .compareTo(tabbarSort.indexOf((b['type'] as TabType).id))); - tabs.value = defaultTabs - .where((i) => tabbarSort.contains((i['type'] as TabType).id)) - .toList(); + tabs.value = defaultTabs; if (tabbarSort.contains(TabType.rcmd.id)) { initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id); diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index a55c143e..6f33d9cd 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -11,6 +11,7 @@ import 'package:pilipala/pages/home/view.dart'; import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; +import '../../models/common/dynamic_badge_mode.dart'; class MainController extends GetxController { List pages = [ @@ -65,6 +66,7 @@ class MainController extends GetxController { int selectedIndex = 0; Box userInfoCache = GStrorage.userInfo; RxBool userLogin = false.obs; + late Rx dynamicBadgeType = DynamicBadgeMode.number.obs; @override void onInit() { @@ -75,7 +77,12 @@ class MainController extends GetxController { hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); var userInfo = userInfoCache.get('userInfoCache'); userLogin.value = userInfo != null; - getUnreadDynamic(); + dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( + SettingBoxKey.dynamicBadgeMode, + defaultValue: DynamicBadgeMode.number.code)]; + if (dynamicBadgeType.value != DynamicBadgeMode.hidden) { + getUnreadDynamic(); + } } void onBackPressed(BuildContext context) { diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 5a570a8f..04e0f087 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/models/common/dynamic_badge_mode.dart'; import 'package:pilipala/pages/dynamics/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/media/index.dart'; @@ -127,11 +128,21 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { destinations: [ ..._mainController.navigationBars.map((e) { return NavigationDestination( - icon: Badge( - label: Text(e['count'].toString()), - padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: e['count'] > 0, - child: e['icon'], + icon: Obx( + () => Badge( + label: + _mainController.dynamicBadgeType.value == + DynamicBadgeMode.number + ? Text(e['count'].toString()) + : null, + padding: + const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: + _mainController.dynamicBadgeType.value != + DynamicBadgeMode.hidden && + e['count'] > 0, + child: e['icon'], + ), ), selectedIcon: e['selectIcon'], label: e['label'], @@ -148,11 +159,21 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { items: [ ..._mainController.navigationBars.map((e) { return BottomNavigationBarItem( - icon: Badge( - label: Text(e['count'].toString()), - padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: e['count'] > 0, - child: e['icon'], + icon: Obx( + () => Badge( + label: + _mainController.dynamicBadgeType.value == + DynamicBadgeMode.number + ? Text(e['count'].toString()) + : null, + padding: + const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: + _mainController.dynamicBadgeType.value != + DynamicBadgeMode.hidden && + e['count'] > 0, + child: e['icon'], + ), ), activeIcon: e['selectIcon'], label: e['label'], diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index 06c375da..091b2149 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -64,7 +64,7 @@ class _MinePageState extends State { ), ), IconButton( - onPressed: () => Get.toNamed('/setting'), + onPressed: () => Get.toNamed('/setting', preventDuplicates: false), icon: const Icon( CupertinoIcons.slider_horizontal_3, ), diff --git a/lib/pages/search_panel/widgets/article_panel.dart b/lib/pages/search_panel/widgets/article_panel.dart index 35e39640..c7074229 100644 --- a/lib/pages/search_panel/widgets/article_panel.dart +++ b/lib/pages/search_panel/widgets/article_panel.dart @@ -25,16 +25,17 @@ Widget searchArticlePanel(BuildContext context, ctr, list) { padding: const EdgeInsets.fromLTRB( StyleString.safeSpace, 5, StyleString.safeSpace, 5), child: LayoutBuilder(builder: (context, boxConstraints) { - double width = (boxConstraints.maxWidth - - StyleString.cardSpace * - 6 / - MediaQuery.textScalerOf(context).scale(2.0)); + final double width = (boxConstraints.maxWidth - + StyleString.cardSpace * + 6 / + MediaQuery.textScalerOf(context).scale(1.0)) / + 2; return Container( constraints: const BoxConstraints(minHeight: 88), height: width / StyleString.aspectRatio, child: Row( crossAxisAlignment: CrossAxisAlignment.start, - children: [ + children: [ if (list[index].imageUrls != null && list[index].imageUrls.isNotEmpty) AspectRatio( diff --git a/lib/pages/setting/controller.dart b/lib/pages/setting/controller.dart index afd14156..2e6680e5 100644 --- a/lib/pages/setting/controller.dart +++ b/lib/pages/setting/controller.dart @@ -7,6 +7,9 @@ import 'package:pilipala/models/common/theme_type.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/login.dart'; import 'package:pilipala/utils/storage.dart'; +import '../../models/common/dynamic_badge_mode.dart'; +import '../main/index.dart'; +import 'widgets/select_dialog.dart'; class SettingController extends GetxController { Box userInfoCache = GStrorage.userInfo; @@ -19,6 +22,7 @@ class SettingController extends GetxController { RxInt picQuality = 10.obs; Rx themeType = ThemeType.system.obs; var userInfo; + Rx dynamicBadgeType = DynamicBadgeMode.number.obs; @override void onInit() { @@ -33,6 +37,9 @@ class SettingController extends GetxController { setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)]; + dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( + SettingBoxKey.dynamicBadgeMode, + defaultValue: DynamicBadgeMode.number.code)]; } loginOut() async { @@ -76,4 +83,31 @@ class SettingController extends GetxController { feedBackEnable.value = !feedBackEnable.value; setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value); } + + // 设置动态未读标记 + setDynamicBadgeMode(BuildContext context) async { + DynamicBadgeMode? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '动态未读标记', + value: dynamicBadgeType.value, + values: DynamicBadgeMode.values.map((e) { + return {'title': e.description, 'value': e}; + }).toList(), + ); + }, + ); + if (result != null) { + dynamicBadgeType.value = result; + setting.put(SettingBoxKey.dynamicBadgeMode, result.code); + MainController mainController = Get.put(MainController()); + mainController.dynamicBadgeType.value = + DynamicBadgeMode.values[result.code]; + if (mainController.dynamicBadgeType.value != DynamicBadgeMode.hidden) { + mainController.getUnreadDynamic(); + } + SmartDialog.showToast('设置成功'); + } + } } diff --git a/lib/pages/setting/pages/home_tabbar_set.dart b/lib/pages/setting/pages/home_tabbar_set.dart index 445ca4f5..4cb3944c 100644 --- a/lib/pages/setting/pages/home_tabbar_set.dart +++ b/lib/pages/setting/pages/home_tabbar_set.dart @@ -22,6 +22,17 @@ class _TabbarSetPageState extends State { defaultTabs = tabsConfig; tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); + // 对 tabData 进行排序 + defaultTabs.sort((a, b) { + int indexA = tabbarSort.indexOf((a['type'] as TabType).id); + int indexB = tabbarSort.indexOf((b['type'] as TabType).id); + + // 如果类型在 sortOrder 中不存在,则放在末尾 + if (indexA == -1) indexA = tabbarSort.length; + if (indexB == -1) indexB = tabbarSort.length; + + return indexA.compareTo(indexB); + }); } void saveEdit() { 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/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index 57ec110a..fb3c780a 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -10,6 +10,7 @@ import 'package:pilipala/pages/setting/widgets/select_dialog.dart'; import 'package:pilipala/pages/setting/widgets/slide_dialog.dart'; import 'package:pilipala/utils/storage.dart'; +import '../../models/common/dynamic_badge_mode.dart'; import 'controller.dart'; import 'widgets/switch_item.dart'; @@ -241,6 +242,14 @@ class _StyleSettingState extends State { '当前模式:${settingController.themeType.value.description}', style: subTitleStyle)), ), + ListTile( + dense: false, + onTap: () => settingController.setDynamicBadgeMode(context), + title: Text('动态未读标记', style: titleStyle), + subtitle: Obx(() => Text( + '当前标记样式:${settingController.dynamicBadgeType.value.description}', + style: subTitleStyle)), + ), ListTile( dense: false, onTap: () => Get.toNamed('/colorSetting'), diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 1230a94b..d1298fcc 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -148,7 +148,9 @@ class VideoIntroController extends GetxController { // 获取投币状态 Future queryHasCoinVideo() async { var result = await VideoHttp.hasCoinVideo(bvid: bvid); - hasCoin.value = result["data"]['multiply'] == 0 ? false : true; + if (result['status']) { + hasCoin.value = result["data"]['multiply'] == 0 ? false : true; + } } // 获取收藏状态 @@ -208,6 +210,10 @@ class VideoIntroController extends GetxController { // (取消)点赞 Future actionLikeVideo() async { + if (userInfo == null) { + SmartDialog.showToast('账号未登录'); + return; + } var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); if (result['status']) { // hasLike.value = result["data"] == 1 ? true : false; @@ -570,10 +576,12 @@ class VideoIntroController extends GetxController { cid: lastPlayCid.value, upMid: videoDetail.value.owner!.mid!, ); + SmartDialog.dismiss(); if (res['status']) { modelResult = res['data'].modelResult; + } else { + SmartDialog.showToast("当前视频可能暂不支持AI视频总结"); } - SmartDialog.dismiss(); return res; } } diff --git a/lib/pages/video/detail/related/controller.dart b/lib/pages/video/detail/related/controller.dart index eb1a14d5..f7042871 100644 --- a/lib/pages/video/detail/related/controller.dart +++ b/lib/pages/video/detail/related/controller.dart @@ -4,7 +4,7 @@ import 'package:pilipala/http/video.dart'; class ReleatedController extends GetxController { // 视频aid - String bvid = Get.parameters['bvid']!; + String bvid = Get.parameters['bvid'] ?? ""; // 推荐视频列表 List relatedVideoList = []; diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 7232ebab..b2c67b1e 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -256,7 +256,12 @@ class _VideoReplyPanelState extends State // 请求错误 return HttpError( errMsg: data['msg'], - fn: () => setState(() {}), + fn: () { + setState(() { + _futureBuilderFuture = + _videoReplyController.queryReplyList(); + }); + }, ); } } else { diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 475c0939..7991a366 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; @@ -539,18 +540,6 @@ InlineSpan buildContent( // replyReply 查看二楼回复(回复详情)回调 // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示 final content = replyItem.content; - if (content.emote.isEmpty && - content.atNameToMid.isEmpty && - content.jumpUrl.isEmpty && - content.vote.isEmpty && - content.pictures.isEmpty) { - return TextSpan( - text: content.message, - recognizer: TapGestureRecognizer() - ..onTap = - () => replyReply(replyItem.root == 0 ? replyItem : fReplyItem), - ); - } final List spanChilds = []; bool hasMatchMember = false; @@ -582,258 +571,171 @@ InlineSpan buildContent( }); } // content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' '); - if (content.message.contains('&')) { - content.message = content.message.replaceAll('&', '&'); - } - // 匹配表情 - content.message.splitMapJoin( - RegExp(r"\[.*?\]"), - onMatch: (Match match) { - final String matchStr = match[0]!; - if (content.emote.isNotEmpty && - matchStr.indexOf('[') == matchStr.lastIndexOf('[') && - matchStr.indexOf(']') == matchStr.lastIndexOf(']')) { - final int size = content.emote[matchStr]['meta']['size']; - if (content.emote.keys.contains(matchStr)) { - spanChilds.add( - WidgetSpan( - child: NetworkImgLayer( - src: content.emote[matchStr]['url'], - type: 'emote', - width: size * 20, - height: size * 20, - ), - ), - ); - } else { - spanChilds.add(TextSpan( - text: matchStr, - recognizer: TapGestureRecognizer() - ..onTap = () => - replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); - return matchStr; - } - } else { - spanChilds.add(TextSpan( - text: matchStr, - recognizer: TapGestureRecognizer() - ..onTap = () => - replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); - return matchStr; - } - return ''; - }, - onNonMatch: (String str) { - // 匹配@用户 - String matchMember = str; - if (content.atNameToMid.isNotEmpty) { - final List atNameToMidKeys = content.atNameToMid.keys.toList(); - RegExp reg = RegExp(atNameToMidKeys.map((key) => key).join('|')); - // if (!content.message.contains(':')) { - // reg = RegExp(r"@.*( |:)"); - // } + content.message = content.message + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(' ', ' '); + // print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString()); + // 构建正则表达式 + final List specialTokens = [ + ...content.emote.keys, + ...content.atNameToMid.keys.map((e) => '@$e'), + ...content.jumpUrl.keys.map((e) => + e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')), + ]; - // 只@用户没有内容 - if (!content.message.contains(':') || - (content.atNameToMid.length == 1 && - content.message == '@${content.members.first.uname}')) { - reg = RegExp(r"@.*( |:|$)"); - } - matchMember = str.splitMapJoin( - reg, - onMatch: (Match match) { - if (match[0] != null) { - hasMatchMember = true; - content.atNameToMid.forEach((key, value) { - if (str.contains('回复')) { - spanChilds.add( - TextSpan( - text: '回复 ', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - ), - ), - ); - } - spanChilds.add( - TextSpan( - text: '@$key', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.titleSmall!.fontSize, - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - final String heroTag = Utils.makeHeroTag(value); - Get.toNamed( - '/member?mid=$value', - arguments: {'face': '', 'heroTag': heroTag}, - ); - }, - ), + String patternStr = specialTokens.map(RegExp.escape).join('|'); + if (patternStr.isNotEmpty) { + patternStr += "|"; + } + patternStr += r'(\b\d{1,2}[::]\d{2}\b)'; + final RegExp pattern = RegExp(patternStr); + List matchedStrs = []; + void addPlainTextSpan(str) { + spanChilds.add(TextSpan( + text: str, + recognizer: TapGestureRecognizer() + ..onTap = + () => replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); + } + + // 分割文本并处理每个部分 + content.message.splitMapJoin( + pattern, + onMatch: (Match match) { + String matchStr = match[0]!; + if (content.emote.containsKey(matchStr)) { + // 处理表情 + final int size = content.emote[matchStr]['meta']['size']; + spanChilds.add(WidgetSpan( + child: NetworkImgLayer( + src: content.emote[matchStr]['url'], + type: 'emote', + width: size * 20, + height: size * 20, + ), + )); + } else if (matchStr.startsWith("@") && + content.atNameToMid.containsKey(matchStr.substring(1))) { + // 处理@用户 + final String userName = matchStr.substring(1); + final int userId = content.atNameToMid[userName]; + spanChilds.add( + TextSpan( + text: matchStr, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + final String heroTag = Utils.makeHeroTag(userId); + Get.toNamed( + '/member?mid=$userId', + arguments: {'face': '', 'heroTag': heroTag}, ); - }); - } - return ''; - }, - onNonMatch: (String str) { - if (!str.contains('@')) { - spanChilds.add(TextSpan(text: str)); - } - print(str); - return str; - }, + }, + ), + ); + } else if (RegExp(r'^\b[0-9]{1,2}[::][0-9]{2}\b$').hasMatch(matchStr)) { + spanChilds.add( + TextSpan( + text: ' $matchStr ', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + // 跳转到指定位置 + try { + matchStr = matchStr.replaceAll(':', ':'); + SmartDialog.showToast('跳转至:$matchStr'); + Get.find(tag: Get.arguments['heroTag']) + .plPlayerController + .seekTo( + Duration(seconds: Utils.duration(matchStr)), + ); + } catch (e) { + SmartDialog.showToast('跳转失败: $e'); + } + }, + ), ); } else { - matchMember = str; - } - - // 匹配 jumpUrl - String matchUrl = matchMember; - if (content.jumpUrl.isNotEmpty) { - final List urlKeys = content.jumpUrl.keys.toList().reversed.toList(); - for (int index = 0; index < urlKeys.length; index++) { - var i = urlKeys[index]; - if (i.contains('?')) { - urlKeys[index] = i.replaceAll('?', '\\?'); + // print("matchStr=$matchStr"); + String appUrlSchema = ''; + final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe, + defaultValue: false) as bool; + if (content.jumpUrl[matchStr] != null && + !matchedStrs.contains(matchStr)) { + appUrlSchema = content.jumpUrl[matchStr]['app_url_schema']; + if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) { + addPlainTextSpan(matchStr); + return ""; } - if (i.contains('+')) { - urlKeys[index] = i.replaceAll('+', '\\+'); - } - if (i.contains('*')) { - urlKeys[index] = i.replaceAll('*', '\\*'); - } - } - if (hasMatchMember) { - matchMember = matchMember.split('回复 @ :').length > 1 - ? matchMember.split('回复 @ :')[1] - : matchMember; - } - matchUrl = matchMember.splitMapJoin( - /// RegExp.escape() 转义特殊字符 - RegExp(urlKeys.map((key) => key).join("|")), - // RegExp('What does the fox say\\?'), - onMatch: (Match match) { - final String matchStr = match[0]!; - String appUrlSchema = ''; - if (content.jumpUrl[matchStr] != null) { - appUrlSchema = content.jumpUrl[matchStr]['app_url_schema']; - } - // 默认不显示关键词 - final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe, - defaultValue: false) as bool; - if (content.jumpUrl[matchStr] != null) { - spanChilds.add( - TextSpan( - text: content.jumpUrl[matchStr]['title'], - style: TextStyle( - color: enableWordRe - ? Theme.of(context).colorScheme.primary - : null, + spanChilds.addAll( + [ + if (content.jumpUrl[matchStr]?['prefix_icon'] != null) ...[ + WidgetSpan( + child: Image.network( + content.jumpUrl[matchStr]['prefix_icon'], + height: 19, + color: Theme.of(context).colorScheme.primary, ), - recognizer: TapGestureRecognizer() - ..onTap = () { - if (appUrlSchema == '') { - final String str = Uri.parse(matchStr).pathSegments[0]; - final Map matchRes = IdUtils.matchAvorBv(input: str); - final List matchKeys = matchRes.keys.toList(); - if (matchKeys.isNotEmpty) { - if (matchKeys.first == 'BV') { - Get.toNamed( - '/searchResult', - parameters: {'keyword': matchRes['BV']}, - ); - } - } else { + ) + ], + TextSpan( + text: content.jumpUrl[matchStr]['title'], + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + if (appUrlSchema == '') { + final String str = Uri.parse(matchStr).pathSegments[0]; + final Map matchRes = IdUtils.matchAvorBv(input: str); + final List matchKeys = matchRes.keys.toList(); + if (matchKeys.isNotEmpty) { + if (matchKeys.first == 'BV') { Get.toNamed( - '/webview', - parameters: { - 'url': matchStr, - 'type': 'url', - 'pageTitle': '' - }, + '/searchResult', + parameters: {'keyword': matchRes['BV']}, ); } } else { - if (appUrlSchema.startsWith('bilibili://search') && - enableWordRe) { - Get.toNamed('/searchResult', parameters: { - 'keyword': content.jumpUrl[matchStr]['title'] - }); - } - } - }, - ), - ); - } - - if (appUrlSchema.startsWith('bilibili://search') && enableWordRe) { - spanChilds.add( - WidgetSpan( - child: Icon( - FontAwesomeIcons.magnifyingGlass, - size: 9, - color: Theme.of(context).colorScheme.primary, - ), - alignment: PlaceholderAlignment.top, - ), - ); - } - return ''; - }, - onNonMatch: (String str) { - spanChilds.add(TextSpan( - text: str, - recognizer: TapGestureRecognizer() - ..onTap = () => replyReply( - replyItem.root == 0 ? replyItem : fReplyItem))); - return str; - }, - ); - } - str = matchUrl.splitMapJoin( - RegExp(r'\b\d{2}:\d{2}\b'), - onMatch: (Match match) { - String matchStr = match[0]!; - spanChilds.add( - TextSpan( - text: ' $matchStr ', - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - // 跳转到指定位置 - try { - Get.find( - tag: Get.arguments['heroTag']) - .plPlayerController - .seekTo( - Duration(seconds: Utils.duration(matchStr)), + Get.toNamed( + '/webview', + parameters: { + 'url': matchStr, + 'type': 'url', + 'pageTitle': '' + }, ); - } catch (_) {} - }, - ), + } + } else { + if (appUrlSchema.startsWith('bilibili://search')) { + Get.toNamed('/searchResult', parameters: { + 'keyword': content.jumpUrl[matchStr]['title'] + }); + } + } + }, + ) + ], ); - return ''; - }, - onNonMatch: (str) { - return str; - }, - ); - - if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) { - if (str != '') { - spanChilds.add(TextSpan( - text: str, - recognizer: TapGestureRecognizer() - ..onTap = () => - replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); + // 只显示一次 + matchedStrs.add(matchStr); + } else { + addPlainTextSpan(matchStr); } } - return str; + return ''; + }, + onNonMatch: (String nonMatchStr) { + addPlainTextSpan(nonMatchStr); + return nonMatchStr; }, ); @@ -841,10 +743,10 @@ InlineSpan buildContent( if (content.pictures.isNotEmpty) { final List picList = []; final int len = content.pictures.length; + spanChilds.add(const TextSpan(text: '\n')); if (len == 1) { Map pictureItem = content.pictures.first; picList.add(pictureItem['img_src']); - spanChilds.add(const TextSpan(text: '\n')); spanChilds.add( WidgetSpan( child: LayoutBuilder( @@ -880,7 +782,7 @@ InlineSpan buildContent( height: height, ), ), - height > maxHeight + height > Get.size.height * 0.9 ? const PBadge( text: '长图', right: 8, diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 59fa39b2..3644fa41 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -61,6 +61,7 @@ class _VideoDetailPageState extends State final Floating floating = Floating(); // 生命周期监听 late final AppLifecycleListener _lifecycleListener; + bool isShowing = true; @override void initState() { @@ -216,15 +217,15 @@ class _VideoDetailPageState extends State videoIntroController.isPaused = true; plPlayerController!.removeStatusLister(playerListener); plPlayerController!.pause(); - plPlayerController!.danmakuController?.pause(); - plPlayerController!.danmakuController?.clear(); } + setState(() => isShowing = false); super.didPushNext(); } @override // 返回当前页面时 void didPopNext() async { + setState(() => isShowing = true); videoDetailController.isFirstTime = false; final bool autoplay = autoPlayEnable; videoDetailController.playerInit(autoplay: autoplay); @@ -280,19 +281,13 @@ class _VideoDetailPageState extends State final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; final double pinnedHeaderHeight = statusBarHeight + kToolbarHeight + videoHeight; - if (MediaQuery.of(context).orientation == Orientation.landscape || - plPlayerController?.isFullScreen.value == true) { - enterFullScreen(); - } else { - exitFullScreen(); - } Widget childWhenDisabled = SafeArea( top: MediaQuery.of(context).orientation == Orientation.portrait && plPlayerController?.isFullScreen.value == true, bottom: MediaQuery.of(context).orientation == Orientation.portrait && plPlayerController?.isFullScreen.value == true, - left: plPlayerController?.isFullScreen.value != true, - right: plPlayerController?.isFullScreen.value != true, + left: false, //plPlayerController?.isFullScreen.value != true, + right: false, //plPlayerController?.isFullScreen.value != true, child: Stack( children: [ Scaffold( @@ -309,187 +304,189 @@ class _VideoDetailPageState extends State body: ExtendedNestedScrollView( controller: _extendNestCtr, headerSliverBuilder: - (BuildContext context, bool innerBoxIsScrolled) { + (BuildContext context2, bool innerBoxIsScrolled) { return [ Obx( - () => SliverAppBar( - automaticallyImplyLeading: false, - // 假装使用一个非空变量,避免Obx检测不到而罢工 - pinned: videoDetailController.autoPlay.value ^ - false ^ - videoDetailController.autoPlay.value, - elevation: 0, - scrolledUnderElevation: 0, - forceElevated: innerBoxIsScrolled, - expandedHeight: MediaQuery.of(context).orientation == - Orientation.landscape || - plPlayerController?.isFullScreen.value == true - ? MediaQuery.sizeOf(context).height - - (MediaQuery.of(context).orientation == - Orientation.landscape - ? 0 - : MediaQuery.of(context).padding.top) - : videoHeight, - backgroundColor: Colors.black, - flexibleSpace: FlexibleSpaceBar( - background: PopScope( - canPop: - plPlayerController?.isFullScreen.value != true, - onPopInvoked: (bool didPop) { - if (plPlayerController?.isFullScreen.value == - true) { - plPlayerController! - .triggerFullScreen(status: false); - } - if (MediaQuery.of(context).orientation == - Orientation.landscape) { - verticalScreen(); - } - }, - child: LayoutBuilder( - builder: (BuildContext context, - BoxConstraints boxConstraints) { - final double maxWidth = boxConstraints.maxWidth; - final double maxHeight = - boxConstraints.maxHeight; - return Stack( - children: [ - FutureBuilder( - future: _futureBuilderFuture, - builder: (BuildContext context, - AsyncSnapshot snapshot) { - if (snapshot.hasData && - snapshot.data['status']) { - return Obx( - () => !videoDetailController - .autoPlay.value - ? const SizedBox() - : PLVideoPlayer( - controller: - plPlayerController!, - headerControl: - videoDetailController - .headerControl, - danmuWidget: Obx( - () => PlDanmaku( - key: Key( - videoDetailController - .danmakuCid - .value - .toString()), - cid: - videoDetailController - .danmakuCid - .value, - playerController: - plPlayerController!, - ), - ), - ), - ); - } else { - return const SizedBox(); - } - }, - ), + () { + if (MediaQuery.of(context).orientation == + Orientation.landscape || + plPlayerController?.isFullScreen.value == true) { + enterFullScreen(); + } else { + exitFullScreen(); + } + return SliverAppBar( + automaticallyImplyLeading: false, + // 假装使用一个非空变量,避免Obx检测不到而罢工 + pinned: videoDetailController.autoPlay.value ^ + false ^ + videoDetailController.autoPlay.value, + elevation: 0, + scrolledUnderElevation: 0, + forceElevated: innerBoxIsScrolled, + expandedHeight: MediaQuery.of(context).orientation == + Orientation.landscape || + plPlayerController?.isFullScreen.value == true + ? MediaQuery.sizeOf(context).height - + (MediaQuery.of(context).orientation == + Orientation.landscape + ? 0 + : MediaQuery.of(context).padding.top) + : videoHeight, + backgroundColor: Colors.black, + flexibleSpace: FlexibleSpaceBar( + background: PopScope( + canPop: plPlayerController?.isFullScreen.value != + true, + onPopInvoked: (bool didPop) { + if (plPlayerController?.isFullScreen.value == + true) { + plPlayerController! + .triggerFullScreen(status: false); + } + if (MediaQuery.of(context).orientation == + Orientation.landscape) { + verticalScreen(); + } + }, + child: LayoutBuilder( + builder: (BuildContext context, + BoxConstraints boxConstraints) { + final double maxWidth = + boxConstraints.maxWidth; + final double maxHeight = + boxConstraints.maxHeight; + return Stack( + children: [ + if (isShowing) + FutureBuilder( + future: _futureBuilderFuture, + builder: (BuildContext context, + AsyncSnapshot snapshot) { + if (snapshot.hasData && + snapshot.data['status']) { + return Obx( + () => + !videoDetailController + .autoPlay.value + ? nil + : PLVideoPlayer( + controller: + plPlayerController!, + headerControl: + videoDetailController + .headerControl, + danmuWidget: Obx( + () => PlDanmaku( + key: Key(videoDetailController + .danmakuCid + .value + .toString()), + cid: videoDetailController + .danmakuCid + .value, + playerController: + plPlayerController!, + ), + ), + ), + ); + } else { + return const SizedBox(); + } + }, + ), - /// 关闭自动播放时 手动播放 - if (!videoDetailController - .autoPlay.value) ...[ - Obx( - () => Visibility( - visible: videoDetailController - .isShowCover.value, - child: Positioned( - top: 0, - left: 0, - right: 0, - child: GestureDetector( - onTap: () { - handlePlay(); - }, - child: NetworkImgLayer( - type: 'emote', - src: videoDetailController - .videoItem['pic'], - width: maxWidth, - height: maxHeight, + /// 关闭自动播放时 手动播放 + if (!videoDetailController + .autoPlay.value) ...[ + Obx( + () => Visibility( + visible: videoDetailController + .isShowCover.value, + child: Positioned( + top: 0, + left: 0, + right: 0, + child: GestureDetector( + onTap: () { + handlePlay(); + }, + child: NetworkImgLayer( + type: 'emote', + src: videoDetailController + .videoItem['pic'], + width: maxWidth, + height: maxHeight, + ), ), ), ), ), - ), - Obx( - () => Visibility( - visible: videoDetailController - .isShowCover.value && - videoDetailController - .isEffective.value, - child: Stack( - children: [ - Positioned( - top: 0, - left: 0, - right: 0, - child: AppBar( - primary: false, - foregroundColor: - Colors.white, - elevation: 0, - scrolledUnderElevation: 0, - backgroundColor: - Colors.transparent, - actions: [ - IconButton( - tooltip: '稍后再看', - onPressed: () async { - var res = await UserHttp - .toViewLater( - bvid: - videoDetailController - .bvid); - SmartDialog.showToast( - res['msg']); - }, - icon: const Icon(Icons - .history_outlined), - ), - const SizedBox(width: 14) - ], - ), - ), - Positioned( - right: 12, - bottom: 10, - child: TextButton.icon( - style: ButtonStyle( + Obx( + () => Visibility( + visible: videoDetailController + .isShowCover.value && + videoDetailController + .isEffective.value, + child: Stack( + children: [ + Positioned( + top: 0, + left: 0, + right: 0, + child: AppBar( + primary: false, + foregroundColor: + Colors.white, + elevation: 0, + scrolledUnderElevation: 0, backgroundColor: - MaterialStateProperty - .resolveWith( - (states) { - return Colors.white - .withOpacity(0.8); - }), + Colors.transparent, + actions: [ + IconButton( + tooltip: '稍后再看', + onPressed: () async { + var res = await UserHttp + .toViewLater( + bvid: videoDetailController + .bvid); + SmartDialog + .showToast( + res['msg']); + }, + icon: const Icon(Icons + .history_outlined), + ), + const SizedBox( + width: 14) + ], ), - onPressed: () => - handlePlay(), - icon: const Icon( - Icons.play_circle_outline, - size: 20, - ), - label: const Text('轻触封面播放'), ), - ), - ], - )), - ), - ] - ], - ); - }, - )), - ), - ), + Positioned( + right: 12, + bottom: 10, + child: IconButton( + tooltip: '播放', + onPressed: () => + handlePlay(), + icon: Image.asset( + 'assets/images/play.png', + width: 60, + height: 60, + )), + ), + ], + )), + ), + ] + ], + ); + }, + )), + ), + ); + }, ), ]; }, @@ -500,7 +497,9 @@ class _VideoDetailPageState extends State // }, /// 不收回 pinnedHeaderSliverHeightBuilder: () { - return plPlayerController?.isFullScreen.value == true + return MediaQuery.of(context).orientation == + Orientation.landscape || + plPlayerController?.isFullScreen.value == true ? MediaQuery.sizeOf(context).height : pinnedHeaderHeight; }, diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index f2e38870..3af35efd 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; @@ -724,6 +725,8 @@ class _HeaderControlState extends State { double fontSizeVal = widget.controller!.fontSizeVal; // 弹幕速度 double danmakuDurationVal = widget.controller!.danmakuDurationVal; + // 弹幕描边 + double strokeWidth = widget.controller!.strokeWidth; final DanmakuController danmakuController = widget.controller!.danmakuController!; @@ -857,6 +860,44 @@ class _HeaderControlState extends State { ), ), ), + Text('描边粗细 $strokeWidth'), + Padding( + padding: const EdgeInsets.only( + top: 0, + bottom: 6, + left: 10, + right: 10, + ), + child: SliderTheme( + data: SliderThemeData( + trackShape: MSliderTrackShape(), + thumbColor: Theme.of(context).colorScheme.primary, + activeTrackColor: Theme.of(context).colorScheme.primary, + trackHeight: 10, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6.0), + ), + child: Slider( + min: 0, + max: 3, + value: strokeWidth, + divisions: 6, + label: '$strokeWidth', + onChanged: (double val) { + strokeWidth = val; + widget.controller!.strokeWidth = val; + setState(() {}); + try { + final DanmakuOption currentOption = + danmakuController.option; + final DanmakuOption updatedOption = + currentOption.copyWith(strokeWidth: val); + danmakuController.updateOption(updatedOption); + } catch (_) {} + }, + ), + ), + ), Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'), Padding( padding: const EdgeInsets.only( diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 52a70c49..71dd4c03 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -1,3 +1,4 @@ +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/msg.dart'; import 'package:pilipala/models/msg/session.dart'; @@ -8,6 +9,8 @@ class WhisperDetailController extends GetxController { late String face; late String mid; RxList messageList = [].obs; + //表情转换图片规则 + List? eInfos; @override void onInit() { @@ -22,6 +25,9 @@ class WhisperDetailController extends GetxController { var res = await MsgHttp.sessionMsg(talkerId: talkerId); if (res['status']) { messageList.value = res['data'].messages; + if (messageList.isNotEmpty && res['data'].eInfos != null) { + eInfos = res['data'].eInfos; + } } return res; } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 5d108037..8d2297c4 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -110,12 +110,16 @@ class _WhisperDetailPageState extends State { if (i == 0) { return Column( children: [ - ChatItem(item: messageList[i]), + ChatItem( + item: messageList[i], + e_infos: _whisperDetailController.eInfos), const SizedBox(height: 12), ], ); } else { - return ChatItem(item: messageList[i]); + return ChatItem( + item: messageList[i], + e_infos: _whisperDetailController.eInfos); } }, ), diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index 9ea6bf2a..0925d569 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -1,38 +1,370 @@ // ignore_for_file: must_be_immutable +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/storage.dart'; +import '../../../http/search.dart'; + +enum MsgType { + invalid(value: 0, label: "空空的~"), + text(value: 1, label: "文本消息"), + pic(value: 2, label: "图片消息"), + audio(value: 3, label: "语音消息"), + share(value: 4, label: "分享消息"), + revoke(value: 5, label: "撤回消息"), + custom_face(value: 6, label: "自定义表情"), + share_v2(value: 7, label: "分享v2消息"), + sys_cancel(value: 8, label: "系统撤销"), + mini_program(value: 9, label: "小程序"), + notify_msg(value: 10, label: "业务通知"), + archive_card(value: 11, label: "投稿卡片"), + article_card(value: 12, label: "专栏卡片"), + pic_card(value: 13, label: "图片卡片"), + common_share(value: 14, label: "异形卡片"), + auto_reply_push(value: 16, label: "自动回复推送"), + notify_text(value: 18, label: "文本提示"); + + final int value; + final String label; + const MsgType({required this.value, required this.label}); + static MsgType parse(int value) { + return MsgType.values + .firstWhere((e) => e.value == value, orElse: () => MsgType.invalid); + } +} + class ChatItem extends StatelessWidget { dynamic item; + List? e_infos; ChatItem({ super.key, this.item, + this.e_infos, }); @override Widget build(BuildContext context) { bool isOwner = item.senderUid == GStrorage.userInfo.get('userInfoCache').mid; - bool isPic = item.msgType == 2; // 图片 - bool isText = item.msgType == 1; // 文本 - // bool isAchive = item.msgType == 11; // 投稿 - // bool isArticle = item.msgType == 12; // 专栏 - bool isRevoke = item.msgType == 5; // 撤回消息 - bool isSystem = - item.msgType == 18 || item.msgType == 10 || item.msgType == 13; - int msgType = item.msgType; + bool isPic = item.msgType == MsgType.pic.value; // 图片 + bool isText = item.msgType == MsgType.text.value; // 文本 + // bool isArchive = item.msgType == 11; // 投稿 + // bool isArticle = item.msgType == 12; // 专栏 + bool isRevoke = item.msgType == MsgType.revoke.value; // 撤回消息 + bool isShareV2 = item.msgType == MsgType.share_v2.value; + bool isSystem = item.msgType == MsgType.notify_text.value || + item.msgType == MsgType.notify_msg.value || + item.msgType == MsgType.pic_card.value || + item.msgType == MsgType.auto_reply_push.value; dynamic content = item.content ?? ''; + Color textColor(BuildContext context) { + return isOwner + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSecondaryContainer; + } + + Widget richTextMessage(BuildContext context) { + var text = content['content']; + if (e_infos != null) { + final List children = []; + Map emojiMap = {}; + for (var e in e_infos!) { + emojiMap[e['text']] = e['url']; + } + text.splitMapJoin( + RegExp(r"\[.+?\]"), + onMatch: (Match match) { + final String emojiKey = match[0]!; + if (emojiMap.containsKey(emojiKey)) { + children.add(WidgetSpan( + child: NetworkImgLayer( + width: 18, + height: 18, + src: emojiMap[emojiKey]!, + ), + )); + } + return ''; + }, + onNonMatch: (String text) { + children.add(TextSpan( + text: text, + style: TextStyle( + color: textColor(context), + letterSpacing: 0.6, + height: 1.5, + ))); + return ''; + }, + ); + return RichText( + text: TextSpan( + children: children, + ), + ); + } else { + return Text( + text, + style: TextStyle( + letterSpacing: 0.6, + color: textColor(context), + height: 1.5, + ), + ); + } + } + + Widget messageContent(BuildContext context) { + switch (MsgType.parse(item.msgType)) { + case MsgType.notify_msg: + return SystemNotice(item: item); + case MsgType.pic_card: + return SystemNotice2(item: item); + case MsgType.notify_text: + return Text( + jsonDecode(content['content']) + .map((m) => m['text'] as String) + .join("\n"), + style: TextStyle( + letterSpacing: 0.6, + height: 5, + color: Theme.of(context).colorScheme.outline.withOpacity(0.8), + ), + ); + case MsgType.text: + return richTextMessage(context); + case MsgType.pic: + return NetworkImgLayer( + width: 220, + height: 220 * content['height'] / content['width'], + src: content['url'], + ); + case MsgType.share_v2: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () async { + SmartDialog.showLoading(); + var bvid = content["bvid"]; + final int cid = await SearchHttp.ab2c(bvid: bvid); + final String heroTag = Utils.makeHeroTag(bvid); + SmartDialog.dismiss().then( + (e) => Get.toNamed('/video?bvid=$bvid&cid=$cid', + arguments: { + 'pic': content['thumb'], + 'heroTag': heroTag, + }), + ); + }, + child: NetworkImgLayer( + width: 220, + height: 220 * 9 / 16, + src: content['thumb'], + ), + ), + const SizedBox(height: 6), + Text( + content['title'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 1), + Text( + content['author'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context).withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ); + case MsgType.archive_card: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () async { + SmartDialog.showLoading(); + var bvid = content["bvid"]; + final int cid = await SearchHttp.ab2c(bvid: bvid); + final String heroTag = Utils.makeHeroTag(bvid); + SmartDialog.dismiss().then( + (e) => Get.toNamed('/video?bvid=$bvid&cid=$cid', + arguments: { + 'pic': content['thumb'], + 'heroTag': heroTag, + }), + ); + }, + child: NetworkImgLayer( + width: 220, + height: 220 * 9 / 16, + src: content['cover'], + ), + ), + const SizedBox(height: 6), + Text( + content['title'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 1), + Text( + Utils.timeFormat(content['times']), + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context).withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ); + case MsgType.auto_reply_push: + return Container( + constraints: const BoxConstraints( + maxWidth: 300.0, // 设置最大宽度为200.0 + ), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .secondaryContainer + .withOpacity(0.4), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + bottomLeft: Radius.circular(6), + bottomRight: Radius.circular(16), + ), + ), + margin: const EdgeInsets.all(12), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + content['main_title'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context), + fontWeight: FontWeight.bold, + ), + ), + for (var i in content['sub_cards']) ...[ + const SizedBox(height: 6), + GestureDetector( + onTap: () async { + RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', + caseSensitive: false); + Iterable matches = + bvRegex.allMatches(i['jump_url']); + if (matches.isNotEmpty) { + Match match = matches.first; + String bvid = match.group(0)!; + try { + SmartDialog.showLoading(); + final int cid = await SearchHttp.ab2c(bvid: bvid); + final String heroTag = Utils.makeHeroTag(bvid); + SmartDialog.dismiss().then( + (e) => Get.toNamed( + '/video?bvid=$bvid&cid=$cid', + arguments: { + 'pic': i['cover_url'], + 'heroTag': heroTag, + }), + ); + } catch (err) { + SmartDialog.dismiss(); + SmartDialog.showToast(err.toString()); + } + } else { + SmartDialog.showToast('未匹配到 BV 号'); + Get.toNamed('/webview', + arguments: {'url': i['jump_url']}); + } + }, + child: Row( + children: [ + NetworkImgLayer( + width: 130, + height: 130 * 9 / 16, + src: i['cover_url'], + ), + const SizedBox(width: 6), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i['field1'], + maxLines: 2, + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context), + fontWeight: FontWeight.bold, + ), + ), + Text( + i['field2'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context).withOpacity(0.6), + fontSize: 12, + ), + ), + Text( + Utils.timeFormat(int.parse(i['field3'])), + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context).withOpacity(0.6), + fontSize: 12, + ), + ), + ], + )), + ], + )), + ], + ], + )); + default: + return Text( + content['content'] ?? content.toString(), + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor(context), + fontWeight: FontWeight.bold, + ), + ); + } + } + return isSystem - ? (msgType == 10 - ? SystemNotice(item: item) - : msgType == 13 - ? SystemNotice2(item: item) - : const SizedBox()) + ? messageContent(context) : isRevoke ? const SizedBox() : Row( @@ -66,27 +398,7 @@ class ChatItem extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - isText - ? Text( - content['content'], - style: TextStyle( - color: isOwner - ? Theme.of(context) - .colorScheme - .onPrimary - : Theme.of(context) - .colorScheme - .onSecondaryContainer), - ) - : isPic - ? NetworkImgLayer( - width: 220, - height: 220 * - content['height'] / - content['width'], - src: content['url'], - ) - : const SizedBox(), + messageContent(context), SizedBox(height: isPic ? 7 : 2), Row( mainAxisSize: MainAxisSize.min, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index d35987f8..dfa580ab 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -221,6 +221,7 @@ class PlPlayerController { late double showArea; late double opacityVal; late double fontSizeVal; + late double strokeWidth; late double danmakuDurationVal; late List speedsList; // 缓存 @@ -275,6 +276,9 @@ class PlPlayerController { // 弹幕时间 danmakuDurationVal = localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0); + // 描边粗细 + strokeWidth = + localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5); playRepeat = PlayRepeat.values.toList().firstWhere( (e) => e.value == @@ -1086,6 +1090,7 @@ class PlPlayerController { localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal); + localCache.put(LocalCacheKey.strokeWidth, strokeWidth); if (_videoPlayerController != null) { var pp = _videoPlayerController!.platform as NativePlayer; await pp.setProperty('audio-files', ''); diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 5a86ecf6..ff506630 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'; @@ -153,6 +154,8 @@ class Routes { // 用户专栏 CustomGetPage( name: '/memberSeasons', page: () => const MemberSeasonsPage()), + // 日志 + CustomGetPage(name: '/logs', page: () => const LogsPage()), ]; } diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index 61b32b96..ad510e7d 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -147,8 +147,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { processingState: AudioProcessingState.idle, playing: false, )); - _item.removeLast(); if (_item.isNotEmpty) { + _item.removeLast(); setMediaItem(_item.last); } if (_item.isEmpty) { 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/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index a68bfb40..906f6348 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -1,51 +1,65 @@ -// ignore_for_file: constant_identifier_names +// ignore_for_file: constant_identifier_names, non_constant_identifier_names import 'dart:convert'; -import 'dart:math'; - -import 'package:flutter/material.dart'; class IdUtils { - static const String TABLE = - 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'; - static const List S = [11, 10, 3, 8, 4, 6]; // 位置编码表 - static const int XOR = 177451812; // 固定异或值 - static const int ADD = 8728348608; // 固定加法值 - static const List r = [ - 'B', - 'V', - '1', - '', - '', - '4', - '', - '1', - '', - '7', - '', - '' - ]; + static final XOR_CODE = BigInt.parse('23442827791579'); + static final MASK_CODE = BigInt.parse('2251799813685247'); + static final MAX_AID = BigInt.one << (BigInt.from(51)).toInt(); + static final BASE = BigInt.from(58); + + static const data = + 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf'; /// av转bv - static String av2bv(int av) { - int x_ = (av ^ XOR) + ADD; - List newR = []; - newR.addAll(r); - for (int i = 0; i < S.length; i++) { - newR[S[i]] = - TABLE.characters.elementAt((x_ / pow(58, i).toInt() % 58).toInt()); + static String av2bv(int aid) { + List bytes = [ + 'B', + 'V', + '1', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0' + ]; + int bvIndex = bytes.length - 1; + BigInt tmp = (MAX_AID | BigInt.from(aid)) ^ XOR_CODE; + while (tmp > BigInt.zero) { + bytes[bvIndex] = data[(tmp % BASE).toInt()]; + tmp = tmp ~/ BASE; + bvIndex -= 1; } - return newR.join(); + String tmpSwap = bytes[3]; + bytes[3] = bytes[9]; + bytes[9] = tmpSwap; + + tmpSwap = bytes[4]; + bytes[4] = bytes[7]; + bytes[7] = tmpSwap; + + return bytes.join(); } - /// bv转bv - static int bv2av(String bv) { - int r = 0; - for (int i = 0; i < S.length; i++) { - r += (TABLE.indexOf(bv.characters.elementAt(S[i])).toInt()) * - pow(58, i).toInt(); - } - return (r - ADD) ^ XOR; + /// bv转av + static int bv2av(String bvid) { + List bvidArr = bvid.split(''); + final tmpValue = bvidArr[3]; + bvidArr[3] = bvidArr[9]; + bvidArr[9] = tmpValue; + + final tmpValue2 = bvidArr[4]; + bvidArr[4] = bvidArr[7]; + bvidArr[7] = tmpValue2; + + bvidArr.removeRange(0, 3); + BigInt tmp = bvidArr.fold(BigInt.zero, + (pre, bvidChar) => pre * BASE + BigInt.from(data.indexOf(bvidChar))); + return ((tmp & MASK_CODE) ^ XOR_CODE).toInt(); } // 匹配 diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 1bcb2b9c..1b0b0c37 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -137,7 +137,8 @@ class SettingBoxKey { enableMYBar = 'enableMYBar', hideSearchBar = 'hideSearchBar', // 收起顶栏 hideTabBar = 'hideTabBar', // 收起底栏 - tabbarSort = 'tabbarSort'; // 首页tabbar + tabbarSort = 'tabbarSort', // 首页tabbar + dynamicBadgeMode = 'dynamicBadgeMode'; } class LocalCacheKey { @@ -150,12 +151,13 @@ class LocalCacheKey { wbiKeys = 'wbiKeys', timeStamp = 'timeStamp', - // 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 + // 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 danmakuBlockType = 'danmakuBlockType', danmakuShowArea = 'danmakuShowArea', danmakuOpacity = 'danmakuOpacity', danmakuFontScale = 'danmakuFontScale', danmakuDuration = 'danmakuDuration', + strokeWidth = 'strokeWidth', // 代理host port systemProxyHost = 'systemProxyHost', diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index f0b56fc4..64d20aa3 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -281,16 +281,18 @@ class Utils { // [arm64-v8a] String abi = androidInfo.supportedAbis.first; late String downloadUrl; - for (var i in data.assets) { - if (i.downloadUrl.contains(abi)) { - downloadUrl = i.downloadUrl; + if (data.assets.isNotEmpty) { + for (var i in data.assets) { + if (i.downloadUrl.contains(abi)) { + downloadUrl = i.downloadUrl; + } } + // 应用外下载 + launchUrl( + Uri.parse(downloadUrl), + mode: LaunchMode.externalApplication, + ); } - // 应用外下载 - launchUrl( - Uri.parse(downloadUrl), - mode: LaunchMode.externalApplication, - ); } } diff --git a/pubspec.lock b/pubspec.lock index b8f7aff8..f5d63ca9 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: @@ -492,10 +500,11 @@ packages: floating: dependency: "direct main" description: - name: floating - sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991 - url: "https://pub.flutter-io.cn" - source: hosted + path: "." + ref: main + resolved-ref: d2d8421c4d80f6113f832404109853684721e11a + url: "https://github.com/guozhigq/floating.git" + source: git version: "2.0.1" flutter: dependency: "direct main" @@ -547,6 +556,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 +606,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 +806,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 +822,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 +992,7 @@ packages: source: hosted version: "2.0.1" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" @@ -1214,6 +1255,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..16df4ad7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.17+1017 +version: 1.0.19+1019 environment: sdk: ">=2.19.6 <3.0.0" @@ -51,7 +51,7 @@ dependencies: cached_network_image: ^3.3.0 extended_image: ^8.2.0 saver_gallery: ^3.0.1 - + # 存储 path_provider: ^2.1.1 hive: ^2.2.3 @@ -85,14 +85,14 @@ dependencies: encrypt: ^5.0.3 # 视频播放器 - media_kit: ^1.1.10 # Primary package. - media_kit_video: ^1.2.4 # For video rendering. + media_kit: ^1.1.10 # Primary package. + media_kit_video: ^1.2.4 # For video rendering. media_kit_libs_video: ^1.0.4 # 媒体通知 audio_service: ^0.18.12 audio_session: ^0.1.16 - + # 音量、亮度、屏幕控制 flutter_volume_controller: ^1.3.1 screen_brightness: ^0.2.2+1 @@ -103,7 +103,7 @@ dependencies: auto_orientation: ^2.3.1 protobuf: ^3.0.0 animations: ^2.0.8 - + # 获取appx信息 package_info_plus: ^4.1.0 url_launcher: ^6.1.14 @@ -116,7 +116,7 @@ dependencies: appscheme: ^1.0.8 # 弹幕 ns_danmaku: - git: + git: url: https://github.com/guozhigq/flutter_ns_danmaku.git ref: master # 状态栏图标控制 @@ -124,7 +124,10 @@ dependencies: # 代理 system_proxy: ^0.1.0 # pip - floating: ^2.0.1 + floating: + git: + url: https://github.com/guozhigq/floating.git + ref: main # html解析 html: ^0.15.4 # html渲染 @@ -134,8 +137,10 @@ 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: flutter_test: sdk: flutter @@ -204,7 +209,6 @@ flutter: # - family: HarmonyOS # fonts: # - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf - - + # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages