diff --git a/.github/workflows/beta_ci.yml b/.github/workflows/beta_ci.yml
index 40f3f042..9c40de6b 100644
--- a/.github/workflows/beta_ci.yml
+++ b/.github/workflows/beta_ci.yml
@@ -12,7 +12,6 @@ on:
- ".idea/**"
- "!.github/workflows/**"
-
jobs:
update_version:
name: Read and update version
@@ -96,7 +95,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2
with:
- flutter-version: 3.16.5
+ flutter-version: 3.19.6
channel: any
- name: 下载项目依赖
@@ -206,4 +205,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/.github/workflows/release_ci.yml b/.github/workflows/release_ci.yml
index 78230645..f7c06d29 100644
--- a/.github/workflows/release_ci.yml
+++ b/.github/workflows/release_ci.yml
@@ -36,7 +36,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2
with:
- flutter-version: 3.16.5
+ flutter-version: 3.19.6
channel: any
- name: 下载项目依赖
@@ -98,7 +98,7 @@ jobs:
uses: subosito/flutter-action@v2.10.0
with:
cache: true
- flutter-version: 3.16.5
+ flutter-version: 3.19.6
- name: flutter build ipa
run: |
diff --git a/README.md b/README.md
index 470e9a35..228d17bb 100644
--- a/README.md
+++ b/README.md
@@ -94,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..46b34c20 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -12,7 +12,6 @@
-
@@ -20,7 +19,6 @@
"android.support.customtabs.action.CustomTabsService" />
-
@@ -34,7 +32,6 @@
-
+
+
+
+
+
+
+
+
+
@@ -132,102 +141,55 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/guozhigq/pilipala/MainActivity.kt b/android/app/src/main/kotlin/com/guozhigq/pilipala/MainActivity.kt
index 117c85ef..b6876760 100644
--- a/android/app/src/main/kotlin/com/guozhigq/pilipala/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/guozhigq/pilipala/MainActivity.kt
@@ -1,6 +1,8 @@
package com.guozhigq.pilipala
-import io.flutter.embedding.android.FlutterActivity
+// import io.flutter.embedding.android.FlutterActivity
+import com.ryanheise.audioservice.AudioServiceActivity;
-class MainActivity: FlutterActivity() {
+class MainActivity: AudioServiceActivity() {
+
}
diff --git a/android/build.gradle b/android/build.gradle
index 713d7f6e..674e96f4 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,5 +1,5 @@
buildscript {
- ext.kotlin_version = '1.7.10'
+ ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
diff --git a/assets/images/coin.png b/assets/images/coin.png
new file mode 100644
index 00000000..afca87b2
Binary files /dev/null and b/assets/images/coin.png differ
diff --git a/assets/images/pay/alipay.jpg b/assets/images/pay/alipay.jpg
new file mode 100644
index 00000000..1c1fc4c6
Binary files /dev/null and b/assets/images/pay/alipay.jpg differ
diff --git a/assets/images/pay/wechat.png b/assets/images/pay/wechat.png
new file mode 100644
index 00000000..3aa3a6a2
Binary files /dev/null and b/assets/images/pay/wechat.png differ
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/change_log/1.0.25.1010.md b/change_log/1.0.25.1010.md
new file mode 100644
index 00000000..951efcb1
--- /dev/null
+++ b/change_log/1.0.25.1010.md
@@ -0,0 +1,39 @@
+## 1.0.25
+
+### 功能
++ 直播弹幕
++ 稍后再看、收藏夹播放全部
++ 收藏夹新建、编辑
++ 评论删除
++ 评论保存为图片
++ 动态页滑动切换up
++ up投稿筛选充电视频
++ 直播tab展示关注up
++ up主页专栏展示
+
+### 优化
++ 视频详情页一键三连
++ 动态页标识充电视频
++ 播放器亮度、音量调整百分比展示
++ 封面预览时视频标题可复制
++ 竖屏直播布局
++ 图片预览
++ 专栏渲染优化
++ 私信图片查看
+
+### 修复
++ 收藏夹点击异常
++ 搜索up异常
++ 系统通知已读异常
++ [赞了我的]展示错误
++ 部分up合集无法打开
++ 切换合集视频投币个数未重置
++ 搜索条件筛选面板无法滚动
++ 部分机型导航条未沉浸
++ 专栏图片渲染问题
++ 专栏浏览历史记录
++ 直播间历史记录
+
+
+更多更新日志可在Github上查看
+问题反馈、功能建议请查看「关于」页面。
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 2c1a635b..27baf9e5 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,5 +1,5 @@
PODS:
- - appscheme (1.0.4):
+ - app_links (0.0.2):
- Flutter
- audio_service (0.0.1):
- Flutter
@@ -10,7 +10,6 @@ PODS:
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- - ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
@@ -24,10 +23,12 @@ 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)
+ - image_picker_ios (0.0.1):
+ - Flutter
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
@@ -41,7 +42,6 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- - ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0):
@@ -68,7 +68,7 @@ PODS:
- Flutter
DEPENDENCIES:
- - appscheme (from `.symlinks/plugins/appscheme/ios`)
+ - app_links (from `.symlinks/plugins/app_links/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
@@ -79,6 +79,7 @@ DEPENDENCIES:
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
+ - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@@ -101,12 +102,11 @@ SPEC REPOS:
trunk:
- FMDB
- GT3Captcha-iOS
- - ReachabilitySwift
- Toast
EXTERNAL SOURCES:
- appscheme:
- :path: ".symlinks/plugins/appscheme/ios"
+ app_links:
+ :path: ".symlinks/plugins/app_links/ios"
audio_service:
:path: ".symlinks/plugins/audio_service/ios"
audio_session:
@@ -127,6 +127,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios"
gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
+ image_picker_ios:
+ :path: ".symlinks/plugins/image_picker_ios/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
@@ -163,26 +165,26 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
- appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
+ app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
- connectivity_plus: e2dad488011aeb593e219360e804c43cc1af5770
+ 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
+ image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
- path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
+ path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
- ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@@ -190,11 +192,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: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4
+ 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..24dceb17 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -49,6 +49,8 @@
NSPhotoLibraryAddUsageDescription
请允许APP保存图片到相册
+ NSPhotoLibraryUsageDescription
+ 请允许APP保存图片到相册
NSCameraUsageDescription
App需要您的同意,才能访问相册
NSAppleMusicUsageDescription
@@ -63,44 +65,29 @@
CFBundleURLName
-
+ bilibili
CFBundleURLSchemes
http
https
-
- CFBundleURLTypes
-
-
- CFBundleURLName
-
- CFBundleURLSchemes
-
- m.bilibili.com
- bilibili.com
- www.bilibili.com
- bangumi.bilibili.com
- bilibili.cn
- www.bilibili.cn
- bangumi.bilibili.cn
- bilibili.tv
- www.bilibili.tv
- bangumi.bilibili.tv
- miniapp.bilibili.com
- live.bilibili.com
-
-
-
-
-
-
-
- CFBundleURLName
- bilibili
- CFBundleURLSchemes
-
bilibili
+ m.bilibili.com
+ bilibili.com
+ www.bilibili.com
+ bangumi.bilibili.com
+ bilibili.cn
+ www.bilibili.cn
+ bangumi.bilibili.cn
+ bilibili.tv
+ www.bilibili.tv
+ bangumi.bilibili.tv
+ miniapp.bilibili.com
+ live.bilibili.com
+ pili
+ pilipala
+ FlutterDeepLinkingEnabled
+
UIBackgroundModes
diff --git a/lib/common/constants.dart b/lib/common/constants.dart
index cac13688..0607206c 100644
--- a/lib/common/constants.dart
+++ b/lib/common/constants.dart
@@ -15,6 +15,4 @@ class Constants {
// 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
- static const String thirdApi =
- 'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
}
diff --git a/lib/common/pages_bottom_sheet.dart b/lib/common/pages_bottom_sheet.dart
index c64b58b6..e4f23608 100644
--- a/lib/common/pages_bottom_sheet.dart
+++ b/lib/common/pages_bottom_sheet.dart
@@ -1,34 +1,462 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
-import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/common/constants.dart';
+import 'package:pilipala/common/widgets/network_img_layer.dart';
+import 'package:pilipala/http/video.dart';
+import 'package:pilipala/models/video_detail_res.dart';
+import 'package:pilipala/pages/video/detail/index.dart';
+import 'package:pilipala/utils/utils.dart';
+import 'package:scrollview_observer/scrollview_observer.dart';
import '../models/common/video_episode_type.dart';
+import 'widgets/badge.dart';
+import 'widgets/stat/danmu.dart';
+import 'widgets/stat/view.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;
+ final UgcSeason? ugcSeason;
+ final int? currentEpisodeIndex;
+ final int? currentIndex;
EpisodeBottomSheet({
required this.episodes,
required this.currentCid,
required this.dataType,
- required this.context,
required this.changeFucCall,
this.cid,
this.sheetHeight,
this.isFullScreen = false,
+ this.ugcSeason,
+ this.currentEpisodeIndex,
+ this.currentIndex,
});
- Widget buildEpisodeListItem(
- dynamic episode,
- int index,
- bool isCurrentIndex,
- ) {
+ Widget buildShowContent() {
+ return PagesBottomSheet(
+ episodes: episodes,
+ currentCid: currentCid,
+ dataType: dataType,
+ changeFucCall: changeFucCall,
+ cid: cid,
+ sheetHeight: sheetHeight,
+ isFullScreen: isFullScreen,
+ ugcSeason: ugcSeason,
+ currentEpisodeIndex: currentEpisodeIndex,
+ currentIndex: currentIndex,
+ );
+ }
+
+ PersistentBottomSheetController show(BuildContext context) {
+ final PersistentBottomSheetController btmSheetCtr = showBottomSheet(
+ context: context,
+ builder: (BuildContext context) {
+ return buildShowContent();
+ },
+ );
+ return btmSheetCtr;
+ }
+}
+
+class PagesBottomSheet extends StatefulWidget {
+ const PagesBottomSheet({
+ super.key,
+ required this.episodes,
+ required this.currentCid,
+ required this.dataType,
+ required this.changeFucCall,
+ this.cid,
+ this.sheetHeight,
+ this.isFullScreen = false,
+ this.ugcSeason,
+ this.currentEpisodeIndex,
+ this.currentIndex,
+ });
+
+ final List episodes;
+ final int currentCid;
+ final dynamic dataType;
+ final Function changeFucCall;
+ final int? cid;
+ final double? sheetHeight;
+ final bool isFullScreen;
+ final UgcSeason? ugcSeason;
+ final int? currentEpisodeIndex;
+ final int? currentIndex;
+
+ @override
+ State createState() => _PagesBottomSheetState();
+}
+
+class _PagesBottomSheetState extends State
+ with TickerProviderStateMixin {
+ final ScrollController _listScrollController = ScrollController();
+ late ListObserverController _listObserverController;
+ final ScrollController _scrollController = ScrollController();
+ late int currentIndex;
+ TabController? tabController;
+ List? _listObserverControllerList;
+ List? _listScrollControllerList;
+ final String heroTag = Get.arguments['heroTag'];
+ VideoDetailController? _videoDetailController;
+ RxInt isSubscribe = (-1).obs;
+ bool isVisible = false;
+
+ @override
+ void initState() {
+ super.initState();
+ currentIndex = widget.currentIndex ??
+ widget.episodes.indexWhere((dynamic e) => e.cid == widget.currentCid);
+ _scrollToInit();
+ _scrollPositionInit();
+ if (widget.dataType == VideoEpidoesType.videoEpisode) {
+ _videoDetailController = Get.find(tag: heroTag);
+ _getSubscribeStatus();
+ }
+ }
+
+ String prefix() {
+ switch (widget.dataType) {
+ case VideoEpidoesType.videoEpisode:
+ return '选集';
+ case VideoEpidoesType.videoPart:
+ return '分集';
+ case VideoEpidoesType.bangumiEpisode:
+ return '选集';
+ }
+ return '选集';
+ }
+
+ // 滚动器初始化
+ void _scrollToInit() {
+ /// 单个
+ _listObserverController =
+ ListObserverController(controller: _listScrollController);
+
+ if (widget.dataType == VideoEpidoesType.videoEpisode &&
+ widget.ugcSeason?.sections != null &&
+ widget.ugcSeason!.sections!.length > 1) {
+ tabController = TabController(
+ length: widget.ugcSeason!.sections!.length,
+ vsync: this,
+ initialIndex: widget.currentEpisodeIndex ?? 0,
+ );
+
+ /// 多tab
+ _listScrollControllerList = List.generate(
+ widget.ugcSeason!.sections!.length,
+ (index) {
+ return ScrollController();
+ },
+ );
+ _listObserverControllerList = List.generate(
+ widget.ugcSeason!.sections!.length,
+ (index) {
+ return ListObserverController(
+ controller: _listScrollControllerList![index],
+ );
+ },
+ );
+ }
+ }
+
+ // 滚动器位置初始化
+ void _scrollPositionInit() {
+ if (widget.dataType == VideoEpidoesType.videoEpisode) {
+ // 单个 多tab
+ if (widget.ugcSeason?.sections != null) {
+ if (widget.ugcSeason!.sections!.length == 1) {
+ _listObserverController.initialIndexModel =
+ ObserverIndexPositionModel(
+ index: currentIndex,
+ isFixedHeight: true,
+ );
+ } else {
+ _listObserverControllerList![widget.currentEpisodeIndex!]
+ .initialIndexModel = ObserverIndexPositionModel(
+ index: currentIndex,
+ isFixedHeight: true,
+ );
+ }
+ }
+ }
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (widget.dataType != VideoEpidoesType.videoEpisode) {
+ double itemHeight = (widget.isFullScreen
+ ? 400
+ : Get.size.width - 3 * StyleString.safeSpace) /
+ 5.2;
+ double offset = ((currentIndex - 1) / 2).ceil() * itemHeight;
+ _scrollController.jumpTo(offset);
+ }
+ });
+ }
+
+ // 获取订阅状态
+ void _getSubscribeStatus() async {
+ var res =
+ await VideoHttp.getSubscribeStatus(bvid: _videoDetailController!.bvid);
+ if (res['status']) {
+ isSubscribe.value = res['data']['season_fav'] ? 1 : 0;
+ }
+ }
+
+ // 更改订阅状态
+ void _changeSubscribeStatus() async {
+ if (isSubscribe.value == -1) {
+ return;
+ }
+ dynamic result = await VideoHttp.seasonFav(
+ isFav: isSubscribe.value == 1,
+ seasonId: widget.ugcSeason!.id,
+ );
+ if (result['status']) {
+ SmartDialog.showToast(isSubscribe.value == 1 ? '取消订阅成功' : '订阅成功');
+ isSubscribe.value = isSubscribe.value == 1 ? 0 : 1;
+ } else {
+ SmartDialog.showToast(result['msg']);
+ }
+ }
+
+ // 更改展开状态
+ void _changeVisible() {
+ setState(() {
+ isVisible = !isVisible;
+ });
+ }
+
+ @override
+ void dispose() {
+ try {
+ _listObserverController.controller?.dispose();
+ _listScrollController.dispose();
+ for (var element in _listObserverControllerList!) {
+ element.controller?.dispose();
+ }
+ for (var element in _listScrollControllerList!) {
+ element.dispose();
+ }
+ } catch (_) {}
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return StatefulBuilder(
+ builder: (BuildContext context, StateSetter setState) {
+ return SizedBox(
+ height: widget.sheetHeight,
+ child: Column(
+ children: [
+ TitleBar(
+ title: '${prefix()}(${widget.episodes.length})',
+ isFullScreen: widget.isFullScreen,
+ ),
+ if (widget.ugcSeason != null) ...[
+ UgcSeasonBuild(
+ ugcSeason: widget.ugcSeason!,
+ isSubscribe: isSubscribe,
+ isVisible: isVisible,
+ changeFucCall: _changeSubscribeStatus,
+ changeVisible: _changeVisible,
+ ),
+ ],
+ Expanded(
+ child: Material(
+ child: widget.dataType == VideoEpidoesType.videoEpisode
+ ? (widget.ugcSeason!.sections!.length == 1
+ ? ListViewObserver(
+ controller: _listObserverController,
+ child: ListView.builder(
+ controller: _listScrollController,
+ itemCount: widget.episodes.length + 1,
+ itemBuilder: (BuildContext context, int index) {
+ bool isLastItem =
+ index == widget.episodes.length;
+ bool isCurrentIndex = currentIndex == index;
+ return isLastItem
+ ? SizedBox(
+ height: MediaQuery.of(context)
+ .padding
+ .bottom +
+ 20,
+ )
+ : EpisodeListItem(
+ episode: widget.episodes[index],
+ index: index,
+ isCurrentIndex: isCurrentIndex,
+ dataType: widget.dataType,
+ changeFucCall: widget.changeFucCall,
+ isFullScreen: widget.isFullScreen,
+ );
+ },
+ ),
+ )
+ : buildTabBar())
+ : Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12.0), // 设置左右间距为12
+ child: GridView.count(
+ controller: _scrollController,
+ crossAxisCount: 2,
+ crossAxisSpacing: StyleString.safeSpace,
+ childAspectRatio: 2.6,
+ children: List.generate(
+ widget.episodes.length,
+ (index) {
+ bool isCurrentIndex = currentIndex == index;
+ return EpisodeGridItem(
+ episode: widget.episodes[index],
+ index: index,
+ isCurrentIndex: isCurrentIndex,
+ dataType: widget.dataType,
+ changeFucCall: widget.changeFucCall,
+ isFullScreen: widget.isFullScreen,
+ );
+ },
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ });
+ }
+
+ Widget buildTabBar() {
+ return Column(
+ children: [
+ // 背景色
+ Container(
+ color: Theme.of(context).colorScheme.surface,
+ child: TabBar(
+ controller: tabController,
+ isScrollable: true,
+ indicatorSize: TabBarIndicatorSize.label,
+ tabAlignment: TabAlignment.start,
+ splashBorderRadius: BorderRadius.circular(4),
+ tabs: [
+ ...widget.ugcSeason!.sections!.map((SectionItem section) {
+ return Tab(
+ text: section.title,
+ );
+ }).toList()
+ ],
+ ),
+ ),
+ Expanded(
+ child: TabBarView(
+ controller: tabController,
+ children: [
+ ...widget.ugcSeason!.sections!.map((SectionItem section) {
+ final int fIndex = widget.ugcSeason!.sections!.indexOf(section);
+ return ListViewObserver(
+ controller: _listObserverControllerList![fIndex],
+ child: ListView.builder(
+ controller: _listScrollControllerList![fIndex],
+ itemCount: section.episodes!.length + 1,
+ itemBuilder: (BuildContext context, int index) {
+ final bool isLastItem = index == section.episodes!.length;
+ return isLastItem
+ ? SizedBox(
+ height:
+ MediaQuery.of(context).padding.bottom + 20,
+ )
+ : EpisodeListItem(
+ episode: section.episodes![index], // 调整索引
+ index: index, // 调整索引
+ isCurrentIndex: widget.currentCid ==
+ section.episodes![index].cid,
+ dataType: widget.dataType,
+ changeFucCall: widget.changeFucCall,
+ isFullScreen: widget.isFullScreen,
+ );
+ },
+ ),
+ );
+ }).toList()
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class TitleBar extends StatelessWidget {
+ final String title;
+ final bool isFullScreen;
+
+ const TitleBar({
+ Key? key,
+ required this.title,
+ required this.isFullScreen,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return AppBar(
+ toolbarHeight: 45,
+ automaticallyImplyLeading: false,
+ centerTitle: false,
+ elevation: 1,
+ scrolledUnderElevation: 1,
+ title: Padding(
+ padding: const EdgeInsets.only(left: 12),
+ child: Text(
+ title,
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ ),
+ actions: !isFullScreen
+ ? [
+ SizedBox(
+ width: 35,
+ height: 35,
+ child: IconButton(
+ icon: const Icon(Icons.close, size: 20),
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(EdgeInsets.zero),
+ ),
+ onPressed: () => Navigator.pop(context),
+ ),
+ ),
+ const SizedBox(width: 8),
+ ]
+ : null,
+ );
+ }
+}
+
+class EpisodeListItem extends StatelessWidget {
+ final dynamic episode;
+ final int index;
+ final bool isCurrentIndex;
+ final dynamic dataType;
+ final Function changeFucCall;
+ final bool isFullScreen;
+
+ const EpisodeListItem({
+ Key? key,
+ required this.episode,
+ required this.index,
+ required this.isCurrentIndex,
+ required this.dataType,
+ required this.changeFucCall,
+ required this.isFullScreen,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary;
Color onSurface = Theme.of(context).colorScheme.onSurface;
@@ -44,15 +472,26 @@ class EpisodeBottomSheet {
title = '第${episode.title}话 ${episode.longTitle!}';
break;
}
+
+ return isFullScreen || episode?.cover == null || episode?.cover == ''
+ ? _buildListTile(context, title, primary, onSurface)
+ : _buildInkWell(context, title, primary, onSurface);
+ }
+
+ Widget _buildListTile(
+ BuildContext context, String title, Color primary, Color onSurface) {
return ListTile(
onTap: () {
+ if (isCurrentIndex) {
+ return;
+ }
SmartDialog.showToast('切换至「$title」');
changeFucCall.call(episode, index);
},
dense: false,
leading: isCurrentIndex
? Image.asset(
- 'assets/images/live.gif',
+ 'assets/images/live.png',
color: primary,
height: 12,
)
@@ -67,80 +506,331 @@ class EpisodeBottomSheet {
);
}
- 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),
+ Widget _buildInkWell(
+ BuildContext context, String title, Color primary, Color onSurface) {
+ return InkWell(
+ onTap: () {
+ if (isCurrentIndex) {
+ return;
+ }
+ SmartDialog.showToast('切换至「$title」');
+ changeFucCall.call(episode, index);
+ },
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(
+ StyleString.safeSpace, 6, StyleString.safeSpace, 6),
+ child: LayoutBuilder(
+ builder: (BuildContext context, BoxConstraints boxConstraints) {
+ const double width = 160;
+ 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: [
+ NetworkImgLayer(
+ src: episode?.cover ?? '',
+ width: maxWidth,
+ height: maxHeight,
+ ),
+ if (episode.duration != 0)
+ PBadge(
+ text: Utils.timeFormat(episode.duration!),
+ right: 6.0,
+ bottom: 6.0,
+ type: 'gray',
+ ),
+ ],
+ );
+ },
+ ),
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ episode.title as String,
+ textAlign: TextAlign.start,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ fontWeight: FontWeight.w500,
+ color: isCurrentIndex ? primary : onSurface,
+ ),
+ ),
+ const Spacer(),
+ if (dataType != VideoEpidoesType.videoPart) ...[
+ if (episode?.pubdate != null ||
+ episode.pubTime != null)
+ Text(
+ Utils.dateFormat(
+ episode?.pubdate ?? episode.pubTime),
+ style: TextStyle(
+ fontSize: 11,
+ color:
+ Theme.of(context).colorScheme.outline),
+ ),
+ const SizedBox(height: 2),
+ if (episode.stat != null)
+ Row(
+ children: [
+ StatView(view: episode.stat.view),
+ const SizedBox(width: 8),
+ StatDanMu(danmu: episode.stat.danmaku),
+ const Spacer(),
+ ],
+ ),
+ const SizedBox(height: 4),
+ ]
+ ],
+ ),
+ ),
+ )
+ ],
),
- 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.background,
- 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,
- );
- },
+class EpisodeGridItem extends StatelessWidget {
+ final dynamic episode;
+ final int index;
+ final bool isCurrentIndex;
+ final dynamic dataType;
+ final Function changeFucCall;
+ final bool isFullScreen;
+
+ const EpisodeGridItem({
+ Key? key,
+ required this.episode,
+ required this.index,
+ required this.isCurrentIndex,
+ required this.dataType,
+ required this.changeFucCall,
+ required this.isFullScreen,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ ColorScheme colorScheme = Theme.of(context).colorScheme;
+ TextStyle textStyle = TextStyle(
+ color: isCurrentIndex ? colorScheme.primary : colorScheme.onSurface,
+ fontSize: 14,
+ );
+ return Stack(
+ children: [
+ Container(
+ width: double.infinity,
+ margin: const EdgeInsets.only(top: StyleString.safeSpace),
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(
+ color: isCurrentIndex
+ ? colorScheme.primaryContainer.withOpacity(0.6)
+ : colorScheme.onInverseSurface.withOpacity(0.6),
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: isCurrentIndex
+ ? colorScheme.primary.withOpacity(0.8)
+ : Colors.transparent,
+ width: 1,
+ ),
+ ),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(8),
+ onTap: () {
+ if (isCurrentIndex) {
+ return;
+ }
+ SmartDialog.showToast('切换至「${episode.title}」');
+ changeFucCall.call(episode, index);
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 12.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Text(
+ dataType == VideoEpidoesType.bangumiEpisode
+ ? '第${index + 1}话'
+ : '第${index + 1}p',
+ style: textStyle),
+ const SizedBox(height: 1),
+ Text(
+ episode.title,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: textStyle,
),
+ ],
+ ),
+ ),
+ ),
+ ),
+ if (dataType == VideoEpidoesType.bangumiEpisode &&
+ episode.badge != '' &&
+ episode.badge != null)
+ Positioned(
+ right: 8,
+ top: 18,
+ child: Text(
+ episode.badge,
+ style: const TextStyle(fontSize: 11, color: Color(0xFFFF6699)),
+ ),
+ )
+ ],
+ );
+ }
+}
+
+class UgcSeasonBuild extends StatelessWidget {
+ final UgcSeason ugcSeason;
+ final RxInt isSubscribe;
+ final bool isVisible;
+ final Function changeFucCall;
+ final Function changeVisible;
+
+ const UgcSeasonBuild({
+ Key? key,
+ required this.ugcSeason,
+ required this.isSubscribe,
+ required this.isVisible,
+ required this.changeFucCall,
+ required this.changeVisible,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final ThemeData theme = Theme.of(context);
+ final Color outline = theme.colorScheme.outline;
+ final Color surface = theme.colorScheme.surface;
+ final Color primary = theme.colorScheme.primary;
+ final Color onPrimary = theme.colorScheme.onPrimary;
+ final Color onInverseSurface = theme.colorScheme.onInverseSurface;
+ final TextStyle titleMedium = theme.textTheme.titleMedium!;
+ final TextStyle labelMedium = theme.textTheme.labelMedium!;
+ final Color dividerColor = theme.dividerColor.withOpacity(0.1);
+
+ return isVisible
+ ? Container(
+ padding: const EdgeInsets.fromLTRB(12, 0, 12, 0),
+ color: surface,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Divider(height: 1, thickness: 1, color: dividerColor),
+ const SizedBox(height: 10),
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ '合集:${ugcSeason.title}',
+ style: titleMedium,
+ maxLines: 2,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Obx(
+ () => isSubscribe.value == -1
+ ? const SizedBox(height: 32)
+ : SizedBox(
+ height: 32,
+ child: FilledButton.tonal(
+ onPressed: () => changeFucCall.call(),
+ style: TextButton.styleFrom(
+ padding:
+ const EdgeInsets.only(left: 8, right: 8),
+ foregroundColor: isSubscribe.value == 1
+ ? outline
+ : onPrimary,
+ backgroundColor: isSubscribe.value == 1
+ ? onInverseSurface
+ : primary,
+ ),
+ child:
+ Text(isSubscribe.value == 1 ? '已订阅' : '订阅'),
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (ugcSeason.intro != null && ugcSeason.intro != '') ...[
+ const SizedBox(height: 4),
+ Text(
+ ugcSeason.intro!,
+ style: TextStyle(color: outline, fontSize: 12),
+ ),
+ ],
+ const SizedBox(height: 4),
+ Text.rich(
+ TextSpan(
+ style: TextStyle(
+ fontSize: labelMedium.fontSize, color: outline),
+ children: [
+ TextSpan(
+ text: '${Utils.numFormat(ugcSeason.stat!.view)}播放'),
+ const TextSpan(text: ' · '),
+ TextSpan(
+ text:
+ '${Utils.numFormat(ugcSeason.stat!.danmaku)}弹幕'),
+ ],
+ ),
+ ),
+ const SizedBox(height: 14),
+ Align(
+ alignment: Alignment.center,
+ child: Material(
+ color: surface,
+ child: InkWell(
+ onTap: () => changeVisible.call(),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(
+ vertical: 10, horizontal: 0),
+ child: Text(
+ '收起简介',
+ style: TextStyle(color: primary, fontSize: 12),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Divider(height: 1, thickness: 1, color: dividerColor),
+ ],
+ ),
+ )
+ : Align(
+ alignment: Alignment.center,
+ child: InkWell(
+ onTap: () => changeVisible.call(),
+ child: Padding(
+ padding:
+ const EdgeInsets.symmetric(vertical: 10, horizontal: 0),
+ child: Text(
+ '展开简介',
+ style: TextStyle(color: primary, fontSize: 12),
),
),
),
- ],
- ),
- );
- });
- }
-
- /// 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/media_bangumi.dart b/lib/common/skeleton/media_bangumi.dart
index cf589254..98282cf0 100644
--- a/lib/common/skeleton/media_bangumi.dart
+++ b/lib/common/skeleton/media_bangumi.dart
@@ -3,14 +3,9 @@ import 'package:pilipala/common/constants.dart';
import 'skeleton.dart';
-class MediaBangumiSkeleton extends StatefulWidget {
+class MediaBangumiSkeleton extends StatelessWidget {
const MediaBangumiSkeleton({super.key});
- @override
- State createState() => _MediaBangumiSkeletonState();
-}
-
-class _MediaBangumiSkeletonState extends State {
@override
Widget build(BuildContext context) {
Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
@@ -35,25 +30,25 @@ class _MediaBangumiSkeletonState extends State {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
- color: Theme.of(context).colorScheme.onInverseSurface,
+ color: bgColor,
width: 200,
height: 20,
margin: const EdgeInsets.only(bottom: 15),
),
Container(
- color: Theme.of(context).colorScheme.onInverseSurface,
+ color: bgColor,
width: 150,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Container(
- color: Theme.of(context).colorScheme.onInverseSurface,
+ color: bgColor,
width: 150,
height: 13,
margin: const EdgeInsets.only(bottom: 5),
),
Container(
- color: Theme.of(context).colorScheme.onInverseSurface,
+ color: bgColor,
width: 150,
height: 13,
),
@@ -64,7 +59,7 @@ class _MediaBangumiSkeletonState extends State {
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(20)),
- color: Theme.of(context).colorScheme.onInverseSurface,
+ color: bgColor,
),
),
],
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/skeleton/user_list.dart b/lib/common/skeleton/user_list.dart
new file mode 100644
index 00000000..cd9d4eb3
--- /dev/null
+++ b/lib/common/skeleton/user_list.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+import '../constants.dart';
+
+class UserListSkeleton extends StatelessWidget {
+ const UserListSkeleton({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ Color bgColor = Theme.of(context).colorScheme.onInverseSurface;
+ return Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: StyleString.safeSpace, vertical: 7),
+ child: Row(
+ children: [
+ ClipOval(
+ child: Container(width: 42, height: 42, color: bgColor),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Container(color: bgColor, width: 60, height: 13),
+ const SizedBox(width: 10),
+ Container(color: bgColor, width: 40, height: 13),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Container(
+ color: bgColor,
+ width: 100,
+ height: 13,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ));
+ }
+}
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/custom_toast.dart b/lib/common/widgets/custom_toast.dart
index f732fd85..93695e0d 100644
--- a/lib/common/widgets/custom_toast.dart
+++ b/lib/common/widgets/custom_toast.dart
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
-Box setting = GStrorage.setting;
+Box setting = GStorage.setting;
class CustomToast extends StatelessWidget {
const CustomToast({super.key, required this.msg});
diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart
index bf58d78c..b2aa75ff 100644
--- a/lib/common/widgets/html_render.dart
+++ b/lib/common/widgets/html_render.dart
@@ -1,7 +1,9 @@
+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 'network_img_layer.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';
// ignore: must_be_immutable
class HtmlRender extends StatelessWidget {
@@ -22,6 +24,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 +60,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: imgList?[index] ?? imgUrl,
+ child: CachedNetworkImage(
+ fadeInDuration:
+ const Duration(milliseconds: 0),
+ imageUrl: imgList?[index] ?? 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 +114,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 +126,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..51396c0b 100644
--- a/lib/common/widgets/http_error.dart
+++ b/lib/common/widgets/http_error.dart
@@ -2,50 +2,54 @@ 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,
+ this.fn,
+ this.btnText,
+ this.isShowBtn = true,
+ this.isInSliver = true,
+ super.key,
+ });
final String? errMsg;
final Function()? fn;
final String? btnText;
+ final bool isShowBtn;
+ final bool isInSliver;
@override
Widget build(BuildContext context) {
- return SliverToBoxAdapter(
- child: SizedBox(
- height: 400,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.center,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SvgPicture.asset(
- "assets/images/error.svg",
- height: 200,
- ),
- const SizedBox(height: 30),
- Text(
- errMsg ?? '请求异常',
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.titleSmall,
- ),
- const SizedBox(height: 20),
+ Color primary = Theme.of(context).colorScheme.primary;
+ final errorContent = SizedBox(
+ height: 400,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset("assets/images/error.svg", height: 200),
+ const SizedBox(height: 30),
+ Text(
+ errMsg ?? '请求异常',
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.titleSmall,
+ ),
+ const SizedBox(height: 20),
+ if (isShowBtn)
FilledButton.tonal(
- onPressed: () {
- fn!();
- },
+ onPressed: () => fn?.call(),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.resolveWith((states) {
- return Theme.of(context).colorScheme.primary.withAlpha(20);
+ return primary.withAlpha(20);
}),
),
- child: Text(
- btnText ?? '点击重试',
- style: TextStyle(color: Theme.of(context).colorScheme.primary),
- ),
+ child: Text(btnText ?? '点击重试', style: TextStyle(color: primary)),
),
- ],
- ),
+ ],
),
);
+ if (isInSliver) {
+ return SliverToBoxAdapter(child: errorContent);
+ } else {
+ return Align(alignment: Alignment.topCenter, child: errorContent);
+ }
}
}
diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart
index 06c35974..b7b5de7e 100644
--- a/lib/common/widgets/network_img_layer.dart
+++ b/lib/common/widgets/network_img_layer.dart
@@ -2,11 +2,11 @@ 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';
-Box setting = GStrorage.setting;
+Box setting = GStorage.setting;
class NetworkImgLayer extends StatelessWidget {
const NetworkImgLayer({
@@ -20,6 +20,7 @@ class NetworkImgLayer extends StatelessWidget {
// 图片质量 默认1%
this.quality,
this.origAspectRatio,
+ this.radius,
});
final String? src;
@@ -30,13 +31,31 @@ class NetworkImgLayer extends StatelessWidget {
final Duration? fadeInDuration;
final int? quality;
final double? origAspectRatio;
+ final double? radius;
+
+ BorderRadius getBorderRadius(String? type, double? radius) {
+ return BorderRadius.circular(
+ radius ??
+ (type == 'avatar'
+ ? 50
+ : type == 'emote'
+ ? 0
+ : StyleString.imgRadius.x),
+ );
+ }
@override
Widget build(BuildContext context) {
- final int defaultImgQuality = GlobalData().imgQuality;
+ int defaultImgQuality = 10;
+ try {
+ defaultImgQuality = GlobalDataCache.imgQuality;
+ } catch (_) {}
+
+ 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();
@@ -66,13 +85,7 @@ class NetworkImgLayer extends StatelessWidget {
return src != '' && src != null
? ClipRRect(
clipBehavior: Clip.antiAlias,
- borderRadius: BorderRadius.circular(
- type == 'avatar'
- ? 50
- : type == 'emote'
- ? 0
- : StyleString.imgRadius.x,
- ),
+ borderRadius: getBorderRadius(type, radius),
child: CachedNetworkImage(
imageUrl: imageUrl,
width: width,
@@ -101,11 +114,7 @@ class NetworkImgLayer extends StatelessWidget {
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
- borderRadius: BorderRadius.circular(type == 'avatar'
- ? 50
- : type == 'emote'
- ? 0
- : StyleString.imgRadius.x),
+ borderRadius: getBorderRadius(type, radius),
),
child: type == 'bg'
? const SizedBox()
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 99059a9e..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,23 +21,24 @@ 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) {
@@ -43,102 +49,117 @@ class VideoCardH extends StatelessWidget {
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 {
- if (type == 'ketang') {
- SmartDialog.showToast('课堂视频暂不支持播放');
- 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());
- }
- },
- 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 (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 (videoItem.duration != 0)
+ PBadge(
+ text: Utils.timeFormat(videoItem.duration!),
+ right: 6.0,
+ bottom: 6.0,
+ type: 'gray',
+ ),
+ 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,
+ )
+ ],
+ ),
+ );
+ },
),
),
);
@@ -153,6 +174,7 @@ class VideoContent extends StatelessWidget {
final bool showView;
final bool showDanmaku;
final bool showPubdate;
+ final Function()? onPressedFn;
const VideoContent({
super.key,
@@ -162,6 +184,7 @@ class VideoContent extends StatelessWidget {
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
+ this.onPressedFn,
});
@override
@@ -172,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,
@@ -187,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(
@@ -243,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,
+ ),
+ )
+ ],
],
),
],
@@ -373,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..8cec3523 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,10 @@ class VideoStat extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
- StatView(
- theme: 'gray',
- view: videoItem.stat.view,
- ),
+ if (videoItem.stat.view != null) StatView(view: videoItem.stat.view),
const SizedBox(width: 8),
- StatDanMu(
- theme: 'gray',
- danmu: videoItem.stat.danmu,
- ),
+ if (videoItem.stat.danmu != null)
+ StatDanMu(danmu: videoItem.stat.danmu),
if (videoItem is RecVideoItemModel) ...[
crossAxisCount > 1 ? const Spacer() : const SizedBox(width: 8),
RichText(
@@ -358,99 +304,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 b6975c4b..5b2cdf58 100644
--- a/lib/http/api.dart
+++ b/lib/http/api.dart
@@ -104,7 +104,7 @@ class Api {
// 评论列表
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
- static const String replyList = '/x/v2/reply';
+ static const String replyList = '/x/v2/reply/main';
// 楼中楼
static const String replyReplyList = '/x/v2/reply/reply';
@@ -175,7 +175,7 @@ class Api {
static const String delHistory = '/x/v2/history/delete';
// 搜索历史记录
- static const String searchHistory = '/x/web-goblin/history/search';
+ static const String searchHistory = '/x/web-interface/history/search';
// 热搜
static const String hotSearchList =
@@ -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
@@ -301,10 +301,6 @@ class Api {
static const String bangumiList =
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1©right=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
- // 我的订阅
- static const String bangumiFollow =
- '/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';
-
// 黑名单
static const String blackLst = '/x/relation/blacks';
@@ -400,12 +396,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 +483,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';
@@ -485,7 +495,7 @@ class Api {
static const activateBuvidApi = '/x/internal/gaia-gateway/ExClimbWuzhi';
/// 获取字幕配置
- static const getSubtitleConfig = '/x/player/v2';
+ static const getSubtitleConfig = '/x/player/wbi/v2';
/// 我的订阅
static const userSubFolder = '/x/v3/fav/folder/collected/list';
@@ -511,4 +521,105 @@ class Api {
/// 取消订阅
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 userMessageSystemAPi =
+ '${HttpString.messageBaseUrl}/x/sys-msg/query_user_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';
+
+ /// 直播间记录
+ static const String liveRoomEntry =
+ '${HttpString.liveBaseUrl}/xlive/web-room/v1/index/roomEntryAction';
+
+ /// 用户信息
+ static const String accountInfo = '/x/member/web/account';
+
+ /// 更新用户信息
+ static const String updateAccountInfo = '/x/member/web/update';
+
+ /// 删除评论
+ static const String replyDel = '/x/v2/reply/del';
+
+ /// 图片上传
+ static const String uploadImage = '/x/dynamic/feed/draw/upload_bfs';
+
+ /// 更新追番状态
+ static const String updateBangumiStatus = '/pgc/web/follow/status/update';
+
+ /// 番剧点赞投币收藏状态
+ static const String bangumiActionStatus = '/pgc/season/episode/community';
+
+ /// @我的
+ static const String messageAtAPi = '/x/msgfeed/at?';
+
+ /// 订阅
+ static const String confirmSub = '/x/v3/fav/season/fav';
+
+ /// 订阅状态
+ static const String videoRelation = '/x/web-interface/archive/relation';
+
+ /// 获取空降区间
+ static const String getSkipSegments =
+ '${HttpString.sponsorBlockBaseUrl}/api/skipSegments';
+
+ /// 视频标签
+ static const String videoTag = '/x/tag/archive/tags';
}
diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart
index 91508682..d0c052d6 100644
--- a/lib/http/bangumi.dart
+++ b/lib/http/bangumi.dart
@@ -1,5 +1,8 @@
+import 'dart:convert';
import '../models/bangumi/list.dart';
import 'index.dart';
+import 'package:html/parser.dart' as html_parser;
+import 'package:html/dom.dart' as html_dom;
class BangumiHttp {
static Future bangumiList({int? page}) async {
@@ -18,8 +21,19 @@ class BangumiHttp {
}
}
- static Future bangumiFollow({int? mid}) async {
- var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});
+ static Future getRecentBangumi({
+ int? mid,
+ int type = 1,
+ int pn = 1,
+ int ps = 20,
+ }) async {
+ var res = await Request().get(Api.getRecentBangumiApi, data: {
+ 'vmid': mid,
+ 'type': type,
+ 'follow_status': 0,
+ 'pn': pn,
+ 'ps': ps,
+ });
if (res.data['code'] == 0) {
return {
'status': true,
@@ -33,4 +47,62 @@ class BangumiHttp {
};
}
}
+
+ // 获取追番状态
+ static Future bangumiStatus({required int seasonId}) async {
+ var res = await Request()
+ .get('https://www.bilibili.com/bangumi/play/ss$seasonId');
+ html_dom.Document document = html_parser.parse(res.data);
+ // 查找 id 为 __NEXT_DATA__ 的 script 元素
+ html_dom.Element? scriptElement =
+ document.querySelector('script#\\__NEXT_DATA__');
+ if (scriptElement != null) {
+ // 提取 script 元素的内容
+ String scriptContent = scriptElement.text;
+ final dynamic scriptContentJson = jsonDecode(scriptContent);
+ Map followState = scriptContentJson['props']['pageProps']['followState'];
+ return {
+ 'status': true,
+ 'data': {
+ 'isFollowed': followState['isFollowed'],
+ 'followStatus': followState['followStatus']
+ }
+ };
+ } else {
+ print('Script element with id "__NEXT_DATA__" not found.');
+ }
+ }
+
+ // 更新追番状态
+ static Future updateBangumiStatus({
+ required int seasonId,
+ required int status,
+ }) async {
+ var res = await Request().post(Api.updateBangumiStatus, data: {
+ 'season_id': seasonId,
+ 'status': status,
+ });
+ if (res.data['code'] == 0) {
+ return {'status': true, 'data': res.data['data']};
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': res.data['message'],
+ };
+ }
+ }
+
+ // 获取番剧点赞投币收藏状态
+ static Future bangumiActionStatus({required int epId}) async {
+ var res = await Request().get(
+ Api.bangumiActionStatus,
+ data: {'ep_id': epId},
+ );
+ 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/black.dart b/lib/http/black.dart
index 0c6a63ab..67356a92 100644
--- a/lib/http/black.dart
+++ b/lib/http/black.dart
@@ -28,7 +28,7 @@ class BlackHttp {
static Future removeBlack({required int fid}) async {
var res = await Request().post(
Api.removeBlack,
- queryParameters: {
+ data: {
'act': 6,
'csrf': await Request.getCsrf(),
'fid': fid,
diff --git a/lib/http/common.dart b/lib/http/common.dart
index d711a7e7..2f5f0e84 100644
--- a/lib/http/common.dart
+++ b/lib/http/common.dart
@@ -1,3 +1,5 @@
+import 'package:pilipala/models/sponsor_block/segment.dart';
+
import 'index.dart';
class CommonHttp {
@@ -14,4 +16,31 @@ class CommonHttp {
};
}
}
+
+ static Future querySkipSegments({required String bvid}) async {
+ var res = await Request().getWithoutCookie(Api.getSkipSegments, data: {
+ 'videoID': bvid,
+ });
+ if (res.data is List && res.data.isNotEmpty) {
+ try {
+ return {
+ 'status': true,
+ 'data': res.data
+ .map((e) => SegmentDataModel.fromJson(e))
+ .toList(),
+ };
+ } catch (err) {
+ return {
+ 'status': false,
+ 'data': [],
+ 'msg': 'sponsorBlock数据解析失败: $err',
+ };
+ }
+ } else {
+ return {
+ 'status': false,
+ 'data': [],
+ };
+ }
+ }
}
diff --git a/lib/http/constants.dart b/lib/http/constants.dart
index 3d749ee8..07d06958 100644
--- a/lib/http/constants.dart
+++ b/lib/http/constants.dart
@@ -5,6 +5,9 @@ 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 String sponsorBlockBaseUrl = 'https://www.bsbsb.top';
static const List validateStatusCodes = [
302,
304,
diff --git a/lib/http/danmaku.dart b/lib/http/danmaku.dart
index 0b108755..7b4283ae 100644
--- a/lib/http/danmaku.dart
+++ b/lib/http/danmaku.dart
@@ -17,7 +17,9 @@ class DanmakaHttp {
var response = await Request().get(
Api.webDanmaku,
data: params,
- extra: {'resType': ResponseType.bytes},
+ options: Options(
+ responseType: ResponseType.bytes,
+ ),
);
return DmSegMobileReply.fromBuffer(response.data);
}
@@ -67,9 +69,6 @@ class DanmakaHttp {
var response = await Request().post(
Api.shootDanmaku,
data: params,
- options: Options(
- contentType: Headers.formUrlEncodedContentType,
- ),
);
if (response.statusCode != 200) {
return {
diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart
index d62de12f..53ba6fc1 100644
--- a/lib/http/dynamics.dart
+++ b/lib/http/dynamics.dart
@@ -1,3 +1,5 @@
+import 'dart:math';
+import 'package:dio/dio.dart';
import '../models/dynamics/result.dart';
import '../models/dynamics/up.dart';
import 'index.dart';
@@ -40,6 +42,7 @@ class DynamicsHttp {
'status': false,
'data': [],
'msg': res.data['message'],
+ 'code': res.data['code'],
};
}
}
@@ -67,7 +70,7 @@ class DynamicsHttp {
}) async {
var res = await Request().post(
Api.likeDynamic,
- queryParameters: {
+ data: {
'dynamic_id': dynamicId,
'up': up,
'csrf': await Request.getCsrf(),
@@ -89,7 +92,7 @@ class DynamicsHttp {
//
static Future dynamicDetail({
- String? id,
+ required String id,
}) async {
var res = await Request().get(Api.dynamicDetail, data: {
'timezone_offset': -480,
@@ -117,4 +120,99 @@ 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
+ },
+ options: Options(contentType: 'application/json'),
+ );
+ 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..69577e7e
--- /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,
+ data: {
+ '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,
+ data: {
+ '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/html.dart b/lib/http/html.dart
index 100887e5..87adacb9 100644
--- a/lib/http/html.dart
+++ b/lib/http/html.dart
@@ -21,7 +21,6 @@ class HtmlHttp {
}
try {
Document rootTree = parse(response.data);
- // log(response.data.body.toString());
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
@@ -52,7 +51,6 @@ class HtmlHttp {
.className
.split(' ')[1]
.split('-')[2];
- // List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
@@ -76,20 +74,10 @@ class HtmlHttp {
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!;
- // 头像
- // String avatar =
- // authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
- // print(avatar);
- // avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情
Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间
- // String updateTime =
- // opusDetail.querySelector('.opus-module-author__pub__text')!.text;
- // print(updateTime);
-
- //
String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+');
diff --git a/lib/http/init.dart b/lib/http/init.dart
index a0b36369..8a11034c 100644
--- a/lib/http/init.dart
+++ b/lib/http/init.dart
@@ -8,8 +8,8 @@ import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
-// import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:hive/hive.dart';
+import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../utils/storage.dart';
import '../utils/utils.dart';
@@ -22,16 +22,19 @@ class Request {
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
- Box setting = GStrorage.setting;
- static Box localCache = GStrorage.localCache;
+ Box setting = GStorage.setting;
+ static Box localCache = GStorage.localCache;
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 userInfoCache = GStorage.userInfo;
+ Box setting = GStorage.setting;
final String cookiePath = await Utils.getCookiePath();
final PersistCookieJar cookieJar = PersistCookieJar(
ignoreExpires: true,
@@ -41,7 +44,7 @@ class Request {
dio.interceptors.add(cookieManager);
final List cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
- final userInfo = userInfoCache.get('userInfoCache');
+ final UserInfoData? userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
final List cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
@@ -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'));
}
/*
@@ -142,15 +171,6 @@ class Request {
dio = Dio(options);
- /// fix 第三方登录 302重定向 跟iOS代理问题冲突
- // ..httpClientAdapter = Http2Adapter(
- // ConnectionManager(
- // idleTimeout: const Duration(milliseconds: 10000),
- // onClientCreate: (_, ClientSetting config) =>
- // config.onBadCertificate = (_) => true,
- // ),
- // );
-
/// 设置代理
if (enableSystemProxy) {
dio.httpClientAdapter = IOHttpClientAdapter(
@@ -188,18 +208,15 @@ class Request {
/*
* get请求
*/
- get(url, {data, options, cancelToken, extra}) async {
+ get(url, {data, Options? options, cancelToken, extra}) async {
Response response;
- final Options options = Options();
- ResponseType resType = ResponseType.json;
if (extra != null) {
- resType = extra!['resType'] ?? ResponseType.json;
if (extra['ua'] != null) {
- options.headers = {'user-agent': headerUa(type: extra['ua'])};
+ options ??= Options();
+ options.headers ??= {};
+ options.headers?['user-agent'] = headerUa(type: extra['ua']);
}
}
- options.responseType = resType;
-
try {
response = await dio.get(
url,
@@ -209,32 +226,44 @@ class Request {
);
return response;
} on DioException catch (e) {
- Response errResponse = Response(
- data: {
- 'message': await ApiInterceptor.dioError(e)
- }, // 将自定义 Map 数据赋值给 Response 的 data 属性
+ return Response(
+ data: {'message': await ApiInterceptor.dioError(e)},
statusCode: 200,
requestOptions: RequestOptions(),
);
- return errResponse;
}
}
+ /*
+ * get请求
+ */
+ getWithoutCookie(url, {data}) {
+ return get(
+ url,
+ data: data,
+ options: Options(
+ headers: {
+ 'cookie': 'buvid3= ; b_nut= ; sid= ',
+ 'user-agent': headerUa(type: 'pc'),
+ },
+ ),
+ );
+ }
+
/*
* post请求
*/
post(url, {data, queryParameters, options, cancelToken, extra}) async {
- // print('post-data: $data');
Response response;
try {
response = await dio.post(
url,
data: data,
queryParameters: queryParameters,
- options: options,
+ options:
+ options ?? Options(contentType: Headers.formUrlEncodedContentType),
cancelToken: cancelToken,
);
- // print('post success: ${response.data}');
return response;
} on DioException catch (e) {
Response errResponse = Response(
@@ -290,8 +319,21 @@ class Request {
}
} else {
headerUa =
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36';
}
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 a5359283..b33d18df 100644
--- a/lib/http/interceptor.dart
+++ b/lib/http/interceptor.dart
@@ -3,8 +3,7 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
-import 'package:hive/hive.dart';
-import '../utils/storage.dart';
+import 'package:pilipala/utils/login.dart';
class ApiInterceptor extends Interceptor {
@override
@@ -19,20 +18,9 @@ class ApiInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
- if (response.statusCode == 302) {
- final List locations = response.headers['location']!;
- if (locations.isNotEmpty) {
- if (locations.first.startsWith('https://www.mcbbs.net')) {
- final Uri uri = Uri.parse(locations.first);
- final String? accessKey = uri.queryParameters['access_key'];
- final String? mid = uri.queryParameters['mid'];
- try {
- Box localCache = GStrorage.localCache;
- localCache.put(LocalCacheKey.accessKey,
- {'mid': mid, 'value': accessKey});
- } catch (_) {}
- }
- }
+ // 在响应之后处理数据
+ if (response.data is Map && response.data['code'] == -101) {
+ LoginUtils.loginOut();
}
} catch (err) {
print('ApiInterceptor: $err');
@@ -46,7 +34,8 @@ class ApiInterceptor extends Interceptor {
// 处理网络请求错误
// handler.next(err);
String url = err.requestOptions.uri.toString();
- if (!url.contains('heartBeat')) {
+ final excludedPatterns = RegExp(r'heartbeat|seg\.so|online/total');
+ if (!excludedPatterns.hasMatch(url)) {
SmartDialog.showToast(
await dioError(err),
displayType: SmartToastType.onlyRefresh,
diff --git a/lib/http/live.dart b/lib/http/live.dart
index e624120e..259f86fc 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,96 @@ 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,
+ data: {
+ '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'],
+ };
+ }
+ }
+
+ // 直播历史记录
+ static Future liveRoomEntry({required int roomId}) async {
+ await Request().post(
+ Api.liveRoomEntry,
+ data: {
+ 'room_id': roomId,
+ 'platform': 'pc',
+ 'csrf_token': await Request.getCsrf(),
+ 'csrf': await Request.getCsrf(),
+ 'visit_id': '',
+ },
+ );
+ }
}
diff --git a/lib/http/login.dart b/lib/http/login.dart
index ff3fee23..80f58803 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,50 @@ 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,
+ );
+ 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 {}
@@ -114,9 +149,6 @@ class LoginHttp {
var res = await Request().post(
Api.appSmsCode,
data: data,
- options: Options(
- contentType: Headers.formUrlEncodedContentType,
- ),
);
print(res);
}
@@ -167,10 +199,82 @@ class LoginHttp {
var res = await Request().post(
Api.loginInByPwdApi,
data: data,
- options: Options(
- contentType: Headers.formUrlEncodedContentType,
- ),
);
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,
+ );
+ if (res.data['code'] == 0) {
+ if (res.data['data']['status'] == 0) {
+ return {
+ 'status': true,
+ 'data': res.data['data'],
+ };
+ } else {
+ return {
+ 'status': false,
+ 'code': 1,
+ 'data': res.data['data'],
+ 'msg': res.data['data']['message'],
+ };
+ }
+ } 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..107a9379 100644
--- a/lib/http/member.dart
+++ b/lib/http/member.dart
@@ -1,5 +1,11 @@
+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 'package:pilipala/models/user/info.dart';
+import 'package:pilipala/utils/global_data_cache.dart';
import '../common/constants.dart';
import '../models/dynamics/result.dart';
import '../models/follow/result.dart';
@@ -15,14 +21,20 @@ import 'index.dart';
class MemberHttp {
static Future memberInfo({
- int? mid,
+ required int mid,
String token = '',
}) async {
+ String? wWebid;
+ if ((await getWWebid(mid: mid))['status']) {
+ wWebid = GlobalDataCache.wWebid;
+ }
+
Map params = await WbiSign().makSign({
'mid': mid,
'token': token,
'platform': 'web',
'web_location': 1550101,
+ ...wWebid != null ? {'w_webid': wWebid} : {},
});
var res = await Request().get(
Api.memberInfo,
@@ -95,7 +107,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,
@@ -187,13 +206,15 @@ class MemberHttp {
// 设置分组
static Future addUsers(int? fids, String? tagids) async {
- var res = await Request().post(Api.addUsers, queryParameters: {
- 'fids': fids,
- 'tagids': tagids ?? '0',
- 'csrf': await Request.getCsrf(),
- }, data: {
- 'cross_domain': true
- });
+ var res = await Request().post(
+ Api.addUsers,
+ data: {
+ 'fids': fids,
+ 'tagids': tagids ?? '0',
+ 'csrf': await Request.getCsrf(),
+ },
+ queryParameters: {'cross_domain': true},
+ );
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
@@ -328,7 +349,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 {
@@ -409,11 +432,14 @@ class MemberHttp {
static Future cookieToKey() async {
var authCodeRes = await getTVCode();
if (authCodeRes['status']) {
- var res = await Request().post(Api.cookieToKey, queryParameters: {
- 'auth_code': authCodeRes['data'],
- 'build': 708200,
- 'csrf': await Request.getCsrf(),
- });
+ var res = await Request().post(
+ Api.cookieToKey,
+ data: {
+ 'auth_code': authCodeRes['data'],
+ 'build': 708200,
+ 'csrf': await Request.getCsrf(),
+ },
+ );
await Future.delayed(const Duration(milliseconds: 300));
await qrcodePoll(authCodeRes['data']);
if (res.data['code'] == 0) {
@@ -445,11 +471,11 @@ class MemberHttp {
SmartDialog.dismiss();
if (res.data['code'] == 0) {
String accessKey = res.data['data']['access_token'];
- Box localCache = GStrorage.localCache;
- Box userInfoCache = GStrorage.userInfo;
- var userInfo = userInfoCache.get('userInfoCache');
+ Box localCache = GStorage.localCache;
+ Box userInfoCache = GStorage.userInfo;
+ final UserInfoData? userInfo = userInfoCache.get('userInfoCache');
localCache.put(
- LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey});
+ LocalCacheKey.accessKey, {'mid': userInfo!.mid, 'value': accessKey});
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
@@ -510,4 +536,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 {
+ String? wWebid = GlobalDataCache.wWebid;
+ if (wWebid != null) {
+ return {'status': true, 'data': wWebid};
+ }
+ 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);
+ GlobalDataCache.wWebid = map['access_id'];
+ 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,
+ String? offset,
+ }) async {
+ String? wWebid;
+ if ((await getWWebid(mid: mid))['status']) {
+ wWebid = GlobalDataCache.wWebid;
+ }
+ Map params = await WbiSign().makSign({
+ 'host_mid': mid,
+ 'page': pn,
+ 'offset': offset,
+ 'web_location': 333.999,
+ ...wWebid != null ? {'w_webid': wWebid} : {},
+ });
+ var res = await Request().get(Api.opusList, data: params);
+ 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 d1d31958..65156e03 100644
--- a/lib/http/msg.dart
+++ b/lib/http/msg.dart
@@ -1,4 +1,10 @@
+import 'dart:convert';
import 'dart:math';
+import 'package:flutter/material.dart';
+import 'package:pilipala/models/msg/at.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';
@@ -59,7 +65,7 @@ class MsgHttp {
.toList(),
};
} catch (err) {
- print('err🔟: $err');
+ debugPrint('err: $err');
}
} else {
return {
@@ -122,68 +128,45 @@ 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,
+ },
+ );
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 +203,172 @@ 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, 'data': [], 'msg': err.toString()};
+ }
+ } else {
+ return {'status': false, 'data': [], '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'],
+ };
+ }
+ }
+
+ static Future messageSystemAccount() async {
+ var res = await Request().get(Api.userMessageSystemAPi, 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 messageAt() async {
+ var res = await Request().get(Api.messageAtAPi, data: {
+ 'build': 0,
+ 'mobi_app': 'web',
+ });
+ if (res.data['code'] == 0) {
+ try {
+ return {
+ 'status': true,
+ 'data': MessageAtModel.fromJson(res.data['data']),
+ };
+ } catch (err) {
+ return {'status': false, 'data': [], 'msg': err.toString()};
+ }
+ } else {
+ return {'status': false, 'data': [], 'msg': res.data['message']};
+ }
+ }
}
diff --git a/lib/http/read.dart b/lib/http/read.dart
new file mode 100644
index 00000000..f2542936
--- /dev/null
+++ b/lib/http/read.dart
@@ -0,0 +1,122 @@
+import 'dart:convert';
+import 'package:dio/dio.dart';
+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'