Compare commits

..

72 Commits

Author SHA1 Message Date
41c40dfbc4 feat: 热搜词跳转直播 2024-02-04 00:26:20 +08:00
5d9ecab1b0 mod: code format 2024-02-04 00:20:51 +08:00
3de009ac43 Merge branch 'fix-audioAutoReplay' 2024-02-03 23:46:34 +08:00
b29256f598 Merge branch 'fix-floating' 2024-02-03 23:42:54 +08:00
e7cf472a0f Merge branch 'fix-videoIntroError' 2024-02-03 23:39:17 +08:00
03c59d23b8 Merge branch 'fix-githubModelError' 2024-02-03 23:38:53 +08:00
b6f805f0e4 Merge branch 'main' of github.com:guozhigq/pilipala 2024-02-03 23:38:21 +08:00
e23c2469ed Merge pull request #509 from orz12/feat-auto_reply_push-msgtype
feat: 私信支持显示自动推送回复
2024-02-03 20:04:05 +08:00
387c799de1 feat: 动态未读标记 issues #459 2024-02-03 16:59:54 +08:00
230dd81342 fix: List 越界 2024-02-03 01:13:36 +08:00
47bdfec8c2 fix: github assets null error 2024-02-03 01:07:12 +08:00
6a844da259 mod: 点赞接口登录拦截 2024-02-03 00:53:18 +08:00
18bb58d293 mod: 投币状态响应status 2024-02-03 00:43:38 +08:00
045186b3c8 mod: 视频详情页响应status 2024-02-03 00:33:29 +08:00
b531599893 mod: floating依赖 2024-02-03 00:29:47 +08:00
1da84508d8 feat: 自动推送回复私信显示支持 2024-02-03 00:23:32 +08:00
4c44fab217 Merge pull request #502 from orz12/fix-query-onlineTotal-status-false
fix: 查询在线人数错误时没有返回status
2024-02-02 23:39:09 +08:00
5c3d438a7e Merge pull request #508 from guozhigq/fix-minePanelPush
fix: 个人面板无法跳转设置页面
2024-02-02 23:30:57 +08:00
92a8efdee1 Merge pull request #470 from orz12/fix-reply-reply-parse2
fix: 评论区识别逻辑重构,修复含有关键词的评论重复出现的问题
2024-02-02 23:27:59 +08:00
eb1e2ca5f4 fix: 个人面板无法跳转设置页面 2024-02-02 23:24:36 +08:00
5b1022628c fix: 九图部分位置无法点击 2024-02-02 02:34:59 +08:00
33f61ac0fa fix: 查询在线人数错误时没有返回status 2024-02-02 01:18:06 +08:00
0b349e102e fix:评论区HTML实体转义;逻辑错误短路 2024-02-02 00:56:41 +08:00
81371c5a31 fix: 只有时间的评论区不高亮 2024-02-02 00:56:41 +08:00
85a59e11b9 fix: 修复没有关键词时无法匹配时间、修复不显示关键词时不替换超链接、时间添加中文冒号匹配并提升分支判定严格程度 2024-02-02 00:56:41 +08:00
e24ccc16fa mod: av2bv方法修改 2024-02-01 00:32:52 +08:00
89a43b1285 v1.0.19 更新日志 2024-01-31 23:28:51 +08:00
ea8af28828 fix: 专栏封面图尺寸异常 2024-01-31 23:11:03 +08:00
8a2c023343 fix: magType value 2024-01-31 23:03:45 +08:00
a86fe76e59 Merge branch 'fix-replyReqError' 2024-01-31 22:44:14 +08:00
d703e38c3f fix: avbv转换 2024-01-31 22:43:40 +08:00
9e93b50860 mod: 还原aid 2024-01-31 22:33:04 +08:00
9907967a0a Merge pull request #454 from orz12/feat-whisper-detail-type-and-emoji
feat: 私信显示分享视频内容、富文本表情,补充信息类型枚举
2024-01-31 08:08:29 +08:00
331969cc8d Merge pull request #443 from orz12/opt-video-detail-page
fix: 播放页数个问题
2024-01-31 08:06:26 +08:00
7157f89245 Update main.yml 2024-01-30 23:23:12 +08:00
163bb3c8da v1.0.18 更新 2024-01-30 23:17:00 +08:00
33f71c62df Merge branch 'fix-replyReqError' 2024-01-30 23:15:22 +08:00
365c367cc7 Merge branch 'main' of github.com:guozhigq/pilipala 2024-01-30 23:14:49 +08:00
7ea95e550b Merge branch 'feature-ciActionIpa' 2024-01-30 23:14:24 +08:00
699361e04c fix: aid 2024-01-30 23:11:54 +08:00
e835821f3c Merge pull request #452 from orz12/mod-ai-conclusion-not-support-tips
mod: AI视频总结提示
2024-01-29 23:47:16 +08:00
76d5f6333e Merge branch 'feature-logger' 2024-01-29 23:28:07 +08:00
91899a9537 Merge branch 'fix-replyLoadError' 2024-01-29 23:02:30 +08:00
5591bb3dbf Merge branch 'opt-requestError' 2024-01-29 23:01:30 +08:00
1adbdf127f Merge branch 'main' of github.com:guozhigq/pilipala 2024-01-29 22:58:34 +08:00
8169f5739c fix: 首页tabbar排序无效 2024-01-28 23:44:41 +08:00
127c6734f8 feat: 自动打包ipa文件 2024-01-28 23:15:34 +08:00
a78ce4f6d4 feat: 错误日志记录 2024-01-28 15:52:30 +08:00
22a2628513 fix: 评论区加载超时导致视图崩溃 issues #389 2024-01-27 22:42:04 +08:00
e972d17f1c mod: 优化请求异常信息 2024-01-27 22:37:16 +08:00
e603942b5f fix: 评论区时间正则拼接顺序 2024-01-27 15:49:53 +08:00
0c4bad406e fix: 评论区识别逻辑重构,修复含有关键词的评论重复出现的问题 2024-01-27 14:30:04 +08:00
791047753d Merge pull request #445 from orz12/feat-danmaku-strokewidth
feat: 新增弹幕描边粗细设置,默认值降低
2024-01-27 13:25:28 +08:00
3784f1f87b Merge pull request #468 from guozhigq/feature-modLongImgLabel
mod: 长图标签显示判断
2024-01-27 13:06:40 +08:00
c0162892ef mod: 长图标签显示判断 2024-01-27 13:05:59 +08:00
a64a49df22 Merge pull request #466 from guozhigq/feature-removeRcmdCache
mod: 移除首页推荐视频默认缓存
2024-01-27 10:13:02 +08:00
349de75dfd mod: 移除首页推荐视频默认缓存 2024-01-27 10:12:21 +08:00
1014c26d29 mod: make headers eid issues #435 2024-01-27 01:11:29 +08:00
d1bacf8950 fix: 退出搜索页面控制器未销毁 2024-01-27 00:24:54 +08:00
0595648f4c fix: 搜索页面时长筛选、快速回顶 2024-01-26 23:25:06 +08:00
791eed8a01 Merge pull request #446 from orz12/fix-typo-in-http
fix: typo
2024-01-26 00:16:45 +08:00
9663278916 feat: 私信显示分享视频内容、富文本表情,补充信息类型枚举 2024-01-25 21:22:39 +08:00
1b03f3f31f mod: AI视频总结未支持提醒 2024-01-25 21:15:22 +08:00
a73f2974e1 fix: typo 2024-01-25 20:56:59 +08:00
bf8ae0f317 feat: 新增弹幕描边粗细设置,默认值降低 2024-01-25 20:55:35 +08:00
545def36e6 mod: 自动播放按钮改为官方版 2024-01-25 20:51:01 +08:00
aaeecc9e53 fix: 重力旋转后划出下方的详情页 2024-01-25 20:51:01 +08:00
16895b5c32 fix: 点击视频评论区用户头像后返回详情页灰屏 2024-01-25 20:51:01 +08:00
a68c04001b fix: 竖屏全屏异常 2024-01-25 20:51:01 +08:00
1dd70f482f fix: 旋转横屏仍有状态栏 2024-01-25 20:51:01 +08:00
103423abf7 fix: 修复部分手机横屏两侧不等宽 2024-01-25 20:51:01 +08:00
569184a507 opt: 切换页面时销毁播放器组件提升性能 2024-01-25 20:51:01 +08:00
56 changed files with 1612 additions and 1073 deletions

View File

@ -1,84 +1,157 @@
name: build_apk name: Pilipala Release
# action事件触发 # action事件触发
on: on:
push: push:
# push tag时触发 # push tag时触发
tags: tags:
- 'v*.*.*' - "v*.*.*"
# 可以有多个jobs # 可以有多个jobs
jobs: jobs:
build_apk: android:
# 运行环境 ubuntu-latest window-latest mac-latest # 运行环境 ubuntu-latest window-latest mac-latest
runs-on: ubuntu-latest runs-on: ubuntu-latest
# 每个jobs中可以有多个steps # 每个jobs中可以有多个steps
steps: steps:
- name: 代码迁出 - name: 代码迁出
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 构建Java环境 - name: 构建Java环境
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
distribution: "zulu" distribution: "zulu"
java-version: "17" java-version: "17"
token: ${{secrets.GIT_TOKEN}} token: ${{secrets.GIT_TOKEN}}
- name: 检查缓存 - name: 检查缓存
uses: actions/cache@v2 uses: actions/cache@v2
id: cache-flutter id: cache-flutter
with: with:
path: /root/flutter-sdk # Flutter SDK 的路径 path: /root/flutter-sdk # Flutter SDK 的路径
key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }} key: ${{ runner.os }}-flutter-${{ hashFiles('**/pubspec.lock') }}
- name: 安装Flutter - name: 安装Flutter
if: steps.cache-flutter.outputs.cache-hit != 'true' if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
flutter-version: 3.16.5 flutter-version: 3.16.5
channel: any channel: any
- name: 下载项目依赖 - name: 下载项目依赖
run: flutter pub get run: flutter pub get
- name: 解码生成 jks - name: 解码生成 jks
run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks run: echo $KEYSTORE_BASE64 | base64 -di > android/app/vvex.jks
env: env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
- name: flutter build apk - name: flutter build apk
# 对应 android/app/build.gradle signingConfigs中的配置项 run: flutter build apk --release --split-per-abi
run: flutter build apk --release --split-per-abi env:
env: KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
- name: 获取版本号 - name: flutter build apk
id: version run: flutter build apk --release
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD}}
# - name: 获取当前日期 - name: 获取版本号
# id: date id: version
# run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
- name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk # - name: 获取当前日期
run: | # id: date
# DATE=${{ steps.date.outputs.date }} # run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
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: 构建和发布release - name: 重命名应用
uses: ncipollo/release-action@v1 run: |
with: # DATE=${{ steps.date.outputs.date }}
# release title for file in build/app/outputs/flutter-apk/app-*.apk; do
name: v${{ steps.version.outputs.version }} if [[ $file =~ app-(.?*)release.apk ]]; then
artifacts: "build/app/outputs/flutter-apk/Pili-*.apk" new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}${{ steps.version.outputs.version }}.apk"
bodyFile: "change_log/${{steps.version.outputs.version}}.md" mv "$file" "$new_file_name"
token: ${{ secrets.GIT_TOKEN }} fi
allowUpdates: true 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/*

View File

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

16
change_log/1.0.18.0130.md Normal file
View File

@ -0,0 +1,16 @@
## 1.0.18
### 功能
### 修复
### 优化
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

15
change_log/1.0.19.0131.md Normal file
View File

@ -0,0 +1,15 @@
## 1.0.19
### 修复
+ 视频404、评论加载错误
+ bvav转换
### 优化
+ 视频详情页内存占用
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

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

View File

@ -185,7 +185,7 @@ class Api {
static const String searchDefault = '/x/web-interface/wbi/search/default'; static const String searchDefault = '/x/web-interface/wbi/search/default';
// 搜索关键词 // 搜索关键词
static const String serachSuggest = static const String searchSuggest =
'https://s.search.bilibili.com/main/suggest'; 'https://s.search.bilibili.com/main/suggest';
// 分类搜索 // 分类搜索

View File

@ -8,6 +8,7 @@ import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart';
// import 'package:dio_http2_adapter/dio_http2_adapter.dart'; // import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
import '../utils/utils.dart'; import '../utils/utils.dart';
import 'constants.dart'; import 'constants.dart';
@ -77,10 +78,11 @@ class Request {
static setOptionsHeaders(userInfo, bool status) { static setOptionsHeaders(userInfo, bool status) {
if (status) { if (status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
dio.options.headers['x-bili-aurora-eid'] =
IdUtils.genAuroraEid(userInfo.mid);
} }
dio.options.headers['env'] = 'prod'; dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64'; dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
dio.options.headers['x-bili-aurora-zone'] = 'sh001'; dio.options.headers['x-bili-aurora-zone'] = 'sh001';
dio.options.headers['referer'] = 'https://www.bilibili.com/'; dio.options.headers['referer'] = 'https://www.bilibili.com/';
} }
@ -177,8 +179,14 @@ class Request {
); );
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
print('get error: $e'); Response errResponse = Response(
return Future.error(await ApiInterceptor.dioError(e)); data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200,
requestOptions: RequestOptions(),
);
return errResponse;
} }
} }
@ -199,8 +207,14 @@ class Request {
// print('post success: ${response.data}'); // print('post success: ${response.data}');
return response; return response;
} on DioException catch (e) { } on DioException catch (e) {
print('post error: $e'); Response errResponse = Response(
return Future.error(await ApiInterceptor.dioError(e)); data: {
'message': await ApiInterceptor.dioError(e)
}, // 将自定义 Map 数据赋值给 Response 的 data 属性
statusCode: 200,
requestOptions: RequestOptions(),
);
return errResponse;
} }
} }

View File

@ -5,7 +5,6 @@ import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import '../utils/storage.dart'; import '../utils/storage.dart';
// import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor { class ApiInterceptor extends Interceptor {
@override @override
@ -70,36 +69,29 @@ class ApiInterceptor extends Interceptor {
case DioExceptionType.sendTimeout: case DioExceptionType.sendTimeout:
return '发送请求超时,请检查网络设置'; return '发送请求超时,请检查网络设置';
case DioExceptionType.unknown: case DioExceptionType.unknown:
final String res = await checkConect(); final String res = await checkConnect();
return '$res \n 网络异常,请稍后重试'; return '$res,网络异常';
// default:
// return 'Dio异常';
} }
} }
static Future<String> checkConect() async { static Future<String> checkConnect() async {
final ConnectivityResult connectivityResult = final ConnectivityResult connectivityResult =
await Connectivity().checkConnectivity(); await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.mobile) { switch (connectivityResult) {
return 'connected with mobile network'; case ConnectivityResult.mobile:
} else if (connectivityResult == ConnectivityResult.wifi) { return '正在使用移动流量';
return 'connected with wifi network'; case ConnectivityResult.wifi:
} else if (connectivityResult == ConnectivityResult.ethernet) { return '正在使用wifi';
// I am connected to a ethernet network. case ConnectivityResult.ethernet:
return ''; return '正在使用局域网';
} else if (connectivityResult == ConnectivityResult.vpn) { case ConnectivityResult.vpn:
// I am connected to a vpn network. return '正在使用代理网络';
// Note for iOS and macOS: case ConnectivityResult.other:
// There is no separate network interface type for [vpn]. return '正在使用其他网络';
// It returns [other] on any device (also simulator) case ConnectivityResult.none:
return ''; return '未连接到任何网络';
} else if (connectivityResult == ConnectivityResult.other) { default:
// I am connected to a network which is not in the above mentioned networks. return '';
return '';
} else if (connectivityResult == ConnectivityResult.none) {
return 'not connected to any network';
} else {
return '';
} }
} }
} }

View File

@ -36,7 +36,7 @@ class SearchHttp {
// 获取搜索建议 // 获取搜索建议
static Future searchSuggest({required term}) async { static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest, var res = await Request().get(Api.searchSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term}); data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data is String) { if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data); Map<String, dynamic> resultMap = json.decode(res.data);

View File

@ -224,10 +224,11 @@ class VideoHttp {
// 获取投币状态 // 获取投币状态
static Future hasCoinVideo({required String bvid}) async { static Future hasCoinVideo({required String bvid}) async {
var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid}); var res = await Request().get(Api.hasCoinVideo, data: {'bvid': bvid});
print('res: $res');
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': true, 'data': []}; return {'status': false, 'data': []};
} }
} }
@ -361,7 +362,7 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': true, 'data': []}; return {'status': false, 'data': []};
} }
} }
@ -377,7 +378,7 @@ class VideoHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else { } else {
return {'status': true, 'data': []}; return {'status': false, 'data': []};
} }
} }
@ -433,6 +434,8 @@ class VideoHttp {
}); });
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']}; return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'data': null, 'msg': res.data['message']};
} }
} }
@ -447,11 +450,13 @@ class VideoHttp {
'up_mid': upMid, 'up_mid': upMid,
}); });
var res = await Request().get(Api.aiConclusion, data: params); 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 { return {
'status': true, 'status': true,
'data': AiConclusionModel.fromJson(res.data['data']), 'data': AiConclusionModel.fromJson(res.data['data']),
}; };
} else {
return {'status': false, 'data': []};
} }
} }
} }

View File

@ -21,6 +21,8 @@ import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart'; import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc. 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 { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -32,7 +34,32 @@ void main() async {
await setupServiceLocator(); await setupServiceLocator();
Request(); Request();
await Request.setCookie(); 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.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(

View File

@ -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];
}

View File

@ -17,8 +17,9 @@ class LatestDataModel {
url = json['url']; url = json['url'];
tagName = json['tag_name']; tagName = json['tag_name'];
createdAt = json['created_at']; createdAt = json['created_at'];
assets = assets = json['assets'] != null
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList(); ? json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList()
: [];
body = json['body']; body = json['body'];
} }
} }

View File

@ -1,8 +1,3 @@
import 'package:hive/hive.dart';
part 'result.g.dart';
@HiveType(typeId: 0)
class RecVideoItemAppModel { class RecVideoItemAppModel {
RecVideoItemAppModel({ RecVideoItemAppModel({
this.id, this.id,
@ -27,47 +22,27 @@ class RecVideoItemAppModel {
this.adInfo, this.adInfo,
}); });
@HiveField(0)
int? id; int? id;
@HiveField(1)
int? aid; int? aid;
@HiveField(2)
String? bvid; String? bvid;
@HiveField(3)
int? cid; int? cid;
@HiveField(4)
String? pic; String? pic;
@HiveField(5)
RcmdStat? stat; RcmdStat? stat;
@HiveField(6)
String? duration; String? duration;
@HiveField(7)
String? title; String? title;
@HiveField(8)
int? isFollowed; int? isFollowed;
@HiveField(9)
RcmdOwner? owner; RcmdOwner? owner;
@HiveField(10)
RcmdReason? rcmdReason; RcmdReason? rcmdReason;
@HiveField(11)
String? goto; String? goto;
@HiveField(12)
int? param; int? param;
@HiveField(13)
String? uri; String? uri;
@HiveField(14)
String? talkBack; String? talkBack;
// 番剧 // 番剧
@HiveField(15)
String? bangumiView; String? bangumiView;
@HiveField(16)
String? bangumiFollow; String? bangumiFollow;
@HiveField(17)
String? bangumiBadge; String? bangumiBadge;
@HiveField(18)
String? cardType; String? cardType;
@HiveField(19)
Map? adInfo; Map? adInfo;
RecVideoItemAppModel.fromJson(Map<String, dynamic> json) { RecVideoItemAppModel.fromJson(Map<String, dynamic> json) {
@ -102,18 +77,14 @@ class RecVideoItemAppModel {
} }
} }
@HiveType(typeId: 1)
class RcmdStat { class RcmdStat {
RcmdStat({ RcmdStat({
this.view, this.view,
this.like, this.like,
this.danmu, this.danmu,
}); });
@HiveField(0)
String? view; String? view;
@HiveField(1)
String? like; String? like;
@HiveField(2)
String? danmu; String? danmu;
RcmdStat.fromJson(Map<String, dynamic> json) { RcmdStat.fromJson(Map<String, dynamic> json) {
@ -122,13 +93,10 @@ class RcmdStat {
} }
} }
@HiveType(typeId: 2)
class RcmdOwner { class RcmdOwner {
RcmdOwner({this.name, this.mid}); RcmdOwner({this.name, this.mid});
@HiveField(0)
String? name; String? name;
@HiveField(1)
int? mid; int? mid;
RcmdOwner.fromJson(Map<String, dynamic> json) { RcmdOwner.fromJson(Map<String, dynamic> json) {
@ -141,13 +109,11 @@ class RcmdOwner {
} }
} }
@HiveType(typeId: 8)
class RcmdReason { class RcmdReason {
RcmdReason({ RcmdReason({
this.content, this.content,
}); });
@HiveField(0)
String? content; String? content;
RcmdReason.fromJson(Map<String, dynamic> json) { RcmdReason.fromJson(Map<String, dynamic> json) {

View File

@ -1,209 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'result.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemAppModelAdapter extends TypeAdapter<RecVideoItemAppModel> {
@override
final int typeId = 0;
@override
RecVideoItemAppModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemAppModel(
id: fields[0] as int?,
aid: fields[1] as int?,
bvid: fields[2] as String?,
cid: fields[3] as int?,
pic: fields[4] as String?,
stat: fields[5] as RcmdStat?,
duration: fields[6] as String?,
title: fields[7] as String?,
isFollowed: fields[8] as int?,
owner: fields[9] as RcmdOwner?,
rcmdReason: fields[10] as RcmdReason?,
goto: fields[11] as String?,
param: fields[12] as int?,
uri: fields[13] as String?,
talkBack: fields[14] as String?,
bangumiView: fields[15] as String?,
bangumiFollow: fields[16] as String?,
bangumiBadge: fields[17] as String?,
cardType: fields[18] as String?,
adInfo: (fields[19] as Map?)?.cast<dynamic, dynamic>(),
);
}
@override
void write(BinaryWriter writer, RecVideoItemAppModel obj) {
writer
..writeByte(20)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.aid)
..writeByte(2)
..write(obj.bvid)
..writeByte(3)
..write(obj.cid)
..writeByte(4)
..write(obj.pic)
..writeByte(5)
..write(obj.stat)
..writeByte(6)
..write(obj.duration)
..writeByte(7)
..write(obj.title)
..writeByte(8)
..write(obj.isFollowed)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.rcmdReason)
..writeByte(11)
..write(obj.goto)
..writeByte(12)
..write(obj.param)
..writeByte(13)
..write(obj.uri)
..writeByte(14)
..write(obj.talkBack)
..writeByte(15)
..write(obj.bangumiView)
..writeByte(16)
..write(obj.bangumiFollow)
..writeByte(17)
..write(obj.bangumiBadge)
..writeByte(18)
..write(obj.cardType)
..writeByte(19)
..write(obj.adInfo);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemAppModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdStatAdapter extends TypeAdapter<RcmdStat> {
@override
final int typeId = 1;
@override
RcmdStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdStat(
view: fields[0] as String?,
like: fields[1] as String?,
danmu: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdStat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdOwnerAdapter extends TypeAdapter<RcmdOwner> {
@override
final int typeId = 2;
@override
RcmdOwner read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdOwner(
name: fields[0] as String?,
mid: fields[1] as int?,
);
}
@override
void write(BinaryWriter writer, RcmdOwner obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.mid);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdOwnerAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
@override
final int typeId = 8;
@override
RcmdReason read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdReason(
content: fields[0] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdReason obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.content);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdReasonAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -166,7 +166,7 @@ class SessionMsgDataModel {
int? hasMore; int? hasMore;
int? minSeqno; int? minSeqno;
int? maxSeqno; int? maxSeqno;
List? eInfos; List<dynamic>? eInfos;
SessionMsgDataModel.fromJson(Map<String, dynamic> json) { SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
messages = json['messages'] messages = json['messages']

View File

@ -1,14 +1,8 @@
import 'package:hive/hive.dart';
part 'hot.g.dart';
@HiveType(typeId: 6)
class HotSearchModel { class HotSearchModel {
HotSearchModel({ HotSearchModel({
this.list, this.list,
}); });
@HiveField(0)
List<HotSearchItem>? list; List<HotSearchItem>? list;
HotSearchModel.fromJson(Map<String, dynamic> json) { HotSearchModel.fromJson(Map<String, dynamic> json) {
@ -18,7 +12,6 @@ class HotSearchModel {
} }
} }
@HiveType(typeId: 7)
class HotSearchItem { class HotSearchItem {
HotSearchItem({ HotSearchItem({
this.keyword, this.keyword,
@ -27,20 +20,19 @@ class HotSearchItem {
this.icon, this.icon,
}); });
@HiveField(0)
String? keyword; String? keyword;
@HiveField(1)
String? showName; String? showName;
// 4/5热 11话题 8普通 7直播 // 4/5热 11话题 8普通 7直播
@HiveField(2)
int? wordType; int? wordType;
@HiveField(3)
String? icon; String? icon;
List? liveId;
HotSearchItem.fromJson(Map<String, dynamic> json) { HotSearchItem.fromJson(Map<String, dynamic> json) {
keyword = json['keyword']; keyword = json['keyword'];
showName = json['show_name']; showName = json['show_name'];
wordType = json['word_type']; wordType = json['word_type'];
icon = json['icon']; icon = json['icon'];
liveId = json['live_id'];
liveId = json['live_id'];
} }
} }

View File

@ -1,84 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hot.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HotSearchModelAdapter extends TypeAdapter<HotSearchModel> {
@override
final int typeId = 6;
@override
HotSearchModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HotSearchModel(
list: (fields[0] as List?)?.cast<HotSearchItem>(),
);
}
@override
void write(BinaryWriter writer, HotSearchModel obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.list);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HotSearchModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class HotSearchItemAdapter extends TypeAdapter<HotSearchItem> {
@override
final int typeId = 7;
@override
HotSearchItem read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HotSearchItem(
keyword: fields[0] as String?,
showName: fields[1] as String?,
wordType: fields[2] as int?,
icon: fields[3] as String?,
);
}
@override
void write(BinaryWriter writer, HotSearchItem obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.keyword)
..writeByte(1)
..write(obj.showName)
..writeByte(2)
..write(obj.wordType)
..writeByte(3)
..write(obj.icon);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HotSearchItemAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -133,6 +133,11 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'), title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), 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, mode: LaunchMode.externalApplication,
); );
} }
// 日志
logs() {
Get.toNamed('/logs');
}
} }

View File

@ -35,6 +35,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
late double opacityVal; late double opacityVal;
late double fontSizeVal; late double fontSizeVal;
late double danmakuDurationVal; late double danmakuDurationVal;
late double strokeWidth;
int latestAddedPosition = -1; int latestAddedPosition = -1;
@override @override
@ -65,6 +66,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
showArea = playerController.showArea; showArea = playerController.showArea;
opacityVal = playerController.opacityVal; opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal; fontSizeVal = playerController.fontSizeVal;
strokeWidth = playerController.strokeWidth;
danmakuDurationVal = playerController.danmakuDurationVal; danmakuDurationVal = playerController.danmakuDurationVal;
} }
@ -136,6 +138,7 @@ class _PlDanmakuState extends State<PlDanmaku> {
hideBottom: blockTypes.contains(4), hideBottom: blockTypes.contains(4),
duration: duration:
danmakuDurationVal / playerController.playbackSpeed, danmakuDurationVal / playerController.playbackSpeed,
strokeWidth: strokeWidth,
// initDuration / // initDuration /
// (danmakuSpeedVal * widget.playerController.playbackSpeed), // (danmakuSpeedVal * widget.playerController.playbackSpeed),
), ),

View File

@ -1,5 +1,6 @@
// 内容 // 内容
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
@ -80,7 +81,7 @@ class _ContentState extends State<Content> {
height: height, height: height,
), ),
), ),
height > maxHeight height > Get.size.height * 0.9
? const PBadge( ? const PBadge(
text: '长图', text: '长图',
right: 8, right: 8,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart'; import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
@ -87,7 +88,7 @@ Widget picWidget(item, context) {
childAspectRatio: aspectRatio, childAspectRatio: aspectRatio,
children: list, children: list,
), ),
if (len == 1 && origAspectRatio < 0.4) if (len == 1 && height > Get.size.height * 0.9)
const PBadge( const PBadge(
text: '长图', text: '长图',
top: null, top: null,

View File

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart'; import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../http/index.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin { class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false; bool flag = false;
@ -24,6 +25,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
late bool hideSearchBar; late bool hideSearchBar;
late List defaultTabs; late List defaultTabs;
late List<String> tabbarSort; late List<String> tabbarSort;
RxString defaultSearch = ''.obs;
@override @override
void onInit() { void onInit() {
@ -35,6 +37,9 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
setTabConfig(); setTabConfig();
hideSearchBar = hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}
} }
void onRefresh() { void onRefresh() {
@ -58,13 +63,16 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
} }
void setTabConfig() async { void setTabConfig() async {
defaultTabs = tabsConfig; defaultTabs = [...tabsConfig];
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); 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 tabs.value = defaultTabs;
.where((i) => tabbarSort.contains((i['type'] as TabType).id))
.toList();
if (tabbarSort.contains(TabType.rcmd.id)) { if (tabbarSort.contains(TabType.rcmd.id)) {
initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id); initialIndex.value = tabbarSort.indexOf(TabType.rcmd.id);
@ -94,4 +102,11 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
} }
}); });
} }
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
defaultSearch.value = res.data['data']['name'];
}
}
} }

View File

@ -5,7 +5,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/mine/index.dart'; import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import './controller.dart'; import './controller.dart';
@ -144,6 +143,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0), padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0),
child: UserInfoWidget( child: UserInfoWidget(
top: top, top: top,
ctr: ctr,
userLogin: isUserLoggedIn, userLogin: isUserLoggedIn,
userFace: ctr?.userFace.value, userFace: ctr?.userFace.value,
callback: () => callback!(), callback: () => callback!(),
@ -162,18 +162,20 @@ class UserInfoWidget extends StatelessWidget {
required this.userLogin, required this.userLogin,
required this.userFace, required this.userFace,
required this.callback, required this.callback,
required this.ctr,
}) : super(key: key); }) : super(key: key);
final double top; final double top;
final RxBool userLogin; final RxBool userLogin;
final String? userFace; final String? userFace;
final VoidCallback? callback; final VoidCallback? callback;
final HomeController? ctr;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
const SearchBar(), SearchBar(ctr: ctr),
if (userLogin.value) ...[ if (userLogin.value) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
ClipRect( ClipRect(
@ -335,11 +337,15 @@ class CustomChip extends StatelessWidget {
} }
class SearchBar extends StatelessWidget { class SearchBar extends StatelessWidget {
const SearchBar({super.key}); const SearchBar({
Key? key,
required this.ctr,
}) : super(key: key);
final HomeController? ctr;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final SSearchController searchController = Get.put(SSearchController());
final ColorScheme colorScheme = Theme.of(context).colorScheme; final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Expanded( return Expanded(
child: Container( child: Container(
@ -353,7 +359,10 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer.withOpacity(0.05), color: colorScheme.onSecondaryContainer.withOpacity(0.05),
child: InkWell( child: InkWell(
splashColor: colorScheme.primaryContainer.withOpacity(0.3), splashColor: colorScheme.primaryContainer.withOpacity(0.3),
onTap: () => Get.toNamed('/search'), onTap: () => Get.toNamed(
'/search',
parameters: {'hintText': ctr!.defaultSearch.value},
),
child: Row( child: Row(
children: [ children: [
const SizedBox(width: 14), const SizedBox(width: 14),
@ -362,14 +371,12 @@ class SearchBar extends StatelessWidget {
color: colorScheme.onSecondaryContainer, color: colorScheme.onSecondaryContainer,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Obx(
child: Obx( () => Text(
() => Text( ctr!.defaultSearch.value,
searchController.defaultSearch.value, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: TextStyle(color: colorScheme.outline),
style: TextStyle(color: colorScheme.outline),
),
), ),
), ),
], ],

View File

@ -11,6 +11,7 @@ import 'package:pilipala/pages/home/view.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import '../../models/common/dynamic_badge_mode.dart';
class MainController extends GetxController { class MainController extends GetxController {
List<Widget> pages = <Widget>[ List<Widget> pages = <Widget>[
@ -65,6 +66,7 @@ class MainController extends GetxController {
int selectedIndex = 0; int selectedIndex = 0;
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs; RxBool userLogin = false.obs;
late Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
@override @override
void onInit() { void onInit() {
@ -75,7 +77,12 @@ class MainController extends GetxController {
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
var userInfo = userInfoCache.get('userInfoCache'); var userInfo = userInfoCache.get('userInfoCache');
userLogin.value = userInfo != null; 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) { void onBackPressed(BuildContext context) {

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.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/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/media/index.dart';
@ -127,11 +128,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
destinations: <Widget>[ destinations: <Widget>[
..._mainController.navigationBars.map((e) { ..._mainController.navigationBars.map((e) {
return NavigationDestination( return NavigationDestination(
icon: Badge( icon: Obx(
label: Text(e['count'].toString()), () => Badge(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), label:
isLabelVisible: e['count'] > 0, _mainController.dynamicBadgeType.value ==
child: e['icon'], 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'], selectedIcon: e['selectIcon'],
label: e['label'], label: e['label'],
@ -148,11 +159,21 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
items: [ items: [
..._mainController.navigationBars.map((e) { ..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem( return BottomNavigationBarItem(
icon: Badge( icon: Obx(
label: Text(e['count'].toString()), () => Badge(
padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), label:
isLabelVisible: e['count'] > 0, _mainController.dynamicBadgeType.value ==
child: e['icon'], 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'], activeIcon: e['selectIcon'],
label: e['label'], label: e['label'],

View File

@ -64,7 +64,7 @@ class _MinePageState extends State<MinePage> {
), ),
), ),
IconButton( IconButton(
onPressed: () => Get.toNamed('/setting'), onPressed: () => Get.toNamed('/setting', preventDuplicates: false),
icon: const Icon( icon: const Icon(
CupertinoIcons.slider_horizontal_3, CupertinoIcons.slider_horizontal_3,
), ),

View File

@ -13,7 +13,6 @@ class RcmdController extends GetxController {
RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs; RxList<RecVideoItemModel> webVideoList = <RecVideoItemModel>[].obs;
bool isLoadingMore = true; bool isLoadingMore = true;
OverlayEntry? popupDialog; OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
Box setting = GStrorage.setting; Box setting = GStrorage.setting;
RxInt crossAxisCount = 2.obs; RxInt crossAxisCount = 2.obs;
late bool enableSaveLastData; late bool enableSaveLastData;
@ -24,15 +23,6 @@ class RcmdController extends GetxController {
super.onInit(); super.onInit();
crossAxisCount.value = crossAxisCount.value =
setting.get(SettingBoxKey.customRows, defaultValue: 2); setting.get(SettingBoxKey.customRows, defaultValue: 2);
// 读取app端缓存内容
// if (recVideo.get('cacheList') != null &&
// recVideo.get('cacheList').isNotEmpty) {
// List<RecVideoItemAppModel> list = [];
// for (var i in recVideo.get('cacheList')) {
// list.add(i);
// }
// videoList.value = list;
// }
enableSaveLastData = enableSaveLastData =
setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false); setting.get(SettingBoxKey.enableSaveLastData, defaultValue: false);
defaultRcmdType = defaultRcmdType =
@ -77,7 +67,6 @@ class RcmdController extends GetxController {
} else if (type == 'onLoad') { } else if (type == 'onLoad') {
appVideoList.addAll(res['data']); appVideoList.addAll(res['data']);
} }
recVideo.put('cacheList', res['data']);
_currentPage += 1; _currentPage += 1;
} }
isLoadingMore = false; isLoadingMore = false;

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/search.dart'; import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
@ -27,13 +26,10 @@ class SSearchController extends GetxController {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) {
searchDefault();
}
// 其他页面跳转过来 // 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) { if (Get.parameters.keys.isNotEmpty) {
if (Get.parameters['keyword'] != null) { if (Get.parameters['keyword'] != null) {
onClickKeyword(Get.parameters['keyword']!); onClickKeyword(Get.parameters['keyword']!, null);
} }
if (Get.parameters['hintText'] != null) { if (Get.parameters['hintText'] != null) {
hintText = Get.parameters['hintText']!; hintText = Get.parameters['hintText']!;
@ -92,7 +88,12 @@ class SSearchController extends GetxController {
} }
// 点击热搜关键词 // 点击热搜关键词
void onClickKeyword(String keyword) { void onClickKeyword(String keyword, item) {
if (item != null && item.wordType == 7) {
Get.toNamed('/liveRoom?roomid=${item.liveId.first}',
arguments: {'liveItem': null, 'heroTag': '${item.liveId.first}'});
return;
}
searchKeyWord.value = keyword; searchKeyWord.value = keyword;
controller.value.text = keyword; controller.value.text = keyword;
// 移动光标 // 移动光标
@ -130,12 +131,4 @@ class SSearchController extends GetxController {
historyList.refresh(); historyList.refresh();
histiryWord.put('cacheList', []); histiryWord.put('cacheList', []);
} }
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
searchKeyWord.value =
hintText = defaultSearch.value = res.data['data']['name'];
}
}
} }

View File

@ -115,8 +115,8 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
customBorder: RoundedRectangleBorder( customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
onTap: () => ssCtr onTap: () => ssCtr.onClickKeyword(
.onClickKeyword(ssCtr.searchSuggestList[index].term!), ssCtr.searchSuggestList[index].term!, null),
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9), padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9),
child: ssCtr.searchSuggestList[index].textRich, child: ssCtr.searchSuggestList[index].textRich,
@ -178,11 +178,11 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
width: width, width: width,
// ignore: invalid_use_of_protected_member // ignore: invalid_use_of_protected_member
hotSearchList: _searchController.hotSearchList.value, hotSearchList: _searchController.hotSearchList.value,
onClick: (keyword) async { onClick: (keyword, item) async {
_searchController.searchFocusNode.unfocus(); _searchController.searchFocusNode.unfocus();
await Future.delayed( await Future.delayed(
const Duration(milliseconds: 150)); const Duration(milliseconds: 150));
_searchController.onClickKeyword(keyword); _searchController.onClickKeyword(keyword, item);
}, },
), ),
); );
@ -193,15 +193,7 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
); );
} }
} else { } else {
// 缓存数据 return const SizedBox();
if (_searchController.hotSearchList.isNotEmpty) {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
);
} else {
return const SizedBox();
}
} }
}, },
); );

View File

@ -26,7 +26,7 @@ class HotKeyword extends StatelessWidget {
borderRadius: BorderRadius.circular(3), borderRadius: BorderRadius.circular(3),
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () => onClick!(i.keyword), onTap: () => onClick!(i.keyword, i),
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 2, left: 2,

View File

@ -25,16 +25,17 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 5, StyleString.safeSpace, 5), StyleString.safeSpace, 5, StyleString.safeSpace, 5),
child: LayoutBuilder(builder: (context, boxConstraints) { child: LayoutBuilder(builder: (context, boxConstraints) {
double width = (boxConstraints.maxWidth - final double width = (boxConstraints.maxWidth -
StyleString.cardSpace * StyleString.cardSpace *
6 / 6 /
MediaQuery.textScalerOf(context).scale(2.0)); MediaQuery.textScalerOf(context).scale(1.0)) /
2;
return Container( return Container(
constraints: const BoxConstraints(minHeight: 88), constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio, height: width / StyleString.aspectRatio,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: <Widget>[
if (list[index].imageUrls != null && if (list[index].imageUrls != null &&
list[index].imageUrls.isNotEmpty) list[index].imageUrls.isNotEmpty)
AspectRatio( AspectRatio(

View File

@ -90,7 +90,7 @@ class SearchVideoPanel extends StatelessWidget {
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero), padding: MaterialStateProperty.all(EdgeInsets.zero),
), ),
onPressed: () => controller.onShowFilterDialog(), onPressed: () => controller.onShowFilterDialog(ctr),
icon: Icon( icon: Icon(
Icons.filter_list_outlined, Icons.filter_list_outlined,
size: 18, size: 18,
@ -175,7 +175,7 @@ class VideoPanelController extends GetxController {
super.onInit(); super.onInit();
} }
onShowFilterDialog() { onShowFilterDialog(searchPanelCtr) {
SmartDialog.show( SmartDialog.show(
animationType: SmartAnimationType.centerFade_otherSlide, animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -199,7 +199,8 @@ class VideoPanelController extends GetxController {
SmartDialog.dismiss(); SmartDialog.dismiss();
SmartDialog.showToast("${i['label']}」的筛选结果"); SmartDialog.showToast("${i['label']}」的筛选结果");
SearchPanelController ctr = SearchPanelController ctr =
Get.find<SearchPanelController>(tag: 'video'); Get.find<SearchPanelController>(
tag: 'video${searchPanelCtr.keyword!}');
ctr.duration.value = i['value']; ctr.duration.value = i['value'];
SmartDialog.showLoading(msg: 'loooad'); SmartDialog.showLoading(msg: 'loooad');
await ctr.onRefresh(); await ctr.onRefresh();

View File

@ -86,7 +86,8 @@ class _SearchResultPageState extends State<SearchResultPage>
onTap: (index) { onTap: (index) {
if (index == _searchResultController!.tabIndex) { if (index == _searchResultController!.tabIndex) {
Get.find<SearchPanelController>( Get.find<SearchPanelController>(
tag: SearchType.values[index].type) tag: SearchType.values[index].type +
_searchResultController!.keyword!)
.animateToTop(); .animateToTop();
} }

View File

@ -7,6 +7,9 @@ import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/login.dart'; import 'package:pilipala/utils/login.dart';
import 'package:pilipala/utils/storage.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 { class SettingController extends GetxController {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
@ -19,6 +22,7 @@ class SettingController extends GetxController {
RxInt picQuality = 10.obs; RxInt picQuality = 10.obs;
Rx<ThemeType> themeType = ThemeType.system.obs; Rx<ThemeType> themeType = ThemeType.system.obs;
var userInfo; var userInfo;
Rx<DynamicBadgeMode> dynamicBadgeType = DynamicBadgeMode.number.obs;
@override @override
void onInit() { void onInit() {
@ -33,6 +37,9 @@ class SettingController extends GetxController {
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode, themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)]; defaultValue: ThemeType.system.code)];
dynamicBadgeType.value = DynamicBadgeMode.values[setting.get(
SettingBoxKey.dynamicBadgeMode,
defaultValue: DynamicBadgeMode.number.code)];
} }
loginOut() async { loginOut() async {
@ -76,4 +83,31 @@ class SettingController extends GetxController {
feedBackEnable.value = !feedBackEnable.value; feedBackEnable.value = !feedBackEnable.value;
setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value); setting.put(SettingBoxKey.feedBackEnable, feedBackEnable.value);
} }
// 设置动态未读标记
setDynamicBadgeMode(BuildContext context) async {
DynamicBadgeMode? result = await showDialog(
context: context,
builder: (context) {
return SelectDialog<DynamicBadgeMode>(
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('设置成功');
}
}
} }

View File

@ -22,6 +22,17 @@ class _TabbarSetPageState extends State<TabbarSetPage> {
defaultTabs = tabsConfig; defaultTabs = tabsConfig;
tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort,
defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); 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() { void saveEdit() {

View File

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

View File

@ -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/pages/setting/widgets/slide_dialog.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import '../../models/common/dynamic_badge_mode.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/switch_item.dart'; import 'widgets/switch_item.dart';
@ -241,6 +242,14 @@ class _StyleSettingState extends State<StyleSetting> {
'当前模式:${settingController.themeType.value.description}', '当前模式:${settingController.themeType.value.description}',
style: subTitleStyle)), style: subTitleStyle)),
), ),
ListTile(
dense: false,
onTap: () => settingController.setDynamicBadgeMode(context),
title: Text('动态未读标记', style: titleStyle),
subtitle: Obx(() => Text(
'当前标记样式:${settingController.dynamicBadgeType.value.description}',
style: subTitleStyle)),
),
ListTile( ListTile(
dense: false, dense: false,
onTap: () => Get.toNamed('/colorSetting'), onTap: () => Get.toNamed('/colorSetting'),

View File

@ -148,7 +148,9 @@ class VideoIntroController extends GetxController {
// 获取投币状态 // 获取投币状态
Future queryHasCoinVideo() async { Future queryHasCoinVideo() async {
var result = await VideoHttp.hasCoinVideo(bvid: bvid); 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 { Future actionLikeVideo() async {
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value); var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) { if (result['status']) {
// hasLike.value = result["data"] == 1 ? true : false; // hasLike.value = result["data"] == 1 ? true : false;
@ -570,10 +576,12 @@ class VideoIntroController extends GetxController {
cid: lastPlayCid.value, cid: lastPlayCid.value,
upMid: videoDetail.value.owner!.mid!, upMid: videoDetail.value.owner!.mid!,
); );
SmartDialog.dismiss();
if (res['status']) { if (res['status']) {
modelResult = res['data'].modelResult; modelResult = res['data'].modelResult;
} else {
SmartDialog.showToast("当前视频可能暂不支持AI视频总结");
} }
SmartDialog.dismiss();
return res; return res;
} }
} }

View File

@ -4,7 +4,7 @@ import 'package:pilipala/http/video.dart';
class ReleatedController extends GetxController { class ReleatedController extends GetxController {
// 视频aid // 视频aid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid'] ?? "";
// 推荐视频列表 // 推荐视频列表
List relatedVideoList = []; List relatedVideoList = [];

View File

@ -256,7 +256,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
// 请求错误 // 请求错误
return HttpError( return HttpError(
errMsg: data['msg'], errMsg: data['msg'],
fn: () => setState(() {}), fn: () {
setState(() {
_futureBuilderFuture =
_videoReplyController.queryReplyList();
});
},
); );
} }
} else { } else {

View File

@ -1,5 +1,6 @@
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.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:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -539,18 +540,6 @@ InlineSpan buildContent(
// replyReply 查看二楼回复(回复详情)回调 // replyReply 查看二楼回复(回复详情)回调
// fReplyItem 父级回复内容,用作二楼回复(回复详情)展示 // fReplyItem 父级回复内容,用作二楼回复(回复详情)展示
final content = replyItem.content; 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<InlineSpan> spanChilds = <InlineSpan>[]; final List<InlineSpan> spanChilds = <InlineSpan>[];
bool hasMatchMember = false; bool hasMatchMember = false;
@ -582,258 +571,171 @@ InlineSpan buildContent(
}); });
} }
// content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' '); // content.message = content.message.replaceAll(RegExp(r"\{vote:.*?\}"), ' ');
if (content.message.contains('&amp;')) { content.message = content.message.replaceAll('&amp;', '&')
content.message = content.message.replaceAll('&amp;', '&'); .replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&quot;', '"')
.replaceAll('&apos;', "'")
.replaceAll('&nbsp;', ' ');
// print("content.jumpUrl.keys:" + content.jumpUrl.keys.toString());
// 构建正则表达式
final List<String> specialTokens = [
...content.emote.keys,
...content.atNameToMid.keys.map((e) => '@$e'),
...content.jumpUrl.keys.map((e) =>
e.replaceAll('?', '\\?').replaceAll('+', '\\+').replaceAll('*', '\\*')),
];
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<String> matchedStrs = [];
void addPlainTextSpan(str){
spanChilds.add(TextSpan(
text: str,
recognizer: TapGestureRecognizer()
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
}
// 分割文本并处理每个部分
content.message.splitMapJoin( content.message.splitMapJoin(
RegExp(r"\[.*?\]"), pattern,
onMatch: (Match match) { onMatch: (Match match) {
final String matchStr = match[0]!; String matchStr = match[0]!;
if (content.emote.isNotEmpty && if (content.emote.containsKey(matchStr)) {
matchStr.indexOf('[') == matchStr.lastIndexOf('[') && // 处理表情
matchStr.indexOf(']') == matchStr.lastIndexOf(']')) {
final int size = content.emote[matchStr]['meta']['size']; final int size = content.emote[matchStr]['meta']['size'];
if (content.emote.keys.contains(matchStr)) { spanChilds.add(WidgetSpan(
spanChilds.add( child: NetworkImgLayer(
WidgetSpan( src: content.emote[matchStr]['url'],
child: NetworkImgLayer( type: 'emote',
src: content.emote[matchStr]['url'], width: size * 20,
type: 'emote', height: size * 20,
width: size * 20, ),
height: size * 20, ));
), } else if (matchStr.startsWith("@") &&
), content.atNameToMid.containsKey(matchStr.substring(1))) {
); // 处理@用户
} else { final String userName = matchStr.substring(1);
spanChilds.add(TextSpan( final int userId = content.atNameToMid[userName];
text: matchStr, spanChilds.add(
recognizer: TapGestureRecognizer() TextSpan(
..onTap = () =>
replyReply(replyItem.root == 0 ? replyItem : fReplyItem)));
return matchStr;
}
} else {
spanChilds.add(TextSpan(
text: matchStr, text: matchStr,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => ..onTap = () {
replyReply(replyItem.root == 0 ? replyItem : fReplyItem))); final String heroTag = Utils.makeHeroTag(userId);
return matchStr; Get.toNamed(
} '/member?mid=$userId',
return ''; arguments: {'face': '', 'heroTag': heroTag},
},
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"@.*( |:)");
// }
// 只@用户没有内容
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},
);
},
),
); );
}); },
} ),
return ''; );
}, } else if (RegExp(r'^\b[0-9]{1,2}[:][0-9]{2}\b$').hasMatch(matchStr)) {
onNonMatch: (String str) { spanChilds.add(
if (!str.contains('@')) { TextSpan(
spanChilds.add(TextSpan(text: str)); text: ' $matchStr ',
} style: TextStyle(
print(str); color: Theme.of(context).colorScheme.primary,
return str; ),
}, recognizer: TapGestureRecognizer()
..onTap = () {
// 跳转到指定位置
try {
matchStr = matchStr.replaceAll('', ':');
SmartDialog.showToast('跳转至:$matchStr');
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.plPlayerController
.seekTo(
Duration(seconds: Utils.duration(matchStr)),
);
} catch (e) {
SmartDialog.showToast('跳转失败: $e');
}
},
),
); );
} else { } else {
matchMember = str; // print("matchStr=$matchStr");
} String appUrlSchema = '';
final bool enableWordRe = setting.get(SettingBoxKey.enableWordRe,
// 匹配 jumpUrl defaultValue: false) as bool;
String matchUrl = matchMember; if (content.jumpUrl[matchStr] != null &&
if (content.jumpUrl.isNotEmpty) { !matchedStrs.contains(matchStr)) {
final List urlKeys = content.jumpUrl.keys.toList().reversed.toList(); appUrlSchema = content.jumpUrl[matchStr]['app_url_schema'];
for (int index = 0; index < urlKeys.length; index++) { if (appUrlSchema.startsWith('bilibili://search') && !enableWordRe) {
var i = urlKeys[index]; addPlainTextSpan(matchStr);
if (i.contains('?')) { return "";
urlKeys[index] = i.replaceAll('?', '\\?');
} }
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,
),
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 {
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
);
}
} 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( spanChilds.add(
TextSpan( TextSpan(
text: ' $matchStr ', text: content.jumpUrl[matchStr]['title'],
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
), ),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () { ..onTap = () {
// 跳转到指定位置 if (appUrlSchema == '') {
try { final String str = Uri.parse(matchStr).pathSegments[0];
Get.find<VideoDetailController>( final Map matchRes = IdUtils.matchAvorBv(input: str);
tag: Get.arguments['heroTag']) final List matchKeys = matchRes.keys.toList();
.plPlayerController if (matchKeys.isNotEmpty) {
.seekTo( if (matchKeys.first == 'BV') {
Duration(seconds: Utils.duration(matchStr)), Get.toNamed(
'/searchResult',
parameters: {'keyword': matchRes['BV']},
); );
} catch (_) {} }
} else {
Get.toNamed(
'/webview',
parameters: {
'url': matchStr,
'type': 'url',
'pageTitle': ''
},
);
}
} else {
if (appUrlSchema.startsWith('bilibili://search')) {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
});
}
}
}, },
), ),
); );
return ''; if (appUrlSchema.startsWith('bilibili://search')) {
}, spanChilds.add(
onNonMatch: (str) { WidgetSpan(
return str; child: Icon(
}, FontAwesomeIcons.magnifyingGlass,
); size: 9,
color: Theme.of(context).colorScheme.primary,
if (content.atNameToMid.isEmpty && content.jumpUrl.isEmpty) { ),
if (str != '') { alignment: PlaceholderAlignment.top,
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) { if (content.pictures.isNotEmpty) {
final List<String> picList = <String>[]; final List<String> picList = <String>[];
final int len = content.pictures.length; final int len = content.pictures.length;
spanChilds.add(const TextSpan(text: '\n'));
if (len == 1) { if (len == 1) {
Map pictureItem = content.pictures.first; Map pictureItem = content.pictures.first;
picList.add(pictureItem['img_src']); picList.add(pictureItem['img_src']);
spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add( spanChilds.add(
WidgetSpan( WidgetSpan(
child: LayoutBuilder( child: LayoutBuilder(
@ -880,7 +782,7 @@ InlineSpan buildContent(
height: height, height: height,
), ),
), ),
height > maxHeight height > Get.size.height * 0.9
? const PBadge( ? const PBadge(
text: '长图', text: '长图',
right: 8, right: 8,

View File

@ -61,6 +61,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final Floating floating = Floating(); final Floating floating = Floating();
// 生命周期监听 // 生命周期监听
late final AppLifecycleListener _lifecycleListener; late final AppLifecycleListener _lifecycleListener;
bool isShowing = true;
@override @override
void initState() { void initState() {
@ -216,15 +217,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
videoIntroController.isPaused = true; videoIntroController.isPaused = true;
plPlayerController!.removeStatusLister(playerListener); plPlayerController!.removeStatusLister(playerListener);
plPlayerController!.pause(); plPlayerController!.pause();
plPlayerController!.danmakuController?.pause();
plPlayerController!.danmakuController?.clear();
} }
setState(() => isShowing = false);
super.didPushNext(); super.didPushNext();
} }
@override @override
// 返回当前页面时 // 返回当前页面时
void didPopNext() async { void didPopNext() async {
setState(() => isShowing = true);
videoDetailController.isFirstTime = false; videoDetailController.isFirstTime = false;
final bool autoplay = autoPlayEnable; final bool autoplay = autoPlayEnable;
videoDetailController.playerInit(autoplay: autoplay); videoDetailController.playerInit(autoplay: autoplay);
@ -280,19 +281,13 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16;
final double pinnedHeaderHeight = final double pinnedHeaderHeight =
statusBarHeight + kToolbarHeight + videoHeight; statusBarHeight + kToolbarHeight + videoHeight;
if (MediaQuery.of(context).orientation == Orientation.landscape ||
plPlayerController?.isFullScreen.value == true) {
enterFullScreen();
} else {
exitFullScreen();
}
Widget childWhenDisabled = SafeArea( Widget childWhenDisabled = SafeArea(
top: MediaQuery.of(context).orientation == Orientation.portrait && top: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
bottom: MediaQuery.of(context).orientation == Orientation.portrait && bottom: MediaQuery.of(context).orientation == Orientation.portrait &&
plPlayerController?.isFullScreen.value == true, plPlayerController?.isFullScreen.value == true,
left: plPlayerController?.isFullScreen.value != true, left: false, //plPlayerController?.isFullScreen.value != true,
right: plPlayerController?.isFullScreen.value != true, right: false, //plPlayerController?.isFullScreen.value != true,
child: Stack( child: Stack(
children: [ children: [
Scaffold( Scaffold(
@ -309,187 +304,189 @@ class _VideoDetailPageState extends State<VideoDetailPage>
body: ExtendedNestedScrollView( body: ExtendedNestedScrollView(
controller: _extendNestCtr, controller: _extendNestCtr,
headerSliverBuilder: headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) { (BuildContext context2, bool innerBoxIsScrolled) {
return <Widget>[ return <Widget>[
Obx( Obx(
() => SliverAppBar( () {
automaticallyImplyLeading: false, if (MediaQuery.of(context).orientation ==
// 假装使用一个非空变量避免Obx检测不到而罢工 Orientation.landscape ||
pinned: videoDetailController.autoPlay.value ^ plPlayerController?.isFullScreen.value == true) {
false ^ enterFullScreen();
videoDetailController.autoPlay.value, } else {
elevation: 0, exitFullScreen();
scrolledUnderElevation: 0, }
forceElevated: innerBoxIsScrolled, return SliverAppBar(
expandedHeight: MediaQuery.of(context).orientation == automaticallyImplyLeading: false,
Orientation.landscape || // 假装使用一个非空变量避免Obx检测不到而罢工
plPlayerController?.isFullScreen.value == true pinned: videoDetailController.autoPlay.value ^
? MediaQuery.sizeOf(context).height - false ^
(MediaQuery.of(context).orientation == videoDetailController.autoPlay.value,
Orientation.landscape elevation: 0,
? 0 scrolledUnderElevation: 0,
: MediaQuery.of(context).padding.top) forceElevated: innerBoxIsScrolled,
: videoHeight, expandedHeight: MediaQuery.of(context).orientation ==
backgroundColor: Colors.black, Orientation.landscape ||
flexibleSpace: FlexibleSpaceBar( plPlayerController?.isFullScreen.value == true
background: PopScope( ? MediaQuery.sizeOf(context).height -
canPop: (MediaQuery.of(context).orientation ==
plPlayerController?.isFullScreen.value != true, Orientation.landscape
onPopInvoked: (bool didPop) { ? 0
if (plPlayerController?.isFullScreen.value == : MediaQuery.of(context).padding.top)
true) { : videoHeight,
plPlayerController! backgroundColor: Colors.black,
.triggerFullScreen(status: false); flexibleSpace: FlexibleSpaceBar(
} background: PopScope(
if (MediaQuery.of(context).orientation == canPop: plPlayerController?.isFullScreen.value !=
Orientation.landscape) { true,
verticalScreen(); onPopInvoked: (bool didPop) {
} if (plPlayerController?.isFullScreen.value ==
}, true) {
child: LayoutBuilder( plPlayerController!
builder: (BuildContext context, .triggerFullScreen(status: false);
BoxConstraints boxConstraints) { }
final double maxWidth = boxConstraints.maxWidth; if (MediaQuery.of(context).orientation ==
final double maxHeight = Orientation.landscape) {
boxConstraints.maxHeight; verticalScreen();
return Stack( }
children: <Widget>[ },
FutureBuilder( child: LayoutBuilder(
future: _futureBuilderFuture, builder: (BuildContext context,
builder: (BuildContext context, BoxConstraints boxConstraints) {
AsyncSnapshot snapshot) { final double maxWidth =
if (snapshot.hasData && boxConstraints.maxWidth;
snapshot.data['status']) { final double maxHeight =
return Obx( boxConstraints.maxHeight;
() => !videoDetailController return Stack(
.autoPlay.value children: <Widget>[
? const SizedBox() if (isShowing)
: PLVideoPlayer( FutureBuilder(
controller: future: _futureBuilderFuture,
plPlayerController!, builder: (BuildContext context,
headerControl: AsyncSnapshot snapshot) {
videoDetailController if (snapshot.hasData &&
.headerControl, snapshot.data['status']) {
danmuWidget: Obx( return Obx(
() => PlDanmaku( () =>
key: Key( !videoDetailController
videoDetailController .autoPlay.value
.danmakuCid ? nil
.value : PLVideoPlayer(
.toString()), controller:
cid: plPlayerController!,
videoDetailController headerControl:
.danmakuCid videoDetailController
.value, .headerControl,
playerController: danmuWidget: Obx(
plPlayerController!, () => PlDanmaku(
), key: Key(videoDetailController
), .danmakuCid
), .value
); .toString()),
} else { cid: videoDetailController
return const SizedBox(); .danmakuCid
} .value,
}, playerController:
), plPlayerController!,
),
),
),
);
} else {
return const SizedBox();
}
},
),
/// 关闭自动播放时 手动播放 /// 关闭自动播放时 手动播放
if (!videoDetailController if (!videoDetailController
.autoPlay.value) ...<Widget>[ .autoPlay.value) ...<Widget>[
Obx( Obx(
() => Visibility( () => Visibility(
visible: videoDetailController visible: videoDetailController
.isShowCover.value, .isShowCover.value,
child: Positioned( child: Positioned(
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
handlePlay(); handlePlay();
}, },
child: NetworkImgLayer( child: NetworkImgLayer(
type: 'emote', type: 'emote',
src: videoDetailController src: videoDetailController
.videoItem['pic'], .videoItem['pic'],
width: maxWidth, width: maxWidth,
height: maxHeight, height: maxHeight,
),
), ),
), ),
), ),
), ),
), Obx(
Obx( () => Visibility(
() => Visibility( visible: videoDetailController
visible: videoDetailController .isShowCover.value &&
.isShowCover.value && videoDetailController
videoDetailController .isEffective.value,
.isEffective.value, child: Stack(
child: Stack( children: [
children: [ Positioned(
Positioned( top: 0,
top: 0, left: 0,
left: 0, right: 0,
right: 0, child: AppBar(
child: AppBar( primary: false,
primary: false, foregroundColor:
foregroundColor: Colors.white,
Colors.white, elevation: 0,
elevation: 0, scrolledUnderElevation: 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(
backgroundColor: backgroundColor:
MaterialStateProperty Colors.transparent,
.resolveWith( actions: [
(states) { IconButton(
return Colors.white tooltip: '稍后再看',
.withOpacity(0.8); 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<VideoDetailPage>
// }, // },
/// 不收回 /// 不收回
pinnedHeaderSliverHeightBuilder: () { pinnedHeaderSliverHeightBuilder: () {
return plPlayerController?.isFullScreen.value == true return MediaQuery.of(context).orientation ==
Orientation.landscape ||
plPlayerController?.isFullScreen.value == true
? MediaQuery.sizeOf(context).height ? MediaQuery.sizeOf(context).height
: pinnedHeaderHeight; : pinnedHeaderHeight;
}, },

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:floating/floating.dart'; import 'package:floating/floating.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -724,6 +725,8 @@ class _HeaderControlState extends State<HeaderControl> {
double fontSizeVal = widget.controller!.fontSizeVal; double fontSizeVal = widget.controller!.fontSizeVal;
// 弹幕速度 // 弹幕速度
double danmakuDurationVal = widget.controller!.danmakuDurationVal; double danmakuDurationVal = widget.controller!.danmakuDurationVal;
// 弹幕描边
double strokeWidth = widget.controller!.strokeWidth;
final DanmakuController danmakuController = final DanmakuController danmakuController =
widget.controller!.danmakuController!; widget.controller!.danmakuController!;
@ -857,6 +860,44 @@ class _HeaderControlState extends State<HeaderControl> {
), ),
), ),
), ),
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)}%'), Text('字体大小 ${(fontSizeVal * 100).toStringAsFixed(1)}%'),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(

View File

@ -1,3 +1,4 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/http/msg.dart'; import 'package:pilipala/http/msg.dart';
import 'package:pilipala/models/msg/session.dart'; import 'package:pilipala/models/msg/session.dart';
@ -8,6 +9,8 @@ class WhisperDetailController extends GetxController {
late String face; late String face;
late String mid; late String mid;
RxList<MessageItem> messageList = <MessageItem>[].obs; RxList<MessageItem> messageList = <MessageItem>[].obs;
//表情转换图片规则
List<dynamic>? eInfos;
@override @override
void onInit() { void onInit() {
@ -22,6 +25,9 @@ class WhisperDetailController extends GetxController {
var res = await MsgHttp.sessionMsg(talkerId: talkerId); var res = await MsgHttp.sessionMsg(talkerId: talkerId);
if (res['status']) { if (res['status']) {
messageList.value = res['data'].messages; messageList.value = res['data'].messages;
if (messageList.isNotEmpty && res['data'].eInfos != null) {
eInfos = res['data'].eInfos;
}
} }
return res; return res;
} }

View File

@ -110,12 +110,16 @@ class _WhisperDetailPageState extends State<WhisperDetailPage> {
if (i == 0) { if (i == 0) {
return Column( return Column(
children: [ children: [
ChatItem(item: messageList[i]), ChatItem(
item: messageList[i],
e_infos: _whisperDetailController.eInfos),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
); );
} else { } else {
return ChatItem(item: messageList[i]); return ChatItem(
item: messageList[i],
e_infos: _whisperDetailController.eInfos);
} }
}, },
), ),

View File

@ -1,38 +1,370 @@
// ignore_for_file: must_be_immutable // ignore_for_file: must_be_immutable
import 'dart:convert';
import 'package:flutter/material.dart'; 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/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/storage.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 { class ChatItem extends StatelessWidget {
dynamic item; dynamic item;
List? e_infos;
ChatItem({ ChatItem({
super.key, super.key,
this.item, this.item,
this.e_infos,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool isOwner = bool isOwner =
item.senderUid == GStrorage.userInfo.get('userInfoCache').mid; 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 = bool isPic = item.msgType == MsgType.pic.value; // 图片
item.msgType == 18 || item.msgType == 10 || item.msgType == 13; bool isText = item.msgType == MsgType.text.value; // 文本
int msgType = item.msgType; // 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 ?? ''; 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<InlineSpan> children = [];
Map<String, String> 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<dynamic>().then(
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'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<dynamic>().then(
(e) => Get.toNamed<dynamic>('/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'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']) ...<Widget>[
const SizedBox(height: 6),
GestureDetector(
onTap: () async {
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}',
caseSensitive: false);
Iterable<Match> 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<dynamic>().then(
(e) => Get.toNamed<dynamic>(
'/video?bvid=$bvid&cid=$cid',
arguments: <String, String?>{
'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 return isSystem
? (msgType == 10 ? messageContent(context)
? SystemNotice(item: item)
: msgType == 13
? SystemNotice2(item: item)
: const SizedBox())
: isRevoke : isRevoke
? const SizedBox() ? const SizedBox()
: Row( : Row(
@ -66,27 +398,7 @@ class ChatItem extends StatelessWidget {
? CrossAxisAlignment.end ? CrossAxisAlignment.end
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
children: [ children: [
isText messageContent(context),
? 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(),
SizedBox(height: isPic ? 7 : 2), SizedBox(height: isPic ? 7 : 2),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View File

@ -221,6 +221,7 @@ class PlPlayerController {
late double showArea; late double showArea;
late double opacityVal; late double opacityVal;
late double fontSizeVal; late double fontSizeVal;
late double strokeWidth;
late double danmakuDurationVal; late double danmakuDurationVal;
late List<double> speedsList; late List<double> speedsList;
// 缓存 // 缓存
@ -275,6 +276,9 @@ class PlPlayerController {
// 弹幕时间 // 弹幕时间
danmakuDurationVal = danmakuDurationVal =
localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0); localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0);
// 描边粗细
strokeWidth =
localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5);
playRepeat = PlayRepeat.values.toList().firstWhere( playRepeat = PlayRepeat.values.toList().firstWhere(
(e) => (e) =>
e.value == e.value ==
@ -1086,6 +1090,7 @@ class PlPlayerController {
localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); localCache.put(LocalCacheKey.danmakuOpacity, opacityVal);
localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal);
localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal); localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal);
localCache.put(LocalCacheKey.strokeWidth, strokeWidth);
if (_videoPlayerController != null) { if (_videoPlayerController != null) {
var pp = _videoPlayerController!.platform as NativePlayer; var pp = _videoPlayerController!.platform as NativePlayer;
await pp.setProperty('audio-files', ''); await pp.setProperty('audio-files', '');

View File

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

View File

@ -147,8 +147,8 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler {
processingState: AudioProcessingState.idle, processingState: AudioProcessingState.idle,
playing: false, playing: false,
)); ));
_item.removeLast();
if (_item.isNotEmpty) { if (_item.isNotEmpty) {
_item.removeLast();
setMediaItem(_item.last); setMediaItem(_item.last);
} }
if (_item.isEmpty) { if (_item.isEmpty) {

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

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

View File

@ -1,50 +1,65 @@
// ignore_for_file: constant_identifier_names // ignore_for_file: constant_identifier_names, non_constant_identifier_names
import 'dart:math'; import 'dart:convert';
import 'package:flutter/material.dart';
class IdUtils { class IdUtils {
static const String TABLE = static final XOR_CODE = BigInt.parse('23442827791579');
'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF'; static final MASK_CODE = BigInt.parse('2251799813685247');
static const List<int> S = [11, 10, 3, 8, 4, 6]; // 位置编码表 static final MAX_AID = BigInt.one << (BigInt.from(51)).toInt();
static const int XOR = 177451812; // 固定异或值 static final BASE = BigInt.from(58);
static const int ADD = 8728348608; // 固定加法值
static const List<String> r = [ static const data =
'B', 'FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12mUSDQX9RdoZf';
'V',
'1',
'',
'',
'4',
'',
'1',
'',
'7',
'',
''
];
/// av转bv /// av转bv
static String av2bv(int av) { static String av2bv(int aid) {
int x_ = (av ^ XOR) + ADD; List<String> bytes = [
List<String> newR = []; 'B',
newR.addAll(r); 'V',
for (int i = 0; i < S.length; i++) { '1',
newR[S[i]] = '0',
TABLE.characters.elementAt((x_ / pow(58, i).toInt() % 58).toInt()); '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 /// bv转av
static int bv2av(String bv) { static int bv2av(String bvid) {
int r = 0; List<String> bvidArr = bvid.split('');
for (int i = 0; i < S.length; i++) { final tmpValue = bvidArr[3];
r += (TABLE.indexOf(bv.characters.elementAt(S[i])).toInt()) * bvidArr[3] = bvidArr[9];
pow(58, i).toInt(); bvidArr[9] = tmpValue;
}
return (r - ADD) ^ XOR; 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();
} }
// 匹配 // 匹配
@ -72,4 +87,19 @@ class IdUtils {
} }
return result; return result;
} }
// eid生成
static String? genAuroraEid(int uid) {
if (uid == 0) {
return null;
}
String uidString = uid.toString();
List<int> resultBytes = List.generate(
uidString.length,
(i) => uidString.codeUnitAt(i) ^ "ad1va46a7lza".codeUnitAt(i % 12),
);
String auroraEid = base64Url.encode(resultBytes);
auroraEid = auroraEid.replaceAll(RegExp(r'=*$', multiLine: true), '');
return auroraEid;
}
} }

View File

@ -3,13 +3,10 @@ import 'dart:io';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/models/model_owner.dart'; import 'package:pilipala/models/model_owner.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/info.dart';
class GStrorage { class GStrorage {
static late final Box<dynamic> recVideo;
static late final Box<dynamic> userInfo; static late final Box<dynamic> userInfo;
static late final Box<dynamic> historyword; static late final Box<dynamic> historyword;
static late final Box<dynamic> localCache; static late final Box<dynamic> localCache;
@ -21,13 +18,6 @@ class GStrorage {
final String path = dir.path; final String path = dir.path;
await Hive.initFlutter('$path/hive'); await Hive.initFlutter('$path/hive');
regAdapter(); regAdapter();
// 首页推荐视频
recVideo = await Hive.openBox(
'recVideo',
compactionStrategy: (int entries, int deletedEntries) {
return deletedEntries > 12;
},
);
// 登录用户信息 // 登录用户信息
userInfo = await Hive.openBox( userInfo = await Hive.openBox(
'userInfo', 'userInfo',
@ -54,15 +44,9 @@ class GStrorage {
} }
static void regAdapter() { static void regAdapter() {
Hive.registerAdapter(RecVideoItemAppModelAdapter());
Hive.registerAdapter(RcmdReasonAdapter());
Hive.registerAdapter(RcmdStatAdapter());
Hive.registerAdapter(RcmdOwnerAdapter());
Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(OwnerAdapter());
Hive.registerAdapter(UserInfoDataAdapter()); Hive.registerAdapter(UserInfoDataAdapter());
Hive.registerAdapter(LevelInfoAdapter()); Hive.registerAdapter(LevelInfoAdapter());
Hive.registerAdapter(HotSearchModelAdapter());
Hive.registerAdapter(HotSearchItemAdapter());
} }
static Future<void> lazyInit() async { static Future<void> lazyInit() async {
@ -73,8 +57,6 @@ class GStrorage {
static Future<void> close() async { static Future<void> close() async {
// user.compact(); // user.compact();
// user.close(); // user.close();
recVideo.compact();
recVideo.close();
userInfo.compact(); userInfo.compact();
userInfo.close(); userInfo.close();
historyword.compact(); historyword.compact();
@ -145,7 +127,8 @@ class SettingBoxKey {
enableMYBar = 'enableMYBar', enableMYBar = 'enableMYBar',
hideSearchBar = 'hideSearchBar', // 收起顶栏 hideSearchBar = 'hideSearchBar', // 收起顶栏
hideTabBar = 'hideTabBar', // 收起底栏 hideTabBar = 'hideTabBar', // 收起底栏
tabbarSort = 'tabbarSort'; // 首页tabbar tabbarSort = 'tabbarSort', // 首页tabbar
dynamicBadgeMode = 'dynamicBadgeMode';
} }
class LocalCacheKey { class LocalCacheKey {
@ -158,12 +141,13 @@ class LocalCacheKey {
wbiKeys = 'wbiKeys', wbiKeys = 'wbiKeys',
timeStamp = 'timeStamp', timeStamp = 'timeStamp',
// 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 // 弹幕相关设置 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细
danmakuBlockType = 'danmakuBlockType', danmakuBlockType = 'danmakuBlockType',
danmakuShowArea = 'danmakuShowArea', danmakuShowArea = 'danmakuShowArea',
danmakuOpacity = 'danmakuOpacity', danmakuOpacity = 'danmakuOpacity',
danmakuFontScale = 'danmakuFontScale', danmakuFontScale = 'danmakuFontScale',
danmakuDuration = 'danmakuDuration', danmakuDuration = 'danmakuDuration',
strokeWidth = 'strokeWidth',
// 代理host port // 代理host port
systemProxyHost = 'systemProxyHost', systemProxyHost = 'systemProxyHost',

View File

@ -276,16 +276,18 @@ class Utils {
// [arm64-v8a] // [arm64-v8a]
String abi = androidInfo.supportedAbis.first; String abi = androidInfo.supportedAbis.first;
late String downloadUrl; late String downloadUrl;
for (var i in data.assets) { if (data.assets.isNotEmpty) {
if (i.downloadUrl.contains(abi)) { for (var i in data.assets) {
downloadUrl = i.downloadUrl; if (i.downloadUrl.contains(abi)) {
downloadUrl = i.downloadUrl;
}
} }
// 应用外下载
launchUrl(
Uri.parse(downloadUrl),
mode: LaunchMode.externalApplication,
);
} }
// 应用外下载
launchUrl(
Uri.parse(downloadUrl),
mode: LaunchMode.externalApplication,
);
} }
} }

View File

@ -209,6 +209,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" 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: characters:
dependency: transitive dependency: transitive
description: description:
@ -492,10 +500,11 @@ packages:
floating: floating:
dependency: "direct main" dependency: "direct main"
description: description:
name: floating path: "."
sha256: d9d563089e34fbd714ffdcdd2df447ec41b40c9226dacae6b4f78847aef8b991 ref: main
url: "https://pub.flutter-io.cn" resolved-ref: d2d8421c4d80f6113f832404109853684721e11a
source: hosted url: "https://github.com/guozhigq/floating.git"
source: git
version: "2.0.1" version: "2.0.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
@ -547,6 +556,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -589,6 +606,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: font_awesome_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -781,6 +806,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.0" 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: logging:
dependency: transitive dependency: transitive
description: description:
@ -789,6 +822,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.2.0" 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: matcher:
dependency: transitive dependency: transitive
description: description:
@ -951,7 +992,7 @@ packages:
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
@ -1214,6 +1255,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.3.8" 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: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -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 # 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 # 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. # 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: environment:
sdk: ">=2.19.6 <3.0.0" sdk: ">=2.19.6 <3.0.0"
@ -85,8 +85,8 @@ dependencies:
encrypt: ^5.0.3 encrypt: ^5.0.3
# 视频播放器 # 视频播放器
media_kit: ^1.1.10 # Primary package. media_kit: ^1.1.10 # Primary package.
media_kit_video: ^1.2.4 # For video rendering. media_kit_video: ^1.2.4 # For video rendering.
media_kit_libs_video: ^1.0.4 media_kit_libs_video: ^1.0.4
# 媒体通知 # 媒体通知
@ -124,7 +124,10 @@ dependencies:
# 代理 # 代理
system_proxy: ^0.1.0 system_proxy: ^0.1.0
# pip # pip
floating: ^2.0.1 floating:
git:
url: https://github.com/guozhigq/floating.git
ref: main
# html解析 # html解析
html: ^0.15.4 html: ^0.15.4
# html渲染 # html渲染
@ -134,7 +137,9 @@ dependencies:
uuid: ^3.0.7 uuid: ^3.0.7
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
nil: ^1.1.1 nil: ^1.1.1
catcher_2: ^1.1.0
logger: ^2.0.2+1
path: 1.8.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -205,6 +210,5 @@ flutter:
# fonts: # fonts:
# - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf # - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf
# For details regarding fonts from package dependencies, # For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages # see https://flutter.dev/custom-fonts/#from-packages