diff --git a/.github/workflows/beta_ci.yml b/.github/workflows/beta_ci.yml index e839aca1..14b51780 100644 --- a/.github/workflows/beta_ci.yml +++ b/.github/workflows/beta_ci.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - "main" + - "never" paths-ignore: - "**.md" - "**.txt" @@ -12,6 +12,7 @@ on: - ".idea/**" - "!.github/workflows/**" + jobs: update_version: name: Read and update version @@ -205,4 +206,4 @@ jobs: method: sendFile path: Pilipala-Beta/* parse_mode: Markdown - context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}](${{ github.event.head_commit.url }})" + context: "*Beta版本: v${{ needs.update_version.outputs.new_version }}*\n更新内容: [${{ needs.update_version.outputs.last_commit }}]" diff --git a/README.md b/README.md index 237bd07f..228d17bb 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码 ```bash -[✓] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale +[✓] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale zh-Hans-CN) [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 15.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2022.3) -[✓] VS Code (version 1.85.1) +[✓] VS Code (version 1.87.2) [✓] Connected device (3 available) [✓] Network resources @@ -44,6 +44,9 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码 ## 技术交流 Telegram: https://t.me/+lm_oOVmF0RJiODk1 + +Tg Beta版本:@PiliPala_Beta + QQ频道: https://pd.qq.com/s/365esodk3 @@ -91,7 +94,7 @@ QQ频道: https://pd.qq.com/s/365esodk3 - [x] 音质选择(视视频而定) - [x] 解码格式选择(视视频而定) - [x] 弹幕 - - [ ] 字幕 + - [x] 字幕 - [x] 记忆播放 - [x] 视频比例:高度/宽度适应、填充、包含等 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c52d8447..f119eb1e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,7 +45,7 @@ android:fullBackupContent="false" tools:replace="android:allowBackup"> \ No newline at end of file diff --git a/assets/images/video/danmu_open.svg b/assets/images/video/danmu_open.svg new file mode 100644 index 00000000..24e8d7a9 --- /dev/null +++ b/assets/images/video/danmu_open.svg @@ -0,0 +1 @@ +Layer 1 \ No newline at end of file diff --git a/assets/loading.json b/assets/loading.json new file mode 100644 index 00000000..38bccbed --- /dev/null +++ b/assets/loading.json @@ -0,0 +1 @@ +{"v":"5.7.11","fr":60,"ip":0,"op":81,"w":1920,"h":1080,"nm":"Loading Dots","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Dot4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[100]},{"t":55,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":25,"s":[1142,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":39,"s":[1142,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":55,"s":[1142,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":25,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":39,"s":[75,75,100]},{"t":55,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0039,0.6157,0.5686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Dot3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":17,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":31,"s":[100]},{"t":47,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":17,"s":[1022,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":31,"s":[1022,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":47,"s":[1022,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":17,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[75,75,100]},{"t":47,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0039,0.6157,0.5686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Dot2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":23,"s":[100]},{"t":39,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[902,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":23,"s":[902,500,0],"to":[0,0,0],"ti":[0,0,0]},{"t":39,"s":[902,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":9,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":23,"s":[75,75,100]},{"t":39,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0039,0.6157,0.5686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Dot1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[25]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":14,"s":[100]},{"t":30,"s":[25]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[782,540,0],"to":[0,-6.667,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":14,"s":[782,500,0],"to":[0,0,0],"ti":[0,-6.667,0]},{"t":30,"s":[782,540,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-284,92,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":14,"s":[75,75,100]},{"t":30,"s":[50,50,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[120,120],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.0039,0.6157,0.5686,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-284,92],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/assets/trail_loading.json b/assets/trail_loading.json new file mode 100644 index 00000000..9fb39ea6 --- /dev/null +++ b/assets/trail_loading.json @@ -0,0 +1 @@ +{"v":"4.6.8","fr":60,"ip":0,"op":106,"w":500,"h":500,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 5","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":20,"s":[0],"e":[360]},{"t":110}]},"p":{"a":0,"k":[251,250,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10]},"p":{"a":0,"k":[0,-100]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0,0.7294118,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":20,"op":620,"st":20,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 4","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":15,"s":[0],"e":[360]},{"t":105}]},"p":{"a":0,"k":[251,250,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20]},"p":{"a":0,"k":[0,-100]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0,0.7294118,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":15,"op":615,"st":15,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 3","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":10,"s":[0],"e":[360]},{"t":100}]},"p":{"a":0,"k":[251,250,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[30,30]},"p":{"a":0,"k":[0,-100]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0,0.7294118,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":10,"op":610,"st":10,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":5,"s":[0],"e":[360]},{"t":95}]},"p":{"a":0,"k":[251,250,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[40,40]},"p":{"a":0,"k":[0,-100]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0,0.7294118,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":5,"op":605,"st":5,"bm":0,"sr":1},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"n":["0p667_1_0p333_0"],"t":0,"s":[0],"e":[360]},{"t":90}]},"p":{"a":0,"k":[250,250,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":0,"s":[50,50],"e":[40,40]},{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"n":["0p667_1_0p333_0","0p667_1_0p333_0"],"t":84,"s":[40,40],"e":[50,50]},{"t":100}]},"p":{"a":0,"k":[0,-100]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"st","c":{"a":0,"k":[0,0,0,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0},"lc":1,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[0,0.7294118,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":600,"st":0,"bm":0,"sr":1}]} \ No newline at end of file diff --git a/change_log/1.0.22.0430.md b/change_log/1.0.22.0430.md new file mode 100644 index 00000000..29f8aecf --- /dev/null +++ b/change_log/1.0.22.0430.md @@ -0,0 +1,27 @@ +## 1.0.22 + +### 功能 ++ 字幕 ++ 全屏时选集 ++ 动态转发 ++ 评论视频并转发 ++ 收藏夹删除 ++ 合集显示封面 ++ 底部导航栏编辑、排序功能 ++ 历史记录进度条展示 ++ 直播画质切换 ++ 排行榜功能 ++ 视频详情页推荐视频开关 ++ 显示联合投稿up + +### 修复 ++ 收藏夹个数错误 ++ 封面保存权限问题 ++ 合集最后1p未展示 ++ up主页关注按钮触发灰屏 + +### 优化 ++ 视频简介查看逻辑 + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/change_log/1.0.23.0504.md b/change_log/1.0.23.0504.md new file mode 100644 index 00000000..afd401fa --- /dev/null +++ b/change_log/1.0.23.0504.md @@ -0,0 +1,14 @@ +## 1.0.23 + +### 功能 ++ 封面下载 + + +### 修复 ++ 全屏问题 ++ 视频播放器灰屏问题 ++ 评论区点击区域问题 + + +更多更新日志可在Github上查看 +问题反馈、功能建议请查看「关于」页面。 diff --git a/change_log/1.0.24.0626.md b/change_log/1.0.24.0626.md new file mode 100644 index 00000000..d9a8892f --- /dev/null +++ b/change_log/1.0.24.0626.md @@ -0,0 +1,23 @@ +## 1.0.24 + +### 功能 ++ 私信功能 ++ 回复我的、收到的赞查看 ++ 新的登录方式 ++ 全屏选集 ++ 一键三连 ++ 按分区搜索 + +### 优化 ++ 页面跳转动画 ++ 评论区跳转 + +### 修复 ++ 音画不同步问题 ++ 分集字幕未同步 ++ 多语言字幕 ++ 弹幕设置未生效 ++ + + +问题反馈、功能建议请查看「关于」页面。 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 9625e105..7c569640 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8db59815..a400600f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,7 +9,7 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter - - ReachabilitySwift + - FlutterMacOS - device_info_plus (0.0.1): - Flutter - Flutter (1.0.0) @@ -23,7 +23,7 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - gt3_flutter_plugin (0.0.8): + - gt3_flutter_plugin (0.0.9): - Flutter - GT3Captcha-iOS - GT3Captcha-iOS (0.15.8.3) @@ -38,9 +38,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - - ReachabilitySwift (5.0.0) - saver_gallery (0.0.1): - Flutter - screen_brightness_ios (0.1.0): @@ -71,7 +70,7 @@ DEPENDENCIES: - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) @@ -100,7 +99,6 @@ SPEC REPOS: trunk: - FMDB - GT3Captcha-iOS - - ReachabilitySwift - Toast EXTERNAL SOURCES: @@ -113,7 +111,7 @@ EXTERNAL SOURCES: auto_orientation: :path: ".symlinks/plugins/auto_orientation/ios" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + :path: ".symlinks/plugins/connectivity_plus/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: @@ -166,22 +164,21 @@ SPEC CHECKSUMS: audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d - connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a + connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529 fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23 + gt3_flutter_plugin: 5bd2c08d3c19cbb6ee3b08f4358439e54c8ab2ee GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 @@ -189,11 +186,11 @@ SPEC CHECKSUMS: status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446 system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44 Toast: ec33c32b8688982cecc6348adeae667c1b9938da - url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 - webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a + webview_flutter_wkwebview: be0f0d33777f1bfd0c9fdcb594786704dbf65f36 PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index bac856d2..55565d40 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -156,7 +156,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db..5e31d3d3 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { GeneratedPluginRegistrant.register(with: self) + + // 设置音频会话类别,确保在静音模式下播放音频 + do { + try AVAudioSession.sharedInstance().setCategory(.playback, options: [.duckOthers]) + } catch { + print("Failed to set audio session category: \(error)") + } + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } -} +} \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 65906625..1e7d9fed 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -49,6 +49,8 @@ NSPhotoLibraryAddUsageDescription 请允许APP保存图片到相册 + NSPhotoLibraryUsageDescription + 请允许APP保存图片到相册 NSCameraUsageDescription App需要您的同意,才能访问相册 NSAppleMusicUsageDescription diff --git a/lib/common/pages_bottom_sheet.dart b/lib/common/pages_bottom_sheet.dart new file mode 100644 index 00000000..49e9b4d8 --- /dev/null +++ b/lib/common/pages_bottom_sheet.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import '../models/common/video_episode_type.dart'; + +class EpisodeBottomSheet { + final List episodes; + final int currentCid; + final dynamic dataType; + final BuildContext context; + final Function changeFucCall; + final int? cid; + final double? sheetHeight; + bool isFullScreen = false; + + EpisodeBottomSheet({ + required this.episodes, + required this.currentCid, + required this.dataType, + required this.context, + required this.changeFucCall, + this.cid, + this.sheetHeight, + this.isFullScreen = false, + }); + + Widget buildEpisodeListItem( + dynamic episode, + int index, + bool isCurrentIndex, + ) { + Color primary = Theme.of(context).colorScheme.primary; + Color onSurface = Theme.of(context).colorScheme.onSurface; + + String title = ''; + switch (dataType) { + case VideoEpidoesType.videoEpisode: + title = episode.title; + break; + case VideoEpidoesType.videoPart: + title = episode.pagePart; + break; + case VideoEpidoesType.bangumiEpisode: + title = '第${episode.title}话 ${episode.longTitle!}'; + break; + } + return isFullScreen || episode?.cover == null || episode?.cover == '' + ? ListTile( + onTap: () { + SmartDialog.showToast('切换至「$title」'); + changeFucCall.call(episode, index); + }, + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.gif', + color: primary, + height: 12, + ) + : null, + title: Text(title, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex ? primary : onSurface, + ))) + : InkWell( + onTap: () { + SmartDialog.showToast('切换至「$title」'); + changeFucCall.call(episode, index); + }, + child: Padding( + padding: + const EdgeInsets.only(left: 14, right: 14, top: 8, bottom: 8), + child: Row( + children: [ + NetworkImgLayer( + width: 130, height: 75, src: episode?.cover ?? ''), + const SizedBox(width: 10), + Expanded( + child: Text( + title, + maxLines: 2, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex ? primary : onSurface, + ), + ), + ), + ], + ), + ), + ); + } + + Widget buildTitle() { + return AppBar( + toolbarHeight: 45, + automaticallyImplyLeading: false, + centerTitle: false, + title: Text( + '合集(${episodes.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: !isFullScreen + ? [ + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 14), + ] + : null, + ); + } + + Widget buildShowContent(BuildContext context) { + final ItemScrollController itemScrollController = ItemScrollController(); + int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid); + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + itemScrollController.jumpTo(index: currentIndex); + }); + return Container( + height: sheetHeight, + color: Theme.of(context).colorScheme.surface, + child: Column( + children: [ + buildTitle(), + Expanded( + child: Material( + child: PageStorage( + bucket: PageStorageBucket(), + child: ScrollablePositionedList.builder( + itemScrollController: itemScrollController, + itemCount: episodes.length + 1, + itemBuilder: (BuildContext context, int index) { + bool isLastItem = index == episodes.length; + bool isCurrentIndex = currentIndex == index; + return isLastItem + ? SizedBox( + height: + MediaQuery.of(context).padding.bottom + 20, + ) + : buildEpisodeListItem( + episodes[index], + index, + isCurrentIndex, + ); + }, + ), + ), + ), + ), + ], + ), + ); + }); + } + + /// The [BuildContext] of the widget that calls the bottom sheet. + PersistentBottomSheetController show(BuildContext context) { + final PersistentBottomSheetController btmSheetCtr = showBottomSheet( + context: context, + builder: (BuildContext context) { + return buildShowContent(context); + }, + ); + return btmSheetCtr; + } +} diff --git a/lib/common/skeleton/skeleton.dart b/lib/common/skeleton/skeleton.dart index 34e87f55..b17a55fc 100644 --- a/lib/common/skeleton/skeleton.dart +++ b/lib/common/skeleton/skeleton.dart @@ -13,8 +13,8 @@ class Skeleton extends StatelessWidget { var shimmerGradient = LinearGradient( colors: [ Colors.transparent, - Theme.of(context).colorScheme.background.withAlpha(10), - Theme.of(context).colorScheme.background.withAlpha(10), + Theme.of(context).colorScheme.surface.withAlpha(10), + Theme.of(context).colorScheme.surface.withAlpha(10), Colors.transparent, ], stops: const [ diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart index a8f2fc67..1e518f39 100644 --- a/lib/common/widgets/badge.dart +++ b/lib/common/widgets/badge.dart @@ -66,7 +66,7 @@ class PBadge extends StatelessWidget { border: Border.all(color: borderColor), ), child: Text( - text!, + text ?? '', style: TextStyle(fontSize: fs ?? fontSize, color: color), ), ); diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart index bf58d78c..c626978e 100644 --- a/lib/common/widgets/html_render.dart +++ b/lib/common/widgets/html_render.dart @@ -1,6 +1,10 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:get/get.dart'; +import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart'; +import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart'; +import 'package:pilipala/utils/highlight.dart'; import 'network_img_layer.dart'; // ignore: must_be_immutable @@ -22,6 +26,20 @@ class HtmlRender extends StatelessWidget { data: htmlContent, onLinkTap: (String? url, Map buildContext, attributes) {}, extensions: [ + TagExtension( + tagsToExtend: {'pre'}, + builder: (ExtensionContext extensionContext) { + final Map attributes = extensionContext.attributes; + final String lang = attributes['data-lang'] as String; + final String code = attributes['codecontent'] as String; + List selectedLanguages = [lang.split('@').first]; + TextSpan? result = highlightExistingText(code, selectedLanguages); + if (result == null) { + return const Center(child: Text('代码块渲染失败')); + } + return SelectableText.rich(result); + }, + ), TagExtension( tagsToExtend: {'img'}, builder: (ExtensionContext extensionContext) { @@ -44,20 +62,52 @@ class HtmlRender extends StatelessWidget { if (isMall) { return const SizedBox(); } - // bool inTable = - // extensionContext.element!.previousElementSibling == null || - // extensionContext.element!.nextElementSibling == null; - // imgUrl = Utils().imageUrl(imgUrl!); - // return Image.network( - // imgUrl, - // width: isEmote ? 22 : null, - // height: isEmote ? 22 : null, - // ); - return NetworkImgLayer( - width: isEmote ? 22 : Get.size.width - 24, - height: isEmote ? 22 : 200, - src: imgUrl, + return InkWell( + onTap: () { + Navigator.of(context).push( + HeroDialogRoute( + builder: (BuildContext context) => + InteractiveviewerGallery( + sources: imgList ?? [imgUrl], + initIndex: imgList?.indexOf(imgUrl) ?? 0, + itemBuilder: ( + BuildContext context, + int index, + bool isFocus, + bool enablePageView, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (enablePageView) { + Navigator.of(context).pop(); + } + }, + child: Center( + child: Hero( + tag: imgUrl, + child: CachedNetworkImage( + fadeInDuration: + const Duration(milliseconds: 0), + imageUrl: imgUrl, + fit: BoxFit.contain, + ), + ), + ), + ); + }, + onPageChanged: (int pageIndex) {}, + ), + ), + ); + }, + child: CachedNetworkImage(imageUrl: imgUrl), ); + // return NetworkImgLayer( + // width: isEmote ? 22 : Get.size.width - 24, + // height: isEmote ? 22 : 200, + // src: imgUrl, + // ); } catch (err) { return const SizedBox(); } @@ -66,7 +116,7 @@ class HtmlRender extends StatelessWidget { ], style: { 'html': Style( - fontSize: FontSize.medium, + fontSize: FontSize.large, lineHeight: LineHeight.percent(140), ), 'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero), @@ -78,7 +128,7 @@ class HtmlRender extends StatelessWidget { margin: Margins.only(bottom: 10), ), 'span': Style( - fontSize: FontSize.medium, + fontSize: FontSize.large, height: Height(1.65), ), 'div': Style(height: Height.auto()), diff --git a/lib/common/widgets/http_error.dart b/lib/common/widgets/http_error.dart index cbc6659b..0381319e 100644 --- a/lib/common/widgets/http_error.dart +++ b/lib/common/widgets/http_error.dart @@ -2,12 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; class HttpError extends StatelessWidget { - const HttpError( - {required this.errMsg, required this.fn, this.btnText, super.key}); + const HttpError({ + required this.errMsg, + required this.fn, + this.btnText, + this.isShowBtn = true, + super.key, + }); final String? errMsg; final Function()? fn; final String? btnText; + final bool isShowBtn; @override Widget build(BuildContext context) { @@ -29,20 +35,22 @@ class HttpError extends StatelessWidget { style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 20), - FilledButton.tonal( - onPressed: () { - fn!(); - }, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.resolveWith((states) { - return Theme.of(context).colorScheme.primary.withAlpha(20); - }), + if (isShowBtn) + FilledButton.tonal( + onPressed: () { + fn!(); + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.resolveWith((states) { + return Theme.of(context).colorScheme.primary.withAlpha(20); + }), + ), + child: Text( + btnText ?? '点击重试', + style: + TextStyle(color: Theme.of(context).colorScheme.primary), + ), ), - child: Text( - btnText ?? '点击重试', - style: TextStyle(color: Theme.of(context).colorScheme.primary), - ), - ), ], ), ), diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 06c35974..0b715a89 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -2,7 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/utils/extension.dart'; -import 'package:pilipala/utils/global_data.dart'; +import 'package:pilipala/utils/global_data_cache.dart'; import '../../utils/storage.dart'; import '../constants.dart'; @@ -33,10 +33,12 @@ class NetworkImgLayer extends StatelessWidget { @override Widget build(BuildContext context) { - final int defaultImgQuality = GlobalData().imgQuality; + final int defaultImgQuality = GlobalDataCache().imgQuality; + if (src == '' || src == null) { + return placeholder(context); + } final String imageUrl = '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; - print(imageUrl); int? memCacheWidth, memCacheHeight; double aspectRatio = (width / height).toDouble(); diff --git a/lib/common/widgets/overlay_pop.dart b/lib/common/widgets/overlay_pop.dart deleted file mode 100644 index fe9b9377..00000000 --- a/lib/common/widgets/overlay_pop.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; -import '../../utils/download.dart'; -import '../constants.dart'; -import 'network_img_layer.dart'; - -class OverlayPop extends StatelessWidget { - const OverlayPop({super.key, this.videoItem, this.closeFn}); - - final dynamic videoItem; - final Function? closeFn; - - @override - Widget build(BuildContext context) { - final double imgWidth = MediaQuery.sizeOf(context).width - 8 * 2; - return Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(10.0), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - NetworkImgLayer( - width: imgWidth, - height: imgWidth / StyleString.aspectRatio, - src: videoItem.pic! as String, - quality: 100, - ), - Positioned( - right: 8, - top: 8, - child: Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: - const BorderRadius.all(Radius.circular(20))), - child: IconButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () => closeFn!(), - icon: const Icon( - Icons.close, - size: 18, - color: Colors.white, - ), - ), - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.fromLTRB(12, 10, 8, 10), - child: Row( - children: [ - Expanded( - child: Text( - videoItem.title! as String, - ), - ), - const SizedBox(width: 4), - IconButton( - tooltip: '保存封面图', - onPressed: () async { - await DownloadUtils.downloadImg( - videoItem.pic != null - ? videoItem.pic as String - : videoItem.cover as String, - ); - // closeFn!(); - }, - icon: const Icon(Icons.download, size: 20), - ) - ], - )), - ], - ), - ); - } -} diff --git a/lib/common/widgets/pull_to_refresh_header.dart b/lib/common/widgets/pull_to_refresh_header.dart deleted file mode 100644 index 46db5138..00000000 --- a/lib/common/widgets/pull_to_refresh_header.dart +++ /dev/null @@ -1,130 +0,0 @@ -// ignore_for_file: depend_on_referenced_packages - -import 'dart:math'; -import 'dart:ui' as ui show Image; - -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart'; - -double get maxDragOffset => 100; -double hideHeight = maxDragOffset / 2.3; -double refreshHeight = maxDragOffset / 1.5; - -class PullToRefreshHeader extends StatelessWidget { - const PullToRefreshHeader( - this.info, - this.lastRefreshTime, { - this.color, - super.key, - }); - - final PullToRefreshScrollNotificationInfo? info; - final DateTime? lastRefreshTime; - final Color? color; - - @override - Widget build(BuildContext context) { - final PullToRefreshScrollNotificationInfo? infos = info; - if (infos == null) { - return const SizedBox(); - } - String text = ''; - if (infos.mode == PullToRefreshIndicatorMode.armed) { - text = 'Release to refresh'; - } else if (infos.mode == PullToRefreshIndicatorMode.refresh || - infos.mode == PullToRefreshIndicatorMode.snap) { - text = 'Loading...'; - } else if (infos.mode == PullToRefreshIndicatorMode.done) { - text = 'Refresh completed.'; - } else if (infos.mode == PullToRefreshIndicatorMode.drag) { - text = 'Pull to refresh'; - } else if (infos.mode == PullToRefreshIndicatorMode.canceled) { - text = 'Cancel refresh'; - } - - final TextStyle ts = const TextStyle( - color: Colors.grey, - ).copyWith(fontSize: 14); - - final double dragOffset = info?.dragOffset ?? 0.0; - - final DateTime time = lastRefreshTime ?? DateTime.now(); - final double top = -hideHeight + dragOffset; - return Container( - height: dragOffset, - color: color ?? Colors.transparent, - // padding: EdgeInsets.only(top: dragOffset / 3), - // padding: EdgeInsets.only(bottom: 5.0), - child: Stack( - children: [ - Positioned( - left: 0.0, - right: 0.0, - top: top, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Container( - alignment: Alignment.centerRight, - margin: const EdgeInsets.only(right: 12.0), - child: RefreshImage(top, null), - ), - ), - Column( - children: [ - Text(text, style: ts), - Text( - 'Last updated:${DateFormat('yyyy-MM-dd hh:mm').format(time)}', - style: ts.copyWith(fontSize: 14), - ) - ], - ), - const Spacer(), - ], - ), - ) - ], - ), - ); - } -} - -class RefreshImage extends StatelessWidget { - const RefreshImage(this.top, Key? key) : super(key: key); - - final double top; - - @override - Widget build(BuildContext context) { - const double imageSize = 30; - return ExtendedImage.asset( - 'assets/flutterCandies_grey.png', - width: imageSize, - height: imageSize, - afterPaintImage: (Canvas canvas, Rect rect, ui.Image image, Paint paint) { - final double imageHeight = image.height.toDouble(); - final double imageWidth = image.width.toDouble(); - final Size size = rect.size; - final double y = - (1 - min(top / (refreshHeight - hideHeight), 1)) * imageHeight; - - canvas.drawImageRect( - image, - Rect.fromLTWH(0.0, y, imageWidth, imageHeight - y), - Rect.fromLTWH(rect.left, rect.top + y / imageHeight * size.height, - size.width, (imageHeight - y) / imageHeight * size.height), - Paint() - ..colorFilter = - const ColorFilter.mode(Color(0xFFea5504), BlendMode.srcIn) - ..isAntiAlias = false - ..filterQuality = FilterQuality.low, - ); - - //canvas.restore(); - }, - ); - } -} diff --git a/lib/common/widgets/stat/danmu.dart b/lib/common/widgets/stat/danmu.dart index c1c439db..9ea05301 100644 --- a/lib/common/widgets/stat/danmu.dart +++ b/lib/common/widgets/stat/danmu.dart @@ -6,7 +6,7 @@ class StatDanMu extends StatelessWidget { final dynamic danmu; final String? size; - const StatDanMu({Key? key, this.theme, this.danmu, this.size}) + const StatDanMu({Key? key, this.theme = 'gray', this.danmu, this.size}) : super(key: key); @override @@ -14,24 +14,49 @@ class StatDanMu extends StatelessWidget { Map colorObject = { 'white': Colors.white, 'gray': Theme.of(context).colorScheme.outline, - 'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + 'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8), }; Color color = colorObject[theme]!; + return StatIconText( + icon: Icons.subtitles_outlined, + text: Utils.numFormat(danmu!), + color: color, + size: size, + ); + } +} + +class StatIconText extends StatelessWidget { + final IconData icon; + final String text; + final Color color; + final String? size; + + const StatIconText({ + Key? key, + required this.icon, + required this.text, + required this.color, + this.size, + }) : super(key: key); + + @override + Widget build(BuildContext context) { return Row( children: [ Icon( - Icons.subtitles_outlined, + icon, size: 14, color: color, ), const SizedBox(width: 2), Text( - Utils.numFormat(danmu!), + text, style: TextStyle( fontSize: size == 'medium' ? 12 : 11, color: color, ), - ) + ), ], ); } diff --git a/lib/common/widgets/stat/view.dart b/lib/common/widgets/stat/view.dart index 2665e2d4..85bec816 100644 --- a/lib/common/widgets/stat/view.dart +++ b/lib/common/widgets/stat/view.dart @@ -6,27 +6,56 @@ class StatView extends StatelessWidget { final dynamic view; final String? size; - const StatView({Key? key, this.theme, this.view, this.size}) - : super(key: key); + const StatView({ + Key? key, + this.theme = 'gray', + this.view, + this.size, + }) : super(key: key); @override Widget build(BuildContext context) { Map colorObject = { 'white': Colors.white, 'gray': Theme.of(context).colorScheme.outline, - 'black': Theme.of(context).colorScheme.onBackground.withOpacity(0.8), + 'black': Theme.of(context).colorScheme.onSurface.withOpacity(0.8), }; Color color = colorObject[theme]!; + return StatIconText( + icon: Icons.play_circle_outlined, + text: Utils.numFormat(view!), + color: color, + size: size, + ); + } +} + +class StatIconText extends StatelessWidget { + final IconData icon; + final String text; + final Color color; + final String? size; + + const StatIconText({ + Key? key, + required this.icon, + required this.text, + required this.color, + this.size, + }) : super(key: key); + + @override + Widget build(BuildContext context) { return Row( children: [ Icon( - Icons.play_circle_outlined, + icon, size: 13, color: color, ), const SizedBox(width: 2), Text( - Utils.numFormat(view!), + text, style: TextStyle( fontSize: size == 'medium' ? 12 : 11, color: color, diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart index c78643db..78c4ba87 100644 --- a/lib/common/widgets/video_card_h.dart +++ b/lib/common/widgets/video_card_h.dart @@ -1,6 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/http/constants.dart'; +import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/image_save.dart'; +import 'package:pilipala/utils/route_push.dart'; +import 'package:pilipala/utils/url_utils.dart'; import '../../http/search.dart'; import '../../http/user.dart'; import '../../http/video.dart'; @@ -16,113 +21,145 @@ class VideoCardH extends StatelessWidget { const VideoCardH({ super.key, required this.videoItem, - this.longPress, - this.longPressEnd, + this.onPressedFn, this.source = 'normal', this.showOwner = true, this.showView = true, this.showDanmaku = true, this.showPubdate = false, + this.showCharge = false, }); // ignore: prefer_typing_uninitialized_variables final videoItem; - final Function()? longPress; - final Function()? longPressEnd; + final Function()? onPressedFn; + // normal 推荐, later 稍后再看, search 搜索 final String source; final bool showOwner; final bool showView; final bool showDanmaku; final bool showPubdate; + final bool showCharge; @override Widget build(BuildContext context) { final int aid = videoItem.aid; final String bvid = videoItem.bvid; + String type = 'video'; + try { + type = videoItem.type; + } catch (_) {} final String heroTag = Utils.makeHeroTag(aid); - return GestureDetector( - onLongPress: () { - if (longPress != null) { - longPress!(); + return InkWell( + onTap: () async { + try { + if (type == 'ketang') { + SmartDialog.showToast('课堂视频暂不支持播放'); + return; + } + if (showCharge && videoItem?.typeid == 33) { + final String redirectUrl = await UrlUtils.parseRedirectUrl( + '${HttpString.baseUrl}/video/$bvid/'); + final String lastPathSegment = redirectUrl.split('/').last; + if (lastPathSegment.contains('ss')) { + RoutePush.bangumiPush( + Utils.matchNum(lastPathSegment).first, null); + } + if (lastPathSegment.contains('ep')) { + RoutePush.bangumiPush( + null, Utils.matchNum(lastPathSegment).first); + } + return; + } + final int cid = + videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); + Get.toNamed('/video?bvid=$bvid&cid=$cid', + arguments: {'videoItem': videoItem, 'heroTag': heroTag}); + } catch (err) { + SmartDialog.showToast(err.toString()); } }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async { - try { - final int cid = - videoItem.cid ?? await SearchHttp.ab2c(aid: aid, bvid: bvid); - Get.toNamed('/video?bvid=$bvid&cid=$cid', - arguments: {'videoItem': videoItem, 'heroTag': heroTag}); - } catch (err) { - SmartDialog.showToast(err.toString()); - } - }, - child: Padding( - padding: const EdgeInsets.fromLTRB( - StyleString.safeSpace, 5, StyleString.safeSpace, 5), - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints boxConstraints) { - final double width = (boxConstraints.maxWidth - - StyleString.cardSpace * - 6 / - MediaQuery.textScalerOf(context).scale(1.0)) / - 2; - return Container( - constraints: const BoxConstraints(minHeight: 88), - height: width / StyleString.aspectRatio, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder( - builder: (BuildContext context, - BoxConstraints boxConstraints) { - final double maxWidth = boxConstraints.maxWidth; - final double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: videoItem.pic as String, - width: maxWidth, - height: maxHeight, - ), + onLongPress: () => imageSaveDialog( + context, + videoItem, + SmartDialog.dismiss, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.safeSpace, 5, StyleString.safeSpace, 5), + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints boxConstraints) { + final double width = (boxConstraints.maxWidth - + StyleString.cardSpace * + 6 / + MediaQuery.textScalerOf(context).scale(1.0)) / + 2; + return Container( + constraints: const BoxConstraints(minHeight: 88), + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (BuildContext context, + BoxConstraints boxConstraints) { + final double maxWidth = boxConstraints.maxWidth; + final double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.pic as String, + width: maxWidth, + height: maxHeight, ), + ), + if (videoItem.duration != 0) PBadge( text: Utils.timeFormat(videoItem.duration!), right: 6.0, bottom: 6.0, type: 'gray', ), - // if (videoItem.rcmdReason != null && - // videoItem.rcmdReason.content != '') - // pBadge(videoItem.rcmdReason.content, context, - // 6.0, 6.0, null, null), - ], - ); - }, - ), + if (type != 'video') + PBadge( + text: type, + left: 6.0, + bottom: 6.0, + type: 'primary', + ), + // if (videoItem.rcmdReason != null && + // videoItem.rcmdReason.content != '') + // pBadge(videoItem.rcmdReason.content, context, + // 6.0, 6.0, null, null), + if (showCharge && videoItem?.isChargingSrc) + const PBadge( + text: '充电专属', + right: 6.0, + top: 6.0, + type: 'primary', + ), + ], + ); + }, ), - VideoContent( - videoItem: videoItem, - source: source, - showOwner: showOwner, - showView: showView, - showDanmaku: showDanmaku, - showPubdate: showPubdate, - ) - ], - ), - ); - }, - ), + ), + VideoContent( + videoItem: videoItem, + source: source, + showOwner: showOwner, + showView: showView, + showDanmaku: showDanmaku, + showPubdate: showPubdate, + onPressedFn: onPressedFn, + ) + ], + ), + ); + }, ), ), ); @@ -137,6 +174,7 @@ class VideoContent extends StatelessWidget { final bool showView; final bool showDanmaku; final bool showPubdate; + final Function()? onPressedFn; const VideoContent({ super.key, @@ -146,6 +184,7 @@ class VideoContent extends StatelessWidget { this.showView = true, this.showDanmaku = true, this.showPubdate = false, + this.onPressedFn, }); @override @@ -156,7 +195,7 @@ class VideoContent extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (videoItem.title is String) ...[ + if (source == 'normal' || source == 'later') ...[ Text( videoItem.title as String, textAlign: TextAlign.start, @@ -171,7 +210,7 @@ class VideoContent extends StatelessWidget { maxLines: 2, text: TextSpan( children: [ - for (final i in videoItem.title) ...[ + for (final i in videoItem.titleList) ...[ TextSpan( text: i['text'] as String, style: TextStyle( @@ -227,128 +266,49 @@ class VideoContent extends StatelessWidget { Row( children: [ if (showView) ...[ - StatView( - theme: 'gray', - view: videoItem.stat.view as int, - ), + StatView(view: videoItem.stat.view as int), const SizedBox(width: 8), ], if (showDanmaku) - StatDanMu( - theme: 'gray', - danmu: videoItem.stat.danmaku as int, - ), - + StatDanMu(danmu: videoItem.stat.danmaku as int), const Spacer(), - // SizedBox( - // width: 20, - // height: 20, - // child: IconButton( - // tooltip: '稍后再看', - // style: ButtonStyle( - // padding: MaterialStateProperty.all(EdgeInsets.zero), - // ), - // onPressed: () async { - // var res = - // await UserHttp.toViewLater(bvid: videoItem.bvid); - // SmartDialog.showToast(res['msg']); - // }, - // icon: Icon( - // Icons.more_vert_outlined, - // color: Theme.of(context).colorScheme.outline, - // size: 14, - // ), - // ), - // ), if (source == 'normal') SizedBox( width: 24, height: 24, - child: PopupMenuButton( + child: IconButton( padding: EdgeInsets.zero, + onPressed: () { + feedBack(); + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return MorePanel(videoItem: videoItem); + }, + ); + }, icon: Icon( Icons.more_vert_outlined, color: Theme.of(context).colorScheme.outline, size: 14, ), - position: PopupMenuPosition.under, - // constraints: const BoxConstraints(maxHeight: 35), - onSelected: (String type) {}, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - onTap: () async { - var res = await UserHttp.toViewLater( - bvid: videoItem.bvid as String); - SmartDialog.showToast(res['msg']); - }, - value: 'pause', - height: 40, - child: const Row( - children: [ - Icon(Icons.watch_later_outlined, size: 16), - SizedBox(width: 6), - Text('稍后再看', style: TextStyle(fontSize: 13)) - ], - ), - ), - const PopupMenuDivider(), - PopupMenuItem( - onTap: () async { - SmartDialog.show( - useSystem: true, - animationType: - SmartAnimationType.centerFade_otherSlide, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('提示'), - content: Text( - '确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' - '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'), - actions: [ - TextButton( - onPressed: () => SmartDialog.dismiss(), - child: Text( - '点错了', - style: TextStyle( - color: Theme.of(context) - .colorScheme - .outline), - ), - ), - TextButton( - onPressed: () async { - var res = await VideoHttp.relationMod( - mid: videoItem.owner.mid, - act: 5, - reSrc: 11, - ); - SmartDialog.dismiss(); - SmartDialog.showToast(res['code'] == 0 - ? '成功' - : res['msg']); - }, - child: const Text('确认'), - ) - ], - ); - }, - ); - }, - value: 'pause', - height: 40, - child: Row( - children: [ - const Icon(Icons.block, size: 16), - const SizedBox(width: 6), - Text('拉黑:${videoItem.owner.name}', - style: const TextStyle(fontSize: 13)) - ], - ), - ), - ], ), ), + if (source == 'later') ...[ + IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => onPressedFn?.call(), + icon: Icon( + Icons.clear_outlined, + color: Theme.of(context).colorScheme.outline, + size: 18, + ), + ) + ], ], ), ], @@ -357,3 +317,110 @@ class VideoContent extends StatelessWidget { ); } } + +class MorePanel extends StatelessWidget { + final dynamic videoItem; + const MorePanel({super.key, required this.videoItem}); + + Future menuActionHandler(String type) async { + switch (type) { + case 'block': + blockUser(); + break; + case 'watchLater': + var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String); + SmartDialog.showToast(res['msg']); + Get.back(); + break; + default: + } + } + + void blockUser() async { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' + '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: Text( + '点错了', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + var res = await VideoHttp.relationMod( + mid: videoItem.owner.mid, + act: 5, + reSrc: 11, + ); + SmartDialog.dismiss(); + SmartDialog.showToast(res['msg'] ?? '成功'); + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => Get.back(), + child: Container( + height: 35, + padding: const EdgeInsets.only(bottom: 2), + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.outline, + borderRadius: const BorderRadius.all(Radius.circular(3))), + ), + ), + ), + ), + ListTile( + onTap: () async => await menuActionHandler('block'), + minLeadingWidth: 0, + leading: const Icon(Icons.block, size: 19), + title: Text( + '拉黑up主 「${videoItem.owner.name}」', + style: Theme.of(context).textTheme.titleSmall, + ), + ), + ListTile( + onTap: () async => await menuActionHandler('watchLater'), + minLeadingWidth: 0, + leading: const Icon(Icons.watch_later_outlined, size: 19), + title: + Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall), + ), + ListTile( + onTap: () => + imageSaveDialog(context, videoItem, SmartDialog.dismiss), + minLeadingWidth: 0, + leading: const Icon(Icons.photo_outlined, size: 19), + title: + Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall), + ), + const SizedBox(height: 20), + ], + ), + ); + } +} diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart index 0d96f7b7..378c9f75 100644 --- a/lib/common/widgets/video_card_v.dart +++ b/lib/common/widgets/video_card_v.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/image_save.dart'; +import 'package:pilipala/utils/route_push.dart'; import '../../models/model_rec_video_item.dart'; import 'stat/danmu.dart'; import 'stat/view.dart'; import '../../http/dynamics.dart'; -import '../../http/search.dart'; import '../../http/user.dart'; import '../../http/video.dart'; -import '../../models/common/search_type.dart'; import '../../utils/id_utils.dart'; import '../../utils/utils.dart'; import '../constants.dart'; @@ -19,15 +20,13 @@ import 'network_img_layer.dart'; class VideoCardV extends StatelessWidget { final dynamic videoItem; final int crossAxisCount; - final Function()? longPress; - final Function()? longPressEnd; + final Function? blockUserCb; const VideoCardV({ Key? key, required this.videoItem, required this.crossAxisCount, - this.longPress, - this.longPressEnd, + this.blockUserCb, }) : super(key: key); bool isStringNumeric(String str) { @@ -44,23 +43,11 @@ class VideoCardV extends StatelessWidget { return; } int epId = videoItem.param; - SmartDialog.showLoading(msg: '资源获取中'); - var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId); - if (result['status']) { - var bangumiDetail = result['data']; - int cid = bangumiDetail.episodes!.first.cid; - String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid); - SmartDialog.dismiss().then( - (value) => Get.toNamed( - '/video?bvid=$bvid&cid=$cid&epId=$epId', - arguments: { - 'pic': videoItem.pic, - 'heroTag': heroTag, - 'videoType': SearchType.media_bangumi, - }, - ), - ); - } + RoutePush.bangumiPush( + null, + epId, + heroTag: heroTag, + ); break; case 'av': String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid); @@ -73,17 +60,13 @@ class VideoCardV extends StatelessWidget { // 动态 case 'picture': try { - String dynamicType = 'picture'; String uri = videoItem.uri; - String id = ''; if (videoItem.uri.startsWith('bilibili://article/')) { // https://www.bilibili.com/read/cv27063554 - dynamicType = 'read'; RegExp regex = RegExp(r'\d+'); Match match = regex.firstMatch(videoItem.uri)!; String matchedNumber = match.group(0)!; videoItem.param = int.parse(matchedNumber); - id = 'cv${videoItem.param}'; } if (uri.startsWith('http')) { String path = Uri.parse(uri).path; @@ -101,11 +84,10 @@ class VideoCardV extends StatelessWidget { return; } } - Get.toNamed('/htmlRender', parameters: { - 'url': uri, + Get.toNamed('/read', parameters: { 'title': videoItem.title, - 'id': id, - 'dynamicType': dynamicType + 'id': videoItem.param, + 'articleType': 'read' }); } catch (err) { SmartDialog.showToast(err.toString()); @@ -127,64 +109,57 @@ class VideoCardV extends StatelessWidget { @override Widget build(BuildContext context) { String heroTag = Utils.makeHeroTag(videoItem.id); - return Card( - elevation: 0, - clipBehavior: Clip.hardEdge, - margin: EdgeInsets.zero, - child: GestureDetector( - onLongPress: () { - if (longPress != null) { - longPress!(); - } - }, - // onLongPressEnd: (details) { - // if (longPressEnd != null) { - // longPressEnd!(); - // } - // }, - child: InkWell( - onTap: () async => onPushDetail(heroTag), - child: Column( - children: [ - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder(builder: (context, boxConstraints) { - double maxWidth = boxConstraints.maxWidth; - double maxHeight = boxConstraints.maxHeight; - return Stack( - children: [ - Hero( - tag: heroTag, - child: NetworkImgLayer( - src: videoItem.pic, - width: maxWidth, - height: maxHeight, - ), - ), - if (videoItem.duration > 0) - if (crossAxisCount == 1) ...[ - PBadge( - bottom: 10, - right: 10, - text: Utils.timeFormat(videoItem.duration), - ) - ] else ...[ - PBadge( - bottom: 6, - right: 7, - size: 'small', - type: 'gray', - text: Utils.timeFormat(videoItem.duration), - ) - ], + return InkWell( + onTap: () async => onPushDetail(heroTag), + onLongPress: () => imageSaveDialog( + context, + videoItem, + SmartDialog.dismiss, + ), + borderRadius: BorderRadius.circular(16), + child: Column( + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder(builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + src: videoItem.pic, + width: maxWidth, + height: maxHeight, + ), + ), + if (videoItem.duration > 0) + if (crossAxisCount == 1) ...[ + PBadge( + bottom: 10, + right: 10, + text: Utils.timeFormat(videoItem.duration), + ) + ] else ...[ + PBadge( + bottom: 6, + right: 7, + size: 'small', + type: 'gray', + text: Utils.timeFormat(videoItem.duration), + ) ], - ); - }), - ), - VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount) - ], + ], + ); + }), ), - ), + VideoContent( + videoItem: videoItem, + crossAxisCount: crossAxisCount, + blockUserCb: blockUserCb, + ) + ], ), ); } @@ -193,125 +168,101 @@ class VideoCardV extends StatelessWidget { class VideoContent extends StatelessWidget { final dynamic videoItem; final int crossAxisCount; - const VideoContent( - {Key? key, required this.videoItem, required this.crossAxisCount}) - : super(key: key); + final Function? blockUserCb; + + const VideoContent({ + Key? key, + required this.videoItem, + required this.crossAxisCount, + this.blockUserCb, + }) : super(key: key); + + Widget _buildBadge(String text, String type, [double fs = 12]) { + return PBadge( + text: text, + stack: 'normal', + size: 'small', + type: type, + fs: fs, + ); + } + @override Widget build(BuildContext context) { - return Expanded( - flex: crossAxisCount == 1 ? 0 : 1, - child: Padding( - padding: crossAxisCount == 1 - ? const EdgeInsets.fromLTRB(9, 9, 9, 4) - : const EdgeInsets.fromLTRB(5, 8, 5, 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Expanded( - child: Text( - videoItem.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (videoItem.goto == 'av' && crossAxisCount == 1) ...[ - const SizedBox(width: 10), - VideoPopupMenu( - size: 32, - iconSize: 18, - videoItem: videoItem, - ), - ], - ], - ), - if (crossAxisCount > 1) ...[ - const SizedBox(height: 2), - VideoStat( - videoItem: videoItem, - crossAxisCount: crossAxisCount, - ), - ], - if (crossAxisCount == 1) const SizedBox(height: 4), - Row( - children: [ - if (videoItem.goto == 'bangumi') ...[ - PBadge( - text: videoItem.bangumiBadge, - stack: 'normal', - size: 'small', - type: 'line', - fs: 9, - ) - ], - if (videoItem.rcmdReason != null && - videoItem.rcmdReason.content != '') ...[ - PBadge( - text: videoItem.rcmdReason.content, - stack: 'normal', - size: 'small', - type: 'color', - ) - ], - if (videoItem.goto == 'picture') ...[ - const PBadge( - text: '动态', - stack: 'normal', - size: 'small', - type: 'line', - fs: 9, - ) - ], - if (videoItem.isFollowed == 1) ...[ - const PBadge( - text: '已关注', - stack: 'normal', - size: 'small', - type: 'color', - ) - ], - Expanded( - flex: crossAxisCount == 1 ? 0 : 1, - child: Text( - videoItem.owner.name, - maxLines: 1, - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - if (crossAxisCount == 1) ...[ - Text( - ' • ', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - ), - ), - VideoStat( - videoItem: videoItem, - crossAxisCount: crossAxisCount, - ), - const Spacer(), - ], - if (videoItem.goto == 'av' && crossAxisCount != 1) ...[ - VideoPopupMenu( - size: 24, - iconSize: 14, - videoItem: videoItem, - ), - ] else ...[ - const SizedBox(height: 24) - ] - ], - ), + return Padding( + padding: crossAxisCount == 1 + ? const EdgeInsets.fromLTRB(9, 9, 9, 4) + : const EdgeInsets.fromLTRB(5, 8, 5, 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoItem.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (crossAxisCount > 1) ...[ + const SizedBox(height: 2), + VideoStat(videoItem: videoItem, crossAxisCount: crossAxisCount), ], - ), + if (crossAxisCount == 1) const SizedBox(height: 4), + Row( + children: [ + if (videoItem.goto == 'bangumi') + _buildBadge(videoItem.bangumiBadge, 'line', 9), + if (videoItem.rcmdReason != null) + _buildBadge(videoItem.rcmdReason, 'color'), + if (videoItem.goto == 'picture') _buildBadge('动态', 'line', 9), + if (videoItem.isFollowed == 1) _buildBadge('已关注', 'color'), + Expanded( + flex: crossAxisCount == 1 ? 0 : 1, + child: Text( + videoItem.owner.name, + maxLines: 1, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + if (crossAxisCount == 1) ...[ + const SizedBox(width: 10), + VideoStat( + videoItem: videoItem, + crossAxisCount: crossAxisCount, + ), + const Spacer(), + ], + if (videoItem.goto == 'av') + SizedBox( + width: 24, + height: 24, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + feedBack(); + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return MorePanel( + videoItem: videoItem, + blockUserCb: blockUserCb, + ); + }, + ); + }, + icon: Icon( + Icons.more_vert_outlined, + color: Theme.of(context).colorScheme.outline, + size: 14, + ), + ), + ) + ], + ), + ], ), ); } @@ -331,15 +282,9 @@ class VideoStat extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - StatView( - theme: 'gray', - view: videoItem.stat.view, - ), + StatView(view: videoItem.stat.view), const SizedBox(width: 8), - StatDanMu( - theme: 'gray', - danmu: videoItem.stat.danmu, - ), + StatDanMu(danmu: videoItem.stat.danmu), if (videoItem is RecVideoItemModel) ...[ crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8), RichText( @@ -358,99 +303,116 @@ class VideoStat extends StatelessWidget { } } -class VideoPopupMenu extends StatelessWidget { - final double? size; - final double? iconSize; +class MorePanel extends StatelessWidget { final dynamic videoItem; - - const VideoPopupMenu({ - Key? key, - required this.size, - required this.iconSize, + final Function? blockUserCb; + const MorePanel({ + super.key, required this.videoItem, - }) : super(key: key); + this.blockUserCb, + }); + + Future menuActionHandler(String type) async { + switch (type) { + case 'block': + Get.back(); + blockUser(); + break; + case 'watchLater': + var res = await UserHttp.toViewLater(bvid: videoItem.bvid as String); + SmartDialog.showToast(res['msg']); + Get.back(); + break; + default: + } + } + + void blockUser() async { + SmartDialog.show( + useSystem: true, + animationType: SmartAnimationType.centerFade_otherSlide, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('提示'), + content: Text('确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' + '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'), + actions: [ + TextButton( + onPressed: () => SmartDialog.dismiss(), + child: Text( + '点错了', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + var res = await VideoHttp.relationMod( + mid: videoItem.owner.mid, + act: 5, + reSrc: 11, + ); + SmartDialog.dismiss(); + if (res['status']) { + blockUserCb?.call(videoItem.owner.mid); + } + SmartDialog.showToast(res['msg']); + }, + child: const Text('确认'), + ) + ], + ); + }, + ); + } @override Widget build(BuildContext context) { - return SizedBox( - width: size, - height: size, - child: PopupMenuButton( - padding: EdgeInsets.zero, - icon: Icon( - Icons.more_vert_outlined, - color: Theme.of(context).colorScheme.outline, - size: iconSize, - ), - position: PopupMenuPosition.under, - // constraints: const BoxConstraints(maxHeight: 35), - onSelected: (String type) {}, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - onTap: () async { - var res = - await UserHttp.toViewLater(bvid: videoItem.bvid as String); - SmartDialog.showToast(res['msg']); - }, - value: 'pause', - height: 40, - child: const Row( - children: [ - Icon(Icons.watch_later_outlined, size: 16), - SizedBox(width: 6), - Text('稍后再看', style: TextStyle(fontSize: 13)) - ], + return Container( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + InkWell( + onTap: () => Get.back(), + child: Container( + height: 35, + padding: const EdgeInsets.only(bottom: 2), + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.outline, + borderRadius: const BorderRadius.all(Radius.circular(3))), + ), + ), ), ), - const PopupMenuDivider(), - PopupMenuItem( - onTap: () async { - SmartDialog.show( - useSystem: true, - animationType: SmartAnimationType.centerFade_otherSlide, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('提示'), - content: Text( - '确定拉黑:${videoItem.owner.name}(${videoItem.owner.mid})?' - '\n\n注:被拉黑的Up可以在隐私设置-黑名单管理中解除'), - actions: [ - TextButton( - onPressed: () => SmartDialog.dismiss(), - child: Text( - '点错了', - style: TextStyle( - color: Theme.of(context).colorScheme.outline), - ), - ), - TextButton( - onPressed: () async { - var res = await VideoHttp.relationMod( - mid: videoItem.owner.mid, - act: 5, - reSrc: 11, - ); - SmartDialog.dismiss(); - SmartDialog.showToast(res['msg'] ?? '成功'); - }, - child: const Text('确认'), - ) - ], - ); - }, - ); - }, - value: 'pause', - height: 40, - child: Row( - children: [ - const Icon(Icons.block, size: 16), - const SizedBox(width: 6), - Text('拉黑:${videoItem.owner.name}', - style: const TextStyle(fontSize: 13)) - ], + ListTile( + onTap: () async => await menuActionHandler('block'), + minLeadingWidth: 0, + leading: const Icon(Icons.block, size: 19), + title: Text( + '拉黑up主 「${videoItem.owner.name}」', + style: Theme.of(context).textTheme.titleSmall, ), ), + ListTile( + onTap: () async => await menuActionHandler('watchLater'), + minLeadingWidth: 0, + leading: const Icon(Icons.watch_later_outlined, size: 19), + title: + Text('添加至稍后再看', style: Theme.of(context).textTheme.titleSmall), + ), + ListTile( + onTap: () => + imageSaveDialog(context, videoItem, SmartDialog.dismiss), + minLeadingWidth: 0, + leading: const Icon(Icons.photo_outlined, size: 19), + title: + Text('查看视频封面', style: Theme.of(context).textTheme.titleSmall), + ), + const SizedBox(height: 20), ], ), ); diff --git a/lib/http/api.dart b/lib/http/api.dart index 3a23ef80..6c5374dd 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -189,7 +189,7 @@ class Api { 'https://s.search.bilibili.com/main/suggest'; // 分类搜索 - static const String searchByType = '/x/web-interface/search/type'; + static const String searchByType = '/x/web-interface/wbi/search/type'; // 记录视频播放进度 // https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/report.md @@ -400,12 +400,24 @@ class Api { '${HttpString.passBaseUrl}/x/passport-login/captcha?source=main_web'; // web端短信验证码 - static const String smsCode = + static const String webSmsCode = '${HttpString.passBaseUrl}/x/passport-login/web/sms/send'; // web端验证码登录 + static const String webSmsLogin = + '${HttpString.passBaseUrl}/x/passport-login/web/login/sms'; // web端密码登录 + static const String loginInByWebPwd = + '${HttpString.passBaseUrl}/x/passport-login/web/login'; + + // web端二维码 + static const String qrCodeApi = + '${HttpString.passBaseUrl}/x/passport-login/web/qrcode/generate'; + + // 扫码登录 + static const String loginInByQrcode = + '${HttpString.passBaseUrl}/x/passport-login/web/qrcode/poll'; // app端短信验证码 static const String appSmsCode = @@ -475,6 +487,8 @@ class Api { static const getSeasonDetailApi = '/x/polymer/web-space/seasons_archives_list'; + static const getSeriesDetailApi = '/x/series/archives'; + /// 获取未读动态数 static const getUnreadDynamic = '/x/web-interface/dynamic/entrance'; @@ -484,11 +498,17 @@ class Api { /// 激活buvid3 static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi'; + /// 获取字幕配置 + static const getSubtitleConfig = '/x/player/v2'; + /// 我的订阅 static const userSubFolder = '/x/v3/fav/folder/collected/list'; - /// 我的订阅详情 - static const userSubFolderDetail = '/x/space/fav/season/list'; + /// 我的订阅详情 type 21 + static const userSeasonList = '/x/space/fav/season/list'; + + /// 我的订阅详情 type 11 + static const userResourceList = '/x/v3/fav/resource/list'; /// 表情 static const emojiList = '/x/emote/user/panel/web'; @@ -499,4 +519,69 @@ class Api { /// 发送私信 static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg'; + + /// 排行榜 + static const String getRankApi = "/x/web-interface/ranking/v2"; + + /// 取消订阅 + static const String cancelSub = '/x/v3/fav/season/unfav'; + + /// 动态转发 + static const String dynamicForwardUrl = '/x/dynamic/feed/create/submit_check'; + + /// 创建动态 + static const String dynamicCreate = '/x/dynamic/feed/create/dyn'; + + /// 删除收藏夹 + static const String delFavFolder = '/x/v3/fav/folder/del'; + + /// 搜索结果计数 + static const String searchCount = '/x/web-interface/wbi/search/all/v2'; + + /// 关闭会话 + static const String removeSession = + '${HttpString.tUrl}/session_svr/v1/session_svr/remove_session'; + + /// 消息未读数 + static const String unread = '${HttpString.tUrl}/x/im/web/msgfeed/unread'; + + /// 回复我的 + static const String messageReplyAPi = '/x/msgfeed/reply'; + + /// 收到的赞 + static const String messageLikeAPi = '/x/msgfeed/like'; + + /// 系统通知 + static const String messageSystemAPi = + '${HttpString.messageBaseUrl}/x/sys-msg/query_unified_notify'; + + /// 系统通知标记已读 + static const String systemMarkRead = + '${HttpString.messageBaseUrl}/x/sys-msg/update_cursor'; + + /// 编辑收藏夹 + static const String editFavFolder = '/x/v3/fav/folder/edit'; + + /// 新建收藏夹 + static const String addFavFolder = '/x/v3/fav/folder/add'; + + /// 直播间弹幕信息 + static const String getDanmuInfo = + '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/getDanmuInfo'; + + /// 直播间发送弹幕 + static const String sendLiveMsg = '${HttpString.liveBaseUrl}/msg/send'; + + /// 我的关注 - 正在直播 + static const String getFollowingLive = + '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/following'; + + /// 稍后再看&收藏夹视频列表 + static const String mediaList = '/x/v2/medialist/resource/list'; + + /// 用户专栏 + static const String opusList = '/x/polymer/web-dynamic/v1/opus/feed/space'; + + /// + static const String getViewInfo = '/x/article/viewinfo'; } diff --git a/lib/http/constants.dart b/lib/http/constants.dart index 3d749ee8..b734c279 100644 --- a/lib/http/constants.dart +++ b/lib/http/constants.dart @@ -5,6 +5,8 @@ class HttpString { static const String appBaseUrl = 'https://app.bilibili.com'; static const String liveBaseUrl = 'https://api.live.bilibili.com'; static const String passBaseUrl = 'https://passport.bilibili.com'; + static const String messageBaseUrl = 'https://message.bilibili.com'; + static const String bangumiBaseUrl = 'https://bili.meark.me'; static const List validateStatusCodes = [ 302, 304, diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index d62de12f..69619361 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import '../models/dynamics/result.dart'; import '../models/dynamics/up.dart'; import 'index.dart'; @@ -40,6 +41,7 @@ class DynamicsHttp { 'status': false, 'data': [], 'msg': res.data['message'], + 'code': res.data['code'], }; } } @@ -117,4 +119,94 @@ class DynamicsHttp { }; } } + + static Future dynamicForward() async { + var res = await Request().post( + Api.dynamicForwardUrl, + queryParameters: { + 'csrf': await Request.getCsrf(), + 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, + 'x-bili-web-req-json': {'spm_id': '333.999'}, + }, + data: { + 'attach_card': null, + 'scene': 4, + 'content': { + 'conetents': [ + {'raw_text': "2", 'type': 1, 'biz_id': ""} + ] + } + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + static Future dynamicCreate({ + required int mid, + required int scene, + int? oid, + String? dynIdStr, + String? rawText, + }) async { + DateTime now = DateTime.now(); + int timestamp = now.millisecondsSinceEpoch ~/ 1000; + Random random = Random(); + int randomNumber = random.nextInt(9000) + 1000; + String uploadId = '${mid}_${timestamp}_$randomNumber'; + + Map webRepostSrc = { + 'dyn_id_str': dynIdStr ?? '', + }; + + /// 投稿转发 + if (scene == 5) { + webRepostSrc = { + 'revs_id': {'dyn_type': 8, 'rid': oid} + }; + } + var res = await Request().post(Api.dynamicCreate, queryParameters: { + 'platform': 'web', + 'csrf': await Request.getCsrf(), + 'x-bili-device-req-json': {'platform': 'web', 'device': 'pc'}, + 'x-bili-web-req-json': {'spm_id': '333.999'}, + }, data: { + 'dyn_req': { + 'content': { + 'contents': [ + {'raw_text': rawText ?? '', 'type': 1, 'biz_id': ''} + ] + }, + 'scene': scene, + 'attach_card': null, + 'upload_id': uploadId, + 'meta': { + 'app_meta': {'from': 'create.dynamic.web', 'mobi_app': 'web'} + } + }, + 'web_repost_src': webRepostSrc + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/fav.dart b/lib/http/fav.dart new file mode 100644 index 00000000..6f49d68a --- /dev/null +++ b/lib/http/fav.dart @@ -0,0 +1,67 @@ +import 'index.dart'; + +class FavHttp { + /// 编辑收藏夹 + static Future editFolder({ + required String title, + required String intro, + required String mediaId, + String? cover, + int? privacy, + }) async { + var res = await Request().post( + Api.editFavFolder, + queryParameters: { + 'title': title, + 'intro': intro, + 'media_id': mediaId, + 'cover': cover ?? '', + 'privacy': privacy ?? 0, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + /// 新建收藏夹 + static Future addFolder({ + required String title, + required String intro, + String? cover, + int? privacy, + }) async { + var res = await Request().post( + Api.addFavFolder, + queryParameters: { + 'title': title, + 'intro': intro, + 'cover': cover ?? '', + 'privacy': privacy ?? 0, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } +} diff --git a/lib/http/init.dart b/lib/http/init.dart index a0b36369..6a90a87d 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -27,11 +27,14 @@ class Request { late bool enableSystemProxy; late String systemProxyHost; late String systemProxyPort; - static final RegExp spmPrefixExp = RegExp(r''); + static final RegExp spmPrefixExp = + RegExp(r''); + static String? buvid; /// 设置cookie static setCookie() async { Box userInfoCache = GStrorage.userInfo; + Box setting = GStrorage.setting; final String cookiePath = await Utils.getCookiePath(); final PersistCookieJar cookieJar = PersistCookieJar( ignoreExpires: true, @@ -54,7 +57,11 @@ class Request { } } setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null); - + String baseUrlType = 'default'; + if (setting.get(SettingBoxKey.enableGATMode, defaultValue: false)) { + baseUrlType = 'bangumi'; + } + setBaseUrl(type: baseUrlType); try { await buvidActivate(); } catch (e) { @@ -64,6 +71,7 @@ class Request { final String cookieString = cookie .map((Cookie cookie) => '${cookie.name}=${cookie.value}') .join('; '); + dio.options.headers['cookie'] = cookieString; } @@ -78,6 +86,30 @@ class Request { return token; } + static Future getBuvid() async { + if (buvid != null) { + return buvid!; + } + + final List cookies = await cookieManager.cookieJar + .loadForRequest(Uri.parse(HttpString.baseUrl)); + buvid = cookies.firstWhere((cookie) => cookie.name == 'buvid3').value; + if (buvid == null) { + try { + var result = await Request().get( + "${HttpString.apiBaseUrl}/x/frontend/finger/spi", + ); + buvid = result["data"]["b_3"].toString(); + } catch (e) { + // 处理请求错误 + buvid = ''; + print("Error fetching buvid: $e"); + } + } + + return buvid!; + } + static setOptionsHeaders(userInfo, bool status) { if (status) { dio.options.headers['x-bili-mid'] = userInfo.mid.toString(); @@ -95,11 +127,10 @@ class Request { String spmPrefix = spmPrefixExp.firstMatch(html.data)!.group(1)!; Random rand = Random(); String rand_png_end = base64.encode( - List.generate(32, (_) => rand.nextInt(256)) + - List.filled(4, 0) + - [73, 69, 78, 68] + - List.generate(4, (_) => rand.nextInt(256)) - ); + List.generate(32, (_) => rand.nextInt(256)) + + List.filled(4, 0) + + [73, 69, 78, 68] + + List.generate(4, (_) => rand.nextInt(256))); String jsonData = json.encode({ '3064': 1, @@ -110,11 +141,9 @@ class Request { }, }); - await Request().post( - Api.activateBuvidApi, - data: {'payload': jsonData}, - options: Options(contentType: 'application/json') - ); + await Request().post(Api.activateBuvidApi, + data: {'payload': jsonData}, + options: Options(contentType: 'application/json')); } /* @@ -294,4 +323,17 @@ class Request { } return headerUa; } + + static setBaseUrl({String type = 'default'}) { + switch (type) { + case 'default': + dio.options.baseUrl = HttpString.apiBaseUrl; + break; + case 'bangumi': + dio.options.baseUrl = HttpString.bangumiBaseUrl; + break; + default: + dio.options.baseUrl = HttpString.apiBaseUrl; + } + } } diff --git a/lib/http/interceptor.dart b/lib/http/interceptor.dart index 362ff17a..259a3bf9 100644 --- a/lib/http/interceptor.dart +++ b/lib/http/interceptor.dart @@ -45,10 +45,14 @@ class ApiInterceptor extends Interceptor { void onError(DioException err, ErrorInterceptorHandler handler) async { // 处理网络请求错误 // handler.next(err); - SmartDialog.showToast( - await dioError(err), - displayType: SmartToastType.onlyRefresh, - ); + String url = err.requestOptions.uri.toString(); + final excludedPatterns = RegExp(r'heartbeat|seg\.so|online/total'); + if (!excludedPatterns.hasMatch(url)) { + SmartDialog.showToast( + await dioError(err), + displayType: SmartToastType.onlyRefresh, + ); + } super.onError(err, handler); } @@ -75,23 +79,24 @@ class ApiInterceptor extends Interceptor { } static Future checkConnect() async { - final ConnectivityResult connectivityResult = + final List connectivityResult = await Connectivity().checkConnectivity(); - switch (connectivityResult) { - case ConnectivityResult.mobile: - return '正在使用移动流量'; - case ConnectivityResult.wifi: - return '正在使用wifi'; - case ConnectivityResult.ethernet: - return '正在使用局域网'; - case ConnectivityResult.vpn: - return '正在使用代理网络'; - case ConnectivityResult.other: - return '正在使用其他网络'; - case ConnectivityResult.none: - return '未连接到任何网络'; - default: - return ''; + if (connectivityResult.contains(ConnectivityResult.mobile)) { + return '正在使用移动流量'; + } else if (connectivityResult.contains(ConnectivityResult.wifi)) { + return '正在使用wifi'; + } else if (connectivityResult.contains(ConnectivityResult.ethernet)) { + return '正在使用局域网'; + } else if (connectivityResult.contains(ConnectivityResult.vpn)) { + return '正在使用代理网络'; + } else if (connectivityResult.contains(ConnectivityResult.bluetooth)) { + return '正在使用蓝牙网络'; + } else if (connectivityResult.contains(ConnectivityResult.other)) { + return '正在使用其他网络'; + } else if (connectivityResult.contains(ConnectivityResult.none)) { + return '未连接到任何网络'; + } else { + return ''; } } } diff --git a/lib/http/live.dart b/lib/http/live.dart index e624120e..f5fd2a43 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -1,3 +1,5 @@ +import 'package:pilipala/models/live/follow.dart'; + import '../models/live/item.dart'; import '../models/live/room_info.dart'; import '../models/live/room_info_h5.dart'; @@ -65,4 +67,79 @@ class LiveHttp { }; } } + + // 获取弹幕信息 + static Future liveDanmakuInfo({roomId}) async { + var res = await Request().get(Api.getDanmuInfo, data: { + 'id': roomId, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 发送弹幕 + static Future sendDanmaku({roomId, msg}) async { + var res = await Request().post(Api.sendLiveMsg, queryParameters: { + 'bubble': 0, + 'msg': msg, + 'color': 16777215, // 颜色 + 'mode': 1, // 模式 + 'room_type': 0, + 'jumpfrom': 71001, // 直播间来源 + 'reply_mid': 0, + 'reply_attr': 0, + 'replay_dmid': '', + 'statistics': {"appId": 100, "platform": 5}, + 'fontsize': 25, // 字体大小 + 'rnd': DateTime.now().millisecondsSinceEpoch ~/ 1000, // 时间戳 + 'roomid': roomId, + 'csrf': await Request.getCsrf(), + 'csrf_token': await Request.getCsrf(), + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + // 我的关注 正在直播 + static Future liveFollowing({int? pn, int? ps}) async { + var res = await Request().get(Api.getFollowingLive, data: { + 'page': pn, + 'page_size': ps, + 'platform': 'web', + 'ignoreRecord': 1, + 'hit_ab': true, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': LiveFollowingModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/login.dart b/lib/http/login.dart index ff3fee23..2437b72a 100644 --- a/lib/http/login.dart +++ b/lib/http/login.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:dio/dio.dart'; import 'package:encrypt/encrypt.dart'; +import 'package:pilipala/http/constants.dart'; import 'package:uuid/uuid.dart'; import '../models/login/index.dart'; import '../utils/login.dart'; @@ -21,32 +22,32 @@ class LoginHttp { } } - static Future sendSmsCode({ - int? cid, - required int tel, - required String token, - required String challenge, - required String validate, - required String seccode, - }) async { - var res = await Request().post( - Api.appSmsCode, - data: { - 'cid': cid, - 'tel': tel, - "source": "main_web", - 'token': token, - 'challenge': challenge, - 'validate': validate, - 'seccode': seccode, - }, - options: Options( - contentType: Headers.formUrlEncodedContentType, - // headers: {'user-agent': ApiConstants.userAgent} - ), - ); - print(res); - } + // static Future sendSmsCode({ + // int? cid, + // required int tel, + // required String token, + // required String challenge, + // required String validate, + // required String seccode, + // }) async { + // var res = await Request().post( + // Api.appSmsCode, + // data: { + // 'cid': cid, + // 'tel': tel, + // "source": "main_web", + // 'token': token, + // 'challenge': challenge, + // 'validate': validate, + // 'seccode': seccode, + // }, + // options: Options( + // contentType: Headers.formUrlEncodedContentType, + // // headers: {'user-agent': ApiConstants.userAgent} + // ), + // ); + // print(res); + // } // web端验证码 static Future sendWebSmsCode({ @@ -60,6 +61,7 @@ class LoginHttp { Map data = { 'cid': cid, 'tel': tel, + "source": "main_web", 'token': token, 'challenge': challenge, 'validate': validate, @@ -67,17 +69,56 @@ class LoginHttp { }; FormData formData = FormData.fromMap({...data}); var res = await Request().post( - Api.smsCode, + Api.webSmsCode, data: formData, options: Options( contentType: Headers.formUrlEncodedContentType, ), ); - print(res); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } } // web端验证码登录 - static Future loginInByWebSmsCode() async {} + static Future loginInByWebSmsCode({ + int? cid, + required int tel, + required int code, + required String captchaKey, + }) async { + // webSmsLogin + Map data = { + "cid": cid, + "tel": tel, + "code": code, + "source": "main_mini", + "keep": 0, + "captcha_key": captchaKey, + "go_url": HttpString.baseUrl + }; + FormData formData = FormData.fromMap({...data}); + var res = await Request().post( + Api.webSmsLogin, + data: formData, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } // web端密码登录 static Future liginInByWebPwd() async {} @@ -173,4 +214,69 @@ class LoginHttp { ); print(res); } + + // web端密码登录 + static Future loginInByWebPwd({ + required int username, + required String password, + required String token, + required String challenge, + required String validate, + required String seccode, + }) async { + Map data = { + 'username': username, + 'password': password, + 'keep': 0, + 'token': token, + 'challenge': challenge, + 'validate': validate, + 'seccode': seccode, + 'source': 'main-fe-header', + "go_url": HttpString.baseUrl + }; + FormData formData = FormData.fromMap({...data}); + var res = await Request().post( + Api.loginInByWebPwd, + data: formData, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // web端登录二维码 + static Future getWebQrcode() async { + var res = await Request().get(Api.qrCodeApi); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } + + // web端二维码轮询登录状态 + static Future queryWebQrcodeStatus(String qrcodeKey) async { + var res = await Request() + .get(Api.loginInByQrcode, data: {'qrcode_key': qrcodeKey}); + if (res.data['data']['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/http/member.dart b/lib/http/member.dart index 1af0f9a4..459d6747 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -1,5 +1,9 @@ +import 'dart:convert'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:hive/hive.dart'; +import 'package:html/parser.dart'; +import 'package:pilipala/models/member/article.dart'; +import 'package:pilipala/models/member/like.dart'; import '../common/constants.dart'; import '../models/dynamics/result.dart'; import '../models/follow/result.dart'; @@ -95,7 +99,14 @@ class MemberHttp { 'dm_img_str': dmImgStr.substring(0, dmImgStr.length - 2), 'dm_cover_img_str': dmCoverImgStr.substring(0, dmCoverImgStr.length - 2), 'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}', + ...order == 'charge' + ? { + 'order': 'pubdate', + 'special_type': 'charging', + } + : {} }); + var res = await Request().get( Api.memberArchive, data: params, @@ -328,7 +339,9 @@ class MemberHttp { if (res.data['code'] == 0) { return { 'status': true, - 'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists']) + 'data': res.data['data']['list'] + .map((e) => MemberLikeDataModel.fromJson(e)) + .toList(), }; } else { return { @@ -510,4 +523,96 @@ class MemberHttp { }; } } + + static Future getSeriesDetail({ + required int mid, + required int currentMid, + required int seriesId, + required int pn, + }) async { + var res = await Request().get( + Api.getSeriesDetailApi, + data: { + 'mid': mid, + 'series_id': seriesId, + 'only_normal': true, + 'sort': 'desc', + 'pn': pn, + 'ps': 30, + 'current_mid': currentMid, + }, + ); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MemberSeasonsDataModel.fromJson(res.data['data']) + }; + } catch (err) { + print(err); + } + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } + + static Future getWWebid({required int mid}) async { + var res = await Request().get('https://space.bilibili.com/$mid/article'); + String? headContent = parse(res.data).head?.outerHtml; + final regex = RegExp( + r''); + if (headContent != null) { + final match = regex.firstMatch(headContent); + if (match != null && match.groupCount >= 1) { + final content = match.group(1); + String decodedString = Uri.decodeComponent(content!); + Map map = jsonDecode(decodedString); + return {'status': true, 'data': map['access_id']}; + } else { + return {'status': false, 'data': '请检查登录状态'}; + } + } + return {'status': false, 'data': '请检查登录状态'}; + } + + // 获取用户专栏 + static Future getMemberArticle({ + required int mid, + required int pn, + required String wWebid, + String? offset, + }) async { + Map params = await WbiSign().makSign({ + 'host_mid': mid, + 'page': pn, + 'offset': offset, + 'web_location': 333.999, + 'w_webid': wWebid, + }); + var res = await Request().get(Api.opusList, data: { + 'host_mid': mid, + 'page': pn, + 'offset': offset, + 'web_location': 333.999, + 'w_webid': wWebid, + 'w_rid': params['w_rid'], + 'wts': params['wts'], + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': MemberArticleDataModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'] ?? '请求异常', + }; + } + } } diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 4ba2f818..2de9cd49 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,4 +1,9 @@ +import 'dart:convert'; import 'dart:math'; +import 'package:dio/dio.dart'; +import 'package:pilipala/models/msg/like.dart'; +import 'package:pilipala/models/msg/reply.dart'; +import 'package:pilipala/models/msg/system.dart'; import '../models/msg/account.dart'; import '../models/msg/session.dart'; import '../utils/wbi_sign.dart'; @@ -31,14 +36,14 @@ class MsgHttp { } catch (err) { return { 'status': false, - 'date': [], + 'data': [], 'msg': err.toString(), }; } } else { return { 'status': false, - 'date': [], + 'data': [], 'msg': res.data['message'], }; } @@ -122,68 +127,48 @@ class MsgHttp { 'data': res.data['data'], }; } else { - return { - 'status': false, - 'date': [], - 'msg': "message: ${res.data['message']}," - " msg: ${res.data['msg']}," - " code: ${res.data['code']}", - }; + return {'status': false, 'date': [], 'msg': res.data['message']}; } } // 发送私信 static Future sendMsg({ - int? senderUid, - int? receiverId, + required int senderUid, + required int receiverId, int? receiverType, int? msgType, dynamic content, }) async { String csrf = await Request.getCsrf(); - Map params = await WbiSign().makSign({ - 'msg[sender_uid]': senderUid, - 'msg[receiver_id]': receiverId, - 'msg[receiver_type]': receiverType ?? 1, - 'msg[msg_type]': msgType ?? 1, - 'msg[msg_status]': 0, - 'msg[dev_id]': getDevId(), - 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'msg[new_face_version]': 0, - 'msg[content]': content, - 'from_firework': 0, - 'build': 0, - 'mobi_app': 'web', - 'csrf_token': csrf, - 'csrf': csrf, - }); - var res = - await Request().post(Api.sendMsg, queryParameters: { - ...params, - 'csrf_token': csrf, - 'csrf': csrf, - }, data: { - 'w_sender_uid': params['msg[sender_uid]'], - 'w_receiver_id': params['msg[receiver_id]'], - 'w_dev_id': params['msg[dev_id]'], - 'w_rid': params['w_rid'], - 'wts': params['wts'], - 'csrf_token': csrf, - 'csrf': csrf, - }); + var res = await Request().post( + Api.sendMsg, + data: { + 'msg[sender_uid]': senderUid, + 'msg[receiver_id]': receiverId, + 'msg[receiver_type]': 1, + 'msg[msg_type]': 1, + 'msg[msg_status]': 0, + 'msg[content]': jsonEncode(content), + 'msg[timestamp]': DateTime.now().millisecondsSinceEpoch ~/ 1000, + 'msg[new_face_version]': 1, + 'msg[dev_id]': getDevId(), + 'from_firework': 0, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf, + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); if (res.data['code'] == 0) { return { 'status': true, 'data': res.data['data'], }; } else { - return { - 'status': false, - 'date': [], - 'msg': "message: ${res.data['message']}," - " msg: ${res.data['msg']}," - " code: ${res.data['code']}", - }; + return {'status': false, 'date': [], 'msg': res.data['message']}; } } @@ -220,4 +205,129 @@ class MsgHttp { } return s.join(); } + + static Future removeSession({ + int? talkerId, + }) async { + String csrf = await Request.getCsrf(); + Map params = await WbiSign().makSign({ + 'talker_id': talkerId, + 'session_type': 1, + 'build': 0, + 'mobi_app': 'web', + 'csrf_token': csrf, + 'csrf': csrf + }); + var res = await Request().get(Api.removeSession, data: params); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + static Future unread() async { + var res = await Request().get(Api.unread); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': res.data['data'], + }; + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + // 回复我的 + static Future messageReply({ + int? id, + int? replyTime, + }) async { + var params = { + if (id != null) 'id': id, + if (replyTime != null) 'reply_time': replyTime, + }; + var res = await Request().get(Api.messageReplyAPi, data: params); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MessageReplyModel.fromJson(res.data['data']), + }; + } catch (err) { + return {'status': false, 'date': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + // 收到的赞 + static Future messageLike({ + int? id, + int? likeTime, + }) async { + var params = { + if (id != null) 'id': id, + if (likeTime != null) 'like_time': likeTime, + }; + var res = await Request().get(Api.messageLikeAPi, data: params); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': MessageLikeModel.fromJson(res.data['data']), + }; + } catch (err) { + return {'status': false, 'date': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + static Future messageSystem() async { + var res = await Request().get(Api.messageSystemAPi, data: { + 'csrf': await Request.getCsrf(), + 'page_size': 20, + 'build': 0, + 'mobi_app': 'web', + }); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': res.data['data']['system_notify_list'] + .map((e) => MessageSystemModel.fromJson(e)) + .toList(), + }; + } catch (err) { + return {'status': false, 'date': [], 'msg': err.toString()}; + } + } else { + return {'status': false, 'date': [], 'msg': res.data['message']}; + } + } + + // 系统消息标记已读 + static Future systemMarkRead(int cursor) async { + String csrf = await Request.getCsrf(); + var res = await Request().get(Api.systemMarkRead, data: { + 'csrf': csrf, + 'cursor': cursor, + }); + if (res.data['code'] == 0) { + return { + 'status': true, + }; + } else { + return { + 'status': false, + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/http/read.dart b/lib/http/read.dart new file mode 100644 index 00000000..68e72e59 --- /dev/null +++ b/lib/http/read.dart @@ -0,0 +1,116 @@ +import 'dart:convert'; +import 'package:html/parser.dart'; +import 'package:pilipala/models/read/opus.dart'; +import 'package:pilipala/models/read/read.dart'; +import 'package:pilipala/utils/wbi_sign.dart'; +import 'index.dart'; + +class ReadHttp { + static List extractScriptContents(String htmlContent) { + RegExp scriptRegExp = RegExp(r'