Compare commits
305 Commits
v1.0.0.081
...
v1.0.10.10
Author | SHA1 | Date | |
---|---|---|---|
45bd4fc6d5 | |||
789d95e728 | |||
86c87dc1d5 | |||
3d6c270070 | |||
f214c45448 | |||
5f26e19c62 | |||
960104929f | |||
f25dab2eb8 | |||
15947e45da | |||
6c983cf849 | |||
4d5f3eb14a | |||
94aef39f7b | |||
424bdd9fff | |||
c77c8e683d | |||
ad1ced51f2 | |||
c0c1a3a59a | |||
690b168a45 | |||
7f7919d585 | |||
76974bd874 | |||
f8173b0b5f | |||
7ecfbac786 | |||
c794eb465c | |||
7adbf76362 | |||
353287e053 | |||
856d699fd7 | |||
15914e5961 | |||
0e5b1633be | |||
24f22f8afa | |||
7c38340fc6 | |||
79f661e5da | |||
3ba90d6c85 | |||
85e86f1d61 | |||
ec58d060bf | |||
7576f39010 | |||
77f47b8242 | |||
4b3e791370 | |||
f25f5c28d9 | |||
7ca367869b | |||
7222ca4425 | |||
692d596818 | |||
47e3cf46e4 | |||
6b2229dddc | |||
1e202979d3 | |||
82ad1662aa | |||
7feda8d187 | |||
d83b4bc59e | |||
1d1d4f8c7d | |||
1061ffca3d | |||
52ee5b36be | |||
87807466ff | |||
227cfb637e | |||
2c4ee083ef | |||
2ef3a8cd25 | |||
3a19b089c5 | |||
21e6d1aa52 | |||
10965fae73 | |||
8f987e8352 | |||
4e147b6f18 | |||
6d982bdba2 | |||
3edce0c4ec | |||
2eb7b388e1 | |||
2fd23aa20d | |||
2ecd1d3dab | |||
26d8ab5b43 | |||
8fa59f8f58 | |||
6ea4626288 | |||
329f158155 | |||
d6b6df3eed | |||
3f50aab12d | |||
227da31857 | |||
7ad6b25abe | |||
f79e4765c2 | |||
e8671dee6b | |||
97268c36dc | |||
d1272efad4 | |||
ba815bccda | |||
75fb81b959 | |||
a48d15ee73 | |||
620d7214df | |||
7458c33173 | |||
97fa047c60 | |||
9f4b928257 | |||
afcc5a9a02 | |||
7181db66bd | |||
820a1e9162 | |||
566f75f760 | |||
4c49f466db | |||
8f97431665 | |||
f543be562a | |||
93383a5c65 | |||
fd57ebc4cc | |||
9de9b885bc | |||
c2db6a50f0 | |||
427bd2eb79 | |||
8c01de47e4 | |||
83b27e7231 | |||
fc767fab97 | |||
7db9d290f5 | |||
bb66de29d4 | |||
dd97636494 | |||
95f5ac6a71 | |||
7fa7152245 | |||
41df90561b | |||
5c68772f7b | |||
262f244a98 | |||
c91cfedfe2 | |||
1d9372b4f1 | |||
e9095932ed | |||
3daa06a198 | |||
380ada9ae0 | |||
76bd5550c7 | |||
54c66d54da | |||
481d5e77d7 | |||
33413cdb51 | |||
277c7a25cb | |||
2c9b3e8854 | |||
fff54a55a1 | |||
838467451b | |||
8803fbd777 | |||
7c9b5bb891 | |||
7867af0f85 | |||
6d2e0f2049 | |||
74ec4cccea | |||
bd568c4945 | |||
097ab4310a | |||
09ff01905e | |||
ef38844798 | |||
1922a91575 | |||
3c17d18acf | |||
4cf2fc3c23 | |||
b9a47da92b | |||
c16106d676 | |||
0e39453558 | |||
8ff4259972 | |||
5082dc6d59 | |||
627df8e6ad | |||
2467fd0dea | |||
c6f6af4628 | |||
9e907f9151 | |||
22e17d437b | |||
8a06ce65a5 | |||
72ff3fdab0 | |||
517ca032d2 | |||
396f9fbbac | |||
c0332c74d7 | |||
2669b41ede | |||
81dace96d7 | |||
d693d7ad6c | |||
8c02a566f6 | |||
a2420d0bef | |||
29d3f78da9 | |||
a864bea3f4 | |||
070156da86 | |||
69f846760d | |||
2ca79003bf | |||
18af065a1e | |||
0ad54d8c0b | |||
0dfcd4ed40 | |||
5b953ae0be | |||
392980f0e8 | |||
4c938ed8aa | |||
7f961e998c | |||
e6b307ddd7 | |||
f5b4ad33c6 | |||
a2d4613293 | |||
1bebb32a0d | |||
217b036ee3 | |||
fa95ae0cce | |||
977bac84c3 | |||
0f134b8dca | |||
daec283bdf | |||
6f84eefbe4 | |||
4a7f2f027f | |||
a39f81ac2a | |||
0cb580ba8e | |||
c7187f2456 | |||
cd38c0799d | |||
aa63007c8a | |||
b9b1ac7ec5 | |||
4036262bed | |||
e35f39e353 | |||
ff47aff7cf | |||
72b1116b57 | |||
a0d71491be | |||
b000a400c4 | |||
be11598753 | |||
2d122374f6 | |||
c88b89110b | |||
9b1bb8c566 | |||
01df8622e0 | |||
ab8c88c696 | |||
08e4e64764 | |||
6f2ffcf2a1 | |||
3c1fa82010 | |||
4f6dd68954 | |||
f06507925f | |||
a49c400a8e | |||
1a60f74863 | |||
a304df2670 | |||
1e1e2e5a77 | |||
e1c69ac550 | |||
f6c7143d2d | |||
b485399517 | |||
108c37f0d7 | |||
fceb55aaa3 | |||
52e44fb95b | |||
dfbe3b1f6c | |||
e90a9f0323 | |||
184088f96d | |||
73e8972a76 | |||
c15646867c | |||
f7e9fbaea7 | |||
5f730af347 | |||
ce1daec3c5 | |||
67a7113bd7 | |||
4e2808fab7 | |||
a1900c7362 | |||
e8cd0d5330 | |||
9528a6f462 | |||
b9e78bf2ec | |||
d8abfde52d | |||
1d0b91f80b | |||
2dd967da4d | |||
53d4379bb9 | |||
a928c575ef | |||
a0e51c86fc | |||
d670b8123a | |||
2621b096ac | |||
05631f7803 | |||
495ba57ca8 | |||
8990c4ae92 | |||
8bc6a32b06 | |||
6083578f93 | |||
aa7419f352 | |||
c90a6cd86c | |||
2e04c27292 | |||
5741f80536 | |||
161ba1c313 | |||
5d9dc6c1a9 | |||
1abe70d4d4 | |||
5fc959eb59 | |||
6322b29aef | |||
6461f72b5e | |||
e3d561bffd | |||
535cf69967 | |||
4314b0fc3c | |||
9da113726b | |||
201422c150 | |||
b67127123a | |||
3ce7578183 | |||
7b60cc2666 | |||
ead24e90fb | |||
3ad3ca9d48 | |||
8a8e99f30b | |||
0fe6d6c8e2 | |||
5a03bee410 | |||
cafde6cc04 | |||
b6023e35bc | |||
6a1c89f885 | |||
b7c0ef8341 | |||
9e44995082 | |||
8703d9f576 | |||
5812b5cff1 | |||
1884801ed2 | |||
a19ab8d17f | |||
a4078c0a8e | |||
90e811489b | |||
706bb0f924 | |||
01d6308350 | |||
332d5dc38c | |||
8f84c6a6f9 | |||
cc8753e8de | |||
8c8ddc9d93 | |||
5f03244085 | |||
50a5653516 | |||
7bb7159d48 | |||
83341cd62b | |||
8627869309 | |||
6bbbdd7710 | |||
599d6983fc | |||
b7eed8578a | |||
ec9d9739fe | |||
740116e873 | |||
c8810b458f | |||
2b0dc9d285 | |||
7c2518bcd2 | |||
9db76e99db | |||
40849cb68d | |||
b435023c99 | |||
b55568ef2a | |||
47a3c964c0 | |||
85ff7c7a92 | |||
083d05ed7e | |||
08862e7f72 | |||
04d953a3a2 | |||
be1cc25d12 | |||
adff2f2828 | |||
592b32fc7a | |||
d9a758464c | |||
0c067f7ca6 | |||
b6aa144205 | |||
438a94e298 | |||
b800f1b83b | |||
ddceda1184 | |||
0003c057cd |
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug 反馈
|
||||
about: 描述你所遇到的bug
|
||||
title: ''
|
||||
labels: 问题反馈
|
||||
assignees: guozhigq
|
||||
|
||||
---
|
||||
|
||||
### 问题描述
|
||||
请提供一个清晰而简明的问题描述。
|
||||
|
||||
### 复现步骤
|
||||
请提供复现该问题所需的具体步骤。
|
||||
|
||||
### 预期行为
|
||||
请描述你期望的正确行为或结果。
|
||||
|
||||
### 系统信息
|
||||
请提供关于您的环境的详细信息,包括操作系统、浏览器版本等。
|
||||
|
||||
### 相关截图或日志
|
||||
如果有的话,请提供相关的截图、错误日志或其他有助于解决问题的信息。
|
20
.github/ISSUE_TEMPLATE/功能请求.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/功能请求.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 对于功能的一些建议
|
||||
title: ''
|
||||
labels: 功能
|
||||
assignees: guozhigq
|
||||
|
||||
---
|
||||
|
||||
### 功能描述
|
||||
请提供对所请求功能的清晰描述。
|
||||
|
||||
### 目标
|
||||
请描述你希望通过这个功能实现的目标。
|
||||
|
||||
### 解决方案
|
||||
如果你有任何关于如何实现这个功能的想法或建议,请在这里提供。
|
||||
|
||||
### 其他
|
||||
请提供已实现该功能或类似功能的应用
|
10
.github/workflows/main.yml
vendored
10
.github/workflows/main.yml
vendored
@ -59,16 +59,16 @@ jobs:
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/v}" >>$GITHUB_OUTPUT
|
||||
|
||||
- name: 获取当前日期
|
||||
id: date
|
||||
run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
|
||||
# - name: 获取当前日期
|
||||
# id: date
|
||||
# run: echo "date=$(date +'%m%d')" >>$GITHUB_OUTPUT
|
||||
|
||||
- name: 重命名应用 Pili-arm64-v8a-*.*.*.0101.apk
|
||||
run: |
|
||||
DATE=${{ steps.date.outputs.date }}
|
||||
# DATE=${{ steps.date.outputs.date }}
|
||||
for file in build/app/outputs/flutter-apk/app-*-release.apk; do
|
||||
if [[ $file =~ app-(.*)-release.apk ]]; then
|
||||
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}($DATE).apk"
|
||||
new_file_name="build/app/outputs/flutter-apk/Pili-${BASH_REMATCH[1]}-${{ steps.version.outputs.version }}.apk"
|
||||
mv "$file" "$new_file_name"
|
||||
fi
|
||||
done
|
||||
|
24
README.md
24
README.md
@ -11,10 +11,11 @@
|
||||
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/174shots_so.png" width="32%" alt="home" />
|
||||
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/850shots_so.png" width="32%" alt="home" />
|
||||
<br/>
|
||||
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/main_screen.png" width="96%" alt="home" />
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
### 开发环境
|
||||
## 开发环境
|
||||
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||
|
||||
```bash
|
||||
@ -30,7 +31,15 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||
|
||||
<br/>
|
||||
|
||||
### 功能
|
||||
|
||||
## 技术交流
|
||||
|
||||
Telegram: https://t.me/+lm_oOVmF0RJiODk1
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
## 功能
|
||||
|
||||
目前着重移动端(Android、iOS),暂时没有适配桌面端、Pad端、手表端等
|
||||
|
||||
@ -74,12 +83,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||
- [ ] 弹幕
|
||||
- [ ] 字幕
|
||||
- [x] 记忆播放
|
||||
- [x] 视频比例:高度/宽度适应、填充、包含等
|
||||
|
||||
- [x] 搜索相关
|
||||
- [x] 热搜
|
||||
- [x] 搜索历史
|
||||
- [x] 默认搜索词
|
||||
- [x] 投稿、番剧、直播间、用户搜索
|
||||
- [x] 视频搜索排序、按时长筛选
|
||||
|
||||
- [x] 视频详情页相关
|
||||
- [x] 视频选集(分p)切换
|
||||
@ -96,26 +107,29 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||
- [x] 图片质量设定
|
||||
- [x] 主题模式:亮色/暗色/跟随系统
|
||||
- [x] 震动反馈(可选)
|
||||
- [x] 高帧率
|
||||
- [x] 自动全屏
|
||||
- [ ] 等等
|
||||
|
||||
<br/>
|
||||
|
||||
### 下载
|
||||
## 下载
|
||||
|
||||
可以通过右侧release进行下载或拉取代码到本地进行编译
|
||||
|
||||
<br/>
|
||||
|
||||
### 声明
|
||||
## 声明
|
||||
|
||||
此项目(PiliPala)是个人为了兴趣而开发, 仅用于学习和测试。
|
||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
||||
|
||||
感谢使用
|
||||
|
||||
|
||||
<br/>
|
||||
|
||||
### 致谢
|
||||
## 致谢
|
||||
|
||||
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
|
||||
- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)
|
||||
|
@ -20,7 +20,22 @@
|
||||
"android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
<application
|
||||
|
||||
<queries>
|
||||
<!-- If your app checks for http support -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:label="PiliPala"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@ -48,6 +63,164 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bilibili" android:host="forward" />
|
||||
<data android:scheme="bilibili" android:host="comment"
|
||||
android:pathPattern="/detail/.*/.*/.*" />
|
||||
<data android:scheme="bilibili" android:host="uper" />
|
||||
<data android:scheme="bilibili" android:host="article"
|
||||
android:pathPattern="/readlist" />
|
||||
<data android:scheme="bilibili" android:host="advertise" android:path="/home" />
|
||||
<data android:scheme="bilibili" android:host="clip" />
|
||||
<data android:scheme="bilibili" android:host="search" />
|
||||
<data android:scheme="bilibili" android:host="stardust-search" />
|
||||
<data android:scheme="bilibili" android:host="music" />
|
||||
<data android:scheme="bilibili" android:host="bangumi"
|
||||
android:pathPattern="/season.*" />
|
||||
<data android:scheme="bilibili" android:host="bangumi" android:pathPattern="/.*" />
|
||||
<data android:scheme="bilibili" android:host="pictureshow"
|
||||
android:pathPrefix="/creative_center" />
|
||||
<data android:scheme="bilibili" android:host="cliparea" />
|
||||
<data android:scheme="bilibili" android:host="im" />
|
||||
<data android:scheme="bilibili" android:host="im" android:path="/notifications" />
|
||||
<data android:scheme="bilibili" android:host="following" />
|
||||
<data android:scheme="bilibili" android:host="following"
|
||||
android:pathPattern="/detail/.*" />
|
||||
<data android:scheme="bilibili" android:host="following"
|
||||
android:path="/publishInfo/" />
|
||||
<data android:scheme="bilibili" android:host="laser" android:pathPattern="/.*" />
|
||||
<data android:scheme="bilibili" android:host="livearea" />
|
||||
<data android:scheme="bilibili" android:host="live" />
|
||||
<data android:scheme="bilibili" android:host="catalog" />
|
||||
<data android:scheme="bilibili" android:host="browser" />
|
||||
<data android:scheme="bilibili" android:host="user_center" />
|
||||
<data android:scheme="bilibili" android:host="login" />
|
||||
<data android:scheme="bilibili" android:host="space" />
|
||||
<data android:scheme="bilibili" android:host="author" />
|
||||
<data android:scheme="bilibili" android:host="tag" />
|
||||
<data android:scheme="bilibili" android:host="rank" />
|
||||
<data android:scheme="bilibili" android:host="external" />
|
||||
<data android:scheme="bilibili" android:host="blank" />
|
||||
<data android:scheme="bilibili" android:host="home" />
|
||||
<data android:scheme="bilibili" android:host="root" />
|
||||
<data android:scheme="bilibili" android:host="video" />
|
||||
<data android:scheme="bilibili" android:host="story" />
|
||||
<data android:scheme="bilibili" android:host="podcast" />
|
||||
<data android:scheme="bilibili" android:host="search" />
|
||||
<data android:scheme="bilibili" android:host="main" android:path="/favorite" />
|
||||
<data android:scheme="bilibili" android:host="pgc" android:path="/theater/match" />
|
||||
<data android:scheme="bilibili" android:host="pgc" android:path="/theater/square" />
|
||||
<data android:scheme="bilibili" android:host="m.bilibili.com"
|
||||
android:path="/topic-detail" />
|
||||
<data android:scheme="bilibili" android:host="article" />
|
||||
<data android:scheme="bilibili" android:host="pegasus"
|
||||
android:pathPattern="/channel/v2/.*" />
|
||||
<data android:scheme="bilibili" android:host="feed" android:pathPattern="/channel" />
|
||||
<data android:scheme="bilibili" android:host="vip" />
|
||||
<data android:scheme="bilibili" android:host="user_center" android:path="/vip" />
|
||||
<data android:scheme="bilibili" android:host="history" />
|
||||
<data android:scheme="bilibili" android:host="charge" android:path="/rank" />
|
||||
<data android:scheme="bilibili" android:host="assistant" />
|
||||
<data android:scheme="bilibili" android:host="assistant" />
|
||||
<data android:scheme="bilibili" android:host="feedback" />
|
||||
<data android:scheme="bilibili" android:host="auth" android:path="/launch" />
|
||||
|
||||
<data android:scheme="http" android:host="live.bilibili.com"
|
||||
android:pathPattern="/live/.*" />
|
||||
<data android:scheme="https" android:host="live.bilibili.com"
|
||||
android:pathPattern="/live/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.tv"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.tv"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.cn"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.cn"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/mobile/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/mobile/video/.*" />
|
||||
<data android:scheme="http" android:host="m.bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="m.bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/story/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/story/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/bangumi/i/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/bangumi/i/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/mobile/bangumi/i/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/mobile/bangumi/i/.*" />
|
||||
<data android:scheme="http" android:host="bangumi.bilibili.com"
|
||||
android:pathPattern="/.*" />
|
||||
<data android:scheme="https" android:host="bangumi.bilibili.com"
|
||||
android:pathPattern="/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/bangumi/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/bangumi/.*" />
|
||||
<data android:scheme="http" android:host="m.bilibili.com"
|
||||
android:pathPattern="/bangumi/.*" />
|
||||
<data android:scheme="https" android:host="m.bilibili.com"
|
||||
android:pathPattern="/bangumi/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ss.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ss.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ep.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ep.*" />
|
||||
<data android:scheme="http" android:host="m.bilibili.com"
|
||||
android:pathPattern="/bangumi/play/ss.*" />
|
||||
<data android:scheme="https" android:host="m.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ss.*" />
|
||||
<data android:scheme="http" android:host="m.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ep.*" />
|
||||
<data android:scheme="https" android:host="m.bilibili.com"
|
||||
android:pathPattern="/cheese/play/ep.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/read/cv.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/read/cv.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com" android:path="/review/" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" />
|
||||
<data android:scheme="http" android:host="bilibili.cn"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="bilibili.cn"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.cn"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.cn"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/video/.*" />
|
||||
<data android:scheme="http" android:host="www.bilibili.com"
|
||||
android:pathPattern="/mobile/video/.*" />
|
||||
<data android:scheme="https" android:host="www.bilibili.com"
|
||||
android:pathPattern="/mobile/video/.*" />
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
@ -56,7 +229,6 @@
|
||||
android:value="2" />
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
@ -14,5 +14,6 @@
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
assets/images/error.svg
Normal file
1
assets/images/error.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 26 KiB |
BIN
assets/sreenshot/main_screen.png
Normal file
BIN
assets/sreenshot/main_screen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
7
change_log/1.0.1.0817.md
Normal file
7
change_log/1.0.1.0817.md
Normal file
@ -0,0 +1,7 @@
|
||||
## 1.0.1
|
||||
|
||||
### 修复
|
||||
+ 升级播放器依赖
|
||||
+ android平台 AV1格式视频支持
|
||||
+ 视频全屏功能
|
||||
|
4
change_log/1.0.10.1016.md
Normal file
4
change_log/1.0.10.1016.md
Normal file
@ -0,0 +1,4 @@
|
||||
## 1.0.9
|
||||
|
||||
### 修复
|
||||
+ 长按倍速抬起后未恢复默认倍速
|
19
change_log/1.0.2.0819.md
Normal file
19
change_log/1.0.2.0819.md
Normal file
@ -0,0 +1,19 @@
|
||||
## 1.0.2
|
||||
|
||||
### 新功能
|
||||
+ 自动检查更新
|
||||
+ 封面图片保存
|
||||
+ 动态跳转番剧
|
||||
+ 历史记录番剧记忆播放
|
||||
+ 一键清空稍后再看
|
||||
|
||||
### 修复
|
||||
+ 切换分P cid未切换
|
||||
+ cookie存储问题
|
||||
+ 登录/退出登录问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 退出登录提示
|
||||
+ 请求节流
|
||||
+ 全屏播放
|
19
change_log/1.0.3.0821.md
Normal file
19
change_log/1.0.3.0821.md
Normal file
@ -0,0 +1,19 @@
|
||||
## 1.0.3
|
||||
|
||||
建议卸载1.0.2版本,重新安装
|
||||
### 新功能
|
||||
+ 底部播放进度条设置
|
||||
+ 复制图片链接
|
||||
|
||||
|
||||
### 修复
|
||||
+ 用户数据格式修改
|
||||
+ video Fit
|
||||
+ 没有audio 资源的视频异常
|
||||
+ 评论区域图片无法点击
|
||||
+ 视频进度条拖动问题
|
||||
|
||||
### 优化
|
||||
+ 页面空/异常状态样式
|
||||
+ 部分页面样式
|
||||
+ 图片预览页面样式
|
21
change_log/1.0.4.0822.md
Normal file
21
change_log/1.0.4.0822.md
Normal file
@ -0,0 +1,21 @@
|
||||
## 1.0.4
|
||||
|
||||
### 新功能
|
||||
+ 热搜刷新
|
||||
+ 视频搜索排序、筛选
|
||||
+ app字体大小自定义
|
||||
+ app主题色自定义
|
||||
+ 「课堂」类动态渲染
|
||||
|
||||
|
||||
### 修复
|
||||
+ 搜索词联想richText渲染异常
|
||||
+ 部分动态点赞异常
|
||||
+ 默认视频解码格式
|
||||
+ 搜索页面返回搜索词未清空
|
||||
+ 动态详情评论加载异常
|
||||
+ 动态页面下拉刷新数据异常
|
||||
|
||||
### 优化
|
||||
+ 一些样式修改
|
||||
+ 取消热搜词缓存
|
30
change_log/1.0.5.0826.md
Normal file
30
change_log/1.0.5.0826.md
Normal file
@ -0,0 +1,30 @@
|
||||
## 1.0.5
|
||||
|
||||
主要是bug修复跟一部分小功能,弹幕功能需要下一版。
|
||||
问题反馈请前往QQ频道或提交issues。
|
||||
感谢🙏酷友「无力感*」「斤斤计较呀」「Pseudopamine」
|
||||
|
||||
### 新功能
|
||||
+ 高帧率支持
|
||||
+ 默认评论排序设置
|
||||
+ 默认动态类别设置
|
||||
+ 动态合集查看
|
||||
+ 同时观看人数
|
||||
+ iOS路由切换效果
|
||||
|
||||
|
||||
### 修复
|
||||
+ 收藏夹翻页
|
||||
+ 首页搜索框频繁点击消失
|
||||
+ 评论排序切换空白
|
||||
+ 快速返回首页
|
||||
+ 重复进入个人中心页面数据未刷新
|
||||
+ 动态goods数据异常
|
||||
+ 大会员切换番剧
|
||||
+ 高画质codes匹配
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择
|
||||
+ 播放器亮度记忆
|
||||
+ 下载对应版本apk
|
34
change_log/1.0.6.0902.md
Normal file
34
change_log/1.0.6.0902.md
Normal file
@ -0,0 +1,34 @@
|
||||
## 1.0.6
|
||||
|
||||
问题反馈、功能建议请查看「关于」页面。
|
||||
|
||||
### 新功能
|
||||
+ 首页单列布局
|
||||
+ 首页推荐展示播放量、弹幕数
|
||||
+ 简单弹幕功能实现(持续开发中...)
|
||||
+ 评论区搜索关键词开关 issues#46
|
||||
+ 热搜榜隐藏功能 issues#35
|
||||
+ 自动全屏 issues#37
|
||||
+ 快速收藏功能
|
||||
+ 双击快进/快退开关
|
||||
+ 评论链接跳转视频
|
||||
+ 支持移除单个稍后再看
|
||||
+ app scheme外链跳转
|
||||
|
||||
|
||||
### 修复
|
||||
+ 杜比、无损音频切换
|
||||
+ 收藏夹展示 issues#42
|
||||
+ 搜索建议次 issues#47
|
||||
|
||||
|
||||
### 优化
|
||||
+ 倍速选择优化
|
||||
+ 导航条沉浸
|
||||
+ 取消Hero动画
|
||||
+ 视频锁定逻辑
|
||||
+ 登录逻辑优化
|
||||
+ 图片预览样式
|
||||
+ +评论区用户点击范围
|
||||
+ 关注、粉丝页面优化
|
||||
+ 关闭自动播放时播放器初始化逻辑
|
22
change_log/1.0.7.0908.md
Normal file
22
change_log/1.0.7.0908.md
Normal file
@ -0,0 +1,22 @@
|
||||
## 1.0.7
|
||||
|
||||
默认倍速、直播弹幕、专栏等功能开发中
|
||||
|
||||
### 新功能
|
||||
+ 弹幕设置、屏蔽功能
|
||||
+ 不是很完美的后台播放功能
|
||||
+ 不是很完美的画中画(pip)功能(Android端)
|
||||
|
||||
### 修复
|
||||
+ 动态页面加载异常
|
||||
+ 网络异常时页面空白
|
||||
+ 竖屏全屏状态栏问题
|
||||
+ iOS端代理请求异常
|
||||
|
||||
### 优化
|
||||
+ 图片预览
|
||||
+ 全屏播放时自动旋转
|
||||
+ 转发内容增加视频标题
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
24
change_log/1.0.8.0917.md
Normal file
24
change_log/1.0.8.0917.md
Normal file
@ -0,0 +1,24 @@
|
||||
## 1.0.8
|
||||
|
||||
直播弹幕、循环播放等功能开发中
|
||||
|
||||
### 新功能
|
||||
+ 用户拉黑功能
|
||||
+ gif图片保存
|
||||
+ 删除已看历史记录
|
||||
|
||||
### 修复
|
||||
+ 弹幕数量较少
|
||||
+ 弹幕屏蔽设置自动记忆
|
||||
+ 动态页面渲染
|
||||
+ 用户主页数据错乱
|
||||
+ 大家都在搜空白
|
||||
+ 默认自动全屏,顶部操作栏丢失
|
||||
|
||||
|
||||
### 优化
|
||||
+ 全屏状态栏区域显示优化
|
||||
+ 图片保存至PiliPala文件夹
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
28
change_log/1.0.9.1015.md
Normal file
28
change_log/1.0.9.1015.md
Normal file
@ -0,0 +1,28 @@
|
||||
## 1.0.9
|
||||
|
||||
|
||||
### 新功能
|
||||
+ 自定义倍速、默认倍速
|
||||
+ 历史记录搜索
|
||||
+ 收藏夹搜索
|
||||
+ 历史记录多选删除
|
||||
+ 视频循环播放
|
||||
+ 免登录看1080P
|
||||
+ 评论区视频链接跳转
|
||||
+ up主分组
|
||||
+ up主投稿搜索
|
||||
|
||||
### 修复
|
||||
+ 搜索视频标题乱码
|
||||
+ 屏幕帧率
|
||||
+ 动态页面渲染
|
||||
|
||||
|
||||
|
||||
### 优化
|
||||
+ 快进手势
|
||||
+ 视频简介链接匹配
|
||||
+ 视频全屏时安全区域
|
||||
|
||||
更多更新日志可在Github上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
@ -1,15 +1,19 @@
|
||||
PODS:
|
||||
- appscheme (1.0.4):
|
||||
- Flutter
|
||||
- auto_orientation (0.0.1):
|
||||
- Flutter
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_volume_controller (0.0.1):
|
||||
- Flutter
|
||||
- FMDB (2.7.5):
|
||||
- FMDB/standard (= 2.7.5)
|
||||
- FMDB/standard (2.7.5)
|
||||
- image_gallery_saver (2.0.2):
|
||||
- Flutter
|
||||
- media_kit_libs_ios_video (1.0.4):
|
||||
- Flutter
|
||||
- media_kit_native_event_loop (1.0.0):
|
||||
@ -24,6 +28,8 @@ PODS:
|
||||
- permission_handler_apple (9.1.1):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- saver_gallery (0.0.1):
|
||||
- Flutter
|
||||
- screen_brightness_ios (0.1.0):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
@ -31,6 +37,10 @@ PODS:
|
||||
- sqflite (0.0.3):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- status_bar_control (3.2.1):
|
||||
- Flutter
|
||||
- system_proxy (0.0.1):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- volume_controller (0.0.1):
|
||||
@ -43,19 +53,24 @@ PODS:
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
||||
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
|
||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/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`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`)
|
||||
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
@ -68,14 +83,18 @@ SPEC REPOS:
|
||||
- ReachabilitySwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
appscheme:
|
||||
:path: ".symlinks/plugins/appscheme/ios"
|
||||
auto_orientation:
|
||||
:path: ".symlinks/plugins/auto_orientation/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
image_gallery_saver:
|
||||
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
||||
flutter_volume_controller:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
media_kit_libs_ios_video:
|
||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||
media_kit_native_event_loop:
|
||||
@ -88,12 +107,18 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
saver_gallery:
|
||||
:path: ".symlinks/plugins/saver_gallery/ios"
|
||||
screen_brightness_ios:
|
||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
sqflite:
|
||||
:path: ".symlinks/plugins/sqflite/ios"
|
||||
status_bar_control:
|
||||
:path: ".symlinks/plugins/status_bar_control/ios"
|
||||
system_proxy:
|
||||
:path: ".symlinks/plugins/system_proxy/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
volume_controller:
|
||||
@ -106,11 +131,13 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
||||
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
|
||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||
@ -118,9 +145,12 @@ SPEC CHECKSUMS:
|
||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
|
||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
|
||||
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
|
||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||
|
@ -1,62 +1,107 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>PiliPala</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>pilipala</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>请允许APP保存图片到相册</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>App需要您的同意,才能访问相册</string>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>App需要您的同意,才能访问媒体资料库</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>PiliPala</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>PiliPala</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>请允许APP保存图片到相册</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>App需要您的同意,才能访问相册</string>
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>App需要您的同意,才能访问媒体资料库</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
</array>
|
||||
<!-- Add Scheme related information -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>m.bilibili.com</string>
|
||||
<string>bilibili.com</string>
|
||||
<string>www.bilibili.com</string>
|
||||
<string>bangumi.bilibili.com</string>
|
||||
<string>bilibili.cn</string>
|
||||
<string>www.bilibili.cn</string>
|
||||
<string>bangumi.bilibili.cn</string>
|
||||
<string>bilibili.tv</string>
|
||||
<string>www.bilibili.tv</string>
|
||||
<string>bangumi.bilibili.tv</string>
|
||||
<string>miniapp.bilibili.com</string>
|
||||
<string>live.bilibili.com</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
|
||||
<!-- 当其他应用程序或系统通过 bilibili -->
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>bilibili</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>bilibili</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -45,11 +45,6 @@ class VideoCardVSkeleton extends StatelessWidget {
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
Container(
|
||||
width: 80,
|
||||
height: 12,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedDialog extends StatefulWidget {
|
||||
const AnimatedDialog({Key? key, required this.child}) : super(key: key);
|
||||
const AnimatedDialog({Key? key, required this.child, this.closeFn})
|
||||
: super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Function? closeFn;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => AnimatedDialogState();
|
||||
@ -39,12 +41,16 @@ class AnimatedDialogState extends State<AnimatedDialog>
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.black.withOpacity(opacityAnimation!.value),
|
||||
child: Center(
|
||||
child: FadeTransition(
|
||||
opacity: scaleAnimation!,
|
||||
child: ScaleTransition(
|
||||
scale: scaleAnimation!,
|
||||
child: widget.child,
|
||||
child: InkWell(
|
||||
splashColor: Colors.transparent,
|
||||
onTap: () => widget.closeFn!(),
|
||||
child: Center(
|
||||
child: FadeTransition(
|
||||
opacity: scaleAnimation!,
|
||||
child: ScaleTransition(
|
||||
scale: scaleAnimation!,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
351
lib/common/widgets/app_expansion_panel_list.dart
Normal file
351
lib/common/widgets/app_expansion_panel_list.dart
Normal file
@ -0,0 +1,351 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
|
||||
|
||||
class _SaltedKey<S, V> extends LocalKey {
|
||||
const _SaltedKey(this.salt, this.value);
|
||||
|
||||
final S salt;
|
||||
final V value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
return other is _SaltedKey<S, V> &&
|
||||
other.salt == salt &&
|
||||
other.value == value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, salt, value);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String saltString = S == String ? "<'$salt'>" : '<$salt>';
|
||||
final String valueString = V == String ? "<'$value'>" : '<$value>';
|
||||
return '[$saltString $valueString]';
|
||||
}
|
||||
}
|
||||
|
||||
class AppExpansionPanelList extends StatefulWidget {
|
||||
/// Creates an expansion panel list widget. The [expansionCallback] is
|
||||
/// triggered when an expansion panel expand/collapse button is pushed.
|
||||
///
|
||||
/// The [children] and [animationDuration] arguments must not be null.
|
||||
const AppExpansionPanelList({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.expansionCallback,
|
||||
this.animationDuration = kThemeAnimationDuration,
|
||||
this.expandedHeaderPadding = EdgeInsets.zero,
|
||||
this.dividerColor,
|
||||
this.elevation = 2,
|
||||
}) : _allowOnlyOnePanelOpen = false,
|
||||
initialOpenPanelValue = null;
|
||||
|
||||
/// The children of the expansion panel list. They are laid out in a similar
|
||||
/// fashion to [ListBody].
|
||||
final List<AppExpansionPanel> children;
|
||||
|
||||
/// The callback that gets called whenever one of the expand/collapse buttons
|
||||
/// is pressed. The arguments passed to the callback are the index of the
|
||||
/// pressed panel and whether the panel is currently expanded or not.
|
||||
///
|
||||
/// If AppExpansionPanelList.radio is used, the callback may be called a
|
||||
/// second time if a different panel was previously open. The arguments
|
||||
/// passed to the second callback are the index of the panel that will close
|
||||
/// and false, marking that it will be closed.
|
||||
///
|
||||
/// For AppExpansionPanelList, the callback needs to setState when it's notified
|
||||
/// about the closing/opening panel. On the other hand, the callback for
|
||||
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of
|
||||
/// changes, as the radio panels' open/close states are managed internally.
|
||||
///
|
||||
/// This callback is useful in order to keep track of the expanded/collapsed
|
||||
/// panels in a parent widget that may need to react to these changes.
|
||||
final ExpansionPanelCallback? expansionCallback;
|
||||
|
||||
/// The duration of the expansion animation.
|
||||
final Duration animationDuration;
|
||||
|
||||
// Whether multiple panels can be open simultaneously
|
||||
final bool _allowOnlyOnePanelOpen;
|
||||
|
||||
/// The value of the panel that initially begins open. (This value is
|
||||
/// only used when initializing with the [AppExpansionPanelList.radio]
|
||||
/// constructor.)
|
||||
final Object? initialOpenPanelValue;
|
||||
|
||||
/// The padding that surrounds the panel header when expanded.
|
||||
///
|
||||
/// By default, 16px of space is added to the header vertically (above and below)
|
||||
/// during expansion.
|
||||
final EdgeInsets expandedHeaderPadding;
|
||||
|
||||
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.
|
||||
///
|
||||
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
|
||||
/// is null, then [ThemeData.dividerColor] is used.
|
||||
final Color? dividerColor;
|
||||
|
||||
/// Defines elevation for the [AppExpansionPanel] while it's expanded.
|
||||
///
|
||||
/// By default, the value of elevation is 2.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();
|
||||
}
|
||||
|
||||
class _AppExpansionPanelListState extends State<AppExpansionPanelList> {
|
||||
ExpansionPanelRadio? _currentOpenPanel;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
assert(_allIdentifiersUnique(),
|
||||
'All ExpansionPanelRadio identifier values must be unique.');
|
||||
if (widget.initialOpenPanelValue != null) {
|
||||
_currentOpenPanel = searchPanelByValue(
|
||||
widget.children.cast<ExpansionPanelRadio>(),
|
||||
widget.initialOpenPanelValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AppExpansionPanelList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
assert(_allIdentifiersUnique(),
|
||||
'All ExpansionPanelRadio identifier values must be unique.');
|
||||
// If the previous widget was non-radio AppExpansionPanelList, initialize the
|
||||
// open panel to widget.initialOpenPanelValue
|
||||
if (!oldWidget._allowOnlyOnePanelOpen) {
|
||||
_currentOpenPanel = searchPanelByValue(
|
||||
widget.children.cast<ExpansionPanelRadio>(),
|
||||
widget.initialOpenPanelValue);
|
||||
}
|
||||
} else {
|
||||
_currentOpenPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
bool _allIdentifiersUnique() {
|
||||
final Map<Object, bool> identifierMap = <Object, bool>{};
|
||||
for (final ExpansionPanelRadio child
|
||||
in widget.children.cast<ExpansionPanelRadio>()) {
|
||||
identifierMap[child.value] = true;
|
||||
}
|
||||
return identifierMap.length == widget.children.length;
|
||||
}
|
||||
|
||||
bool _isChildExpanded(int index) {
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
final ExpansionPanelRadio radioWidget =
|
||||
widget.children[index] as ExpansionPanelRadio;
|
||||
return _currentOpenPanel?.value == radioWidget.value;
|
||||
}
|
||||
return widget.children[index].isExpanded;
|
||||
}
|
||||
|
||||
void _handlePressed(bool isExpanded, int index) {
|
||||
widget.expansionCallback?.call(index, isExpanded);
|
||||
|
||||
if (widget._allowOnlyOnePanelOpen) {
|
||||
final ExpansionPanelRadio pressedChild =
|
||||
widget.children[index] as ExpansionPanelRadio;
|
||||
|
||||
// If another ExpansionPanelRadio was already open, apply its
|
||||
// expansionCallback (if any) to false, because it's closing.
|
||||
for (int childIndex = 0;
|
||||
childIndex < widget.children.length;
|
||||
childIndex += 1) {
|
||||
final ExpansionPanelRadio child =
|
||||
widget.children[childIndex] as ExpansionPanelRadio;
|
||||
if (widget.expansionCallback != null &&
|
||||
childIndex != index &&
|
||||
child.value == _currentOpenPanel?.value) {
|
||||
widget.expansionCallback!(childIndex, false);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_currentOpenPanel = isExpanded ? null : pressedChild;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ExpansionPanelRadio? searchPanelByValue(
|
||||
List<ExpansionPanelRadio> panels, Object? value) {
|
||||
for (final ExpansionPanelRadio panel in panels) {
|
||||
if (panel.value == value) return panel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(
|
||||
kElevationToShadow.containsKey(widget.elevation),
|
||||
'Invalid value for elevation. See the kElevationToShadow constant for'
|
||||
' possible elevation values.',
|
||||
);
|
||||
|
||||
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
|
||||
|
||||
for (int index = 0; index < widget.children.length; index += 1) {
|
||||
//todo: Uncomment to add gap between selected panels
|
||||
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
|
||||
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/
|
||||
|
||||
final AppExpansionPanel child = widget.children[index];
|
||||
final Widget headerWidget = child.headerBuilder(
|
||||
context,
|
||||
_isChildExpanded(index),
|
||||
);
|
||||
|
||||
Widget? expandIconContainer = ExpandIcon(
|
||||
isExpanded: _isChildExpanded(index),
|
||||
onPressed: !child.canTapOnHeader
|
||||
? (bool isExpanded) => _handlePressed(isExpanded, index)
|
||||
: null,
|
||||
);
|
||||
if (!child.canTapOnHeader) {
|
||||
final MaterialLocalizations localizations =
|
||||
MaterialLocalizations.of(context);
|
||||
expandIconContainer = Semantics(
|
||||
label: _isChildExpanded(index)
|
||||
? localizations.expandedIconTapHint
|
||||
: localizations.collapsedIconTapHint,
|
||||
container: true,
|
||||
child: expandIconContainer,
|
||||
);
|
||||
}
|
||||
|
||||
final iconContainer = child.iconBuilder;
|
||||
if (iconContainer != null) {
|
||||
expandIconContainer = iconContainer(
|
||||
expandIconContainer,
|
||||
_isChildExpanded(index),
|
||||
);
|
||||
}
|
||||
|
||||
Widget header = Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: AnimatedContainer(
|
||||
duration: widget.animationDuration,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
margin: _isChildExpanded(index)
|
||||
? widget.expandedHeaderPadding
|
||||
: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: _kPanelHeaderCollapsedHeight),
|
||||
child: headerWidget,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (expandIconContainer != null) expandIconContainer,
|
||||
],
|
||||
);
|
||||
if (child.canTapOnHeader) {
|
||||
header = MergeSemantics(
|
||||
child: InkWell(
|
||||
onTap: () => _handlePressed(_isChildExpanded(index), index),
|
||||
child: header,
|
||||
),
|
||||
);
|
||||
}
|
||||
items.add(
|
||||
MaterialSlice(
|
||||
key: _SaltedKey<BuildContext, int>(context, index * 2),
|
||||
color: child.backgroundColor,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
header,
|
||||
AnimatedCrossFade(
|
||||
firstChild: Container(height: 0.0),
|
||||
secondChild: child.body,
|
||||
firstCurve:
|
||||
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
|
||||
secondCurve:
|
||||
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
|
||||
sizeCurve: Curves.fastOutSlowIn,
|
||||
crossFadeState: _isChildExpanded(index)
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: widget.animationDuration,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (_isChildExpanded(index) && index != widget.children.length - 1) {
|
||||
items.add(MaterialGap(
|
||||
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
return MergeableMaterial(
|
||||
hasDividers: true,
|
||||
dividerColor: widget.dividerColor,
|
||||
elevation: widget.elevation,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef ExpansionPanelIconBuilder = Widget? Function(
|
||||
Widget child,
|
||||
bool isExpanded,
|
||||
);
|
||||
|
||||
class AppExpansionPanel {
|
||||
/// Creates an expansion panel to be used as a child for [ExpansionPanelList].
|
||||
/// See [ExpansionPanelList] for an example on how to use this widget.
|
||||
///
|
||||
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
|
||||
AppExpansionPanel({
|
||||
required this.headerBuilder,
|
||||
required this.body,
|
||||
this.iconBuilder,
|
||||
this.isExpanded = false,
|
||||
this.canTapOnHeader = false,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
/// The widget builder that builds the expansion panels' header.
|
||||
final ExpansionPanelHeaderBuilder headerBuilder;
|
||||
|
||||
/// The widget builder that builds the expansion panels' icon.
|
||||
///
|
||||
/// If not pass any function, then default icon will be displayed.
|
||||
///
|
||||
/// If builder function return null, then icon will not displayed.
|
||||
final ExpansionPanelIconBuilder? iconBuilder;
|
||||
|
||||
/// The body of the expansion panel that's displayed below the header.
|
||||
///
|
||||
/// This widget is visible only when the panel is expanded.
|
||||
final Widget body;
|
||||
|
||||
/// Whether the panel is expanded.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool isExpanded;
|
||||
|
||||
/// Whether tapping on the panel's header will expand/collapse it.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool canTapOnHeader;
|
||||
|
||||
/// Defines the background color of the panel.
|
||||
///
|
||||
/// Defaults to [ThemeData.cardColor].
|
||||
final Color? backgroundColor;
|
||||
}
|
96
lib/common/widgets/html_render.dart
Normal file
96
lib/common/widgets/html_render.dart
Normal file
@ -0,0 +1,96 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class HtmlRender extends StatelessWidget {
|
||||
String? htmlContent;
|
||||
final int? imgCount;
|
||||
final List? imgList;
|
||||
|
||||
HtmlRender({
|
||||
this.htmlContent,
|
||||
this.imgCount,
|
||||
this.imgList,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Html(
|
||||
data: htmlContent,
|
||||
onLinkTap: (url, buildContext, attributes) => {},
|
||||
extensions: [
|
||||
TagExtension(
|
||||
tagsToExtend: {"img"},
|
||||
builder: (extensionContext) {
|
||||
try {
|
||||
Map attributes = extensionContext.attributes;
|
||||
List key = attributes.keys.toList();
|
||||
String? imgUrl = key.contains('src')
|
||||
? attributes['src']
|
||||
: attributes['data-src'];
|
||||
if (imgUrl!.startsWith('//')) {
|
||||
imgUrl = 'https:$imgUrl';
|
||||
}
|
||||
if (imgUrl.startsWith('http://')) {
|
||||
imgUrl = imgUrl.replaceAll('http://', 'https://');
|
||||
}
|
||||
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
|
||||
bool isEmote = imgUrl.contains('/emote/');
|
||||
bool isMall = imgUrl.contains('/mall/');
|
||||
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,
|
||||
);
|
||||
} catch (err) {
|
||||
print(err);
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
style: {
|
||||
"html": Style(
|
||||
fontSize: FontSize.medium,
|
||||
lineHeight: LineHeight.percent(140),
|
||||
),
|
||||
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||
"a": Style(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textDecoration: TextDecoration.none,
|
||||
),
|
||||
"p": Style(
|
||||
margin: Margins.only(bottom: 10),
|
||||
),
|
||||
"span": Style(
|
||||
fontSize: FontSize.medium,
|
||||
height: Height(1.65),
|
||||
),
|
||||
"div": Style(height: Height.auto()),
|
||||
"li > p": Style(
|
||||
display: Display.inline,
|
||||
),
|
||||
"li": Style(
|
||||
padding: HtmlPaddings.only(bottom: 4),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
"img": Style(margin: Margins.only(top: 4, bottom: 4)),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,31 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class HttpError extends StatelessWidget {
|
||||
const HttpError({required this.errMsg, required this.fn, super.key});
|
||||
const HttpError(
|
||||
{required this.errMsg, required this.fn, this.btnText, super.key});
|
||||
|
||||
final String? errMsg;
|
||||
final Function()? fn;
|
||||
final String? btnText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 150,
|
||||
height: 400,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
"assets/images/error.svg",
|
||||
height: 200,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
errMsg ?? '请求异常',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
fn!();
|
||||
},
|
||||
child: const Text('点击重试'))
|
||||
const SizedBox(height: 30),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
fn!();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward_outlined, size: 20),
|
||||
label: Text(btnText ?? '点击重试'),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/rcmd/controller.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class LiveCard extends StatelessWidget {
|
||||
@ -95,7 +93,7 @@ class LiveContent extends StatelessWidget {
|
||||
liveItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
|
||||
maxLines: Get.find<RcmdController>().crossAxisCount,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(
|
||||
|
@ -70,9 +70,17 @@ class NetworkImgLayer extends StatelessWidget {
|
||||
|
||||
Widget placeholder(context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
|
||||
width: width ?? double.infinity,
|
||||
height: height ?? double.infinity,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
|
||||
borderRadius: BorderRadius.circular(type == 'avatar'
|
||||
? 50
|
||||
: type == 'emote'
|
||||
? 0
|
||||
: StyleString.imgRadius.x),
|
||||
),
|
||||
child: Center(
|
||||
child: Image.asset(
|
||||
type == 'avatar'
|
||||
|
31
lib/common/widgets/no_data.dart
Normal file
31
lib/common/widgets/no_data.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class NoData extends StatelessWidget {
|
||||
const NoData({super.key});
|
||||
|
||||
@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: 20),
|
||||
Text(
|
||||
'没有数据',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,42 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/utils/download.dart';
|
||||
|
||||
class OverlayPop extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const OverlayPop({super.key, this.videoItem});
|
||||
final Function? closeFn;
|
||||
const OverlayPop({super.key, this.videoItem, this.closeFn});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double imgWidth = MediaQuery.of(context).size.width - 8 * 2;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: (MediaQuery.of(context).size.width - 16),
|
||||
height: (MediaQuery.of(context).size.width - 16) /
|
||||
StyleString.aspectRatio,
|
||||
src: videoItem.pic!,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 15, 10, 15),
|
||||
child: Text(
|
||||
videoItem.title!,
|
||||
// maxLines: 1,
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
NetworkImgLayer(
|
||||
width: imgWidth,
|
||||
height: imgWidth / StyleString.aspectRatio,
|
||||
src: videoItem.pic!,
|
||||
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!,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
tooltip: '保存封面图',
|
||||
onPressed: () async {
|
||||
await DownloadUtils.downloadImg(
|
||||
videoItem.pic ?? videoItem.cover);
|
||||
// closeFn!();
|
||||
},
|
||||
icon: const Icon(Icons.download, size: 20),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -37,11 +37,11 @@ class VideoCardH extends StatelessWidget {
|
||||
longPress!();
|
||||
}
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
if (longPressEnd != null) {
|
||||
longPressEnd!();
|
||||
}
|
||||
},
|
||||
// onLongPressEnd: (details) {
|
||||
// if (longPressEnd != null) {
|
||||
// longPressEnd!();
|
||||
// }
|
||||
// },
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
@ -55,11 +55,14 @@ class VideoCardH extends StatelessWidget {
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 7, StyleString.safeSpace, 7),
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
double width = (boxConstraints.maxWidth -
|
||||
StyleString.cardSpace *
|
||||
6 /
|
||||
MediaQuery.of(context).textScaleFactor) /
|
||||
2;
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 88),
|
||||
height: width / StyleString.aspectRatio,
|
||||
@ -123,7 +126,7 @@ class VideoContent extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 6, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -132,7 +135,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
@ -147,7 +149,6 @@ class VideoContent extends StatelessWidget {
|
||||
TextSpan(
|
||||
text: i['text'],
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
color: i['type'] == 'em'
|
||||
@ -177,7 +178,7 @@ class VideoContent extends StatelessWidget {
|
||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 4),
|
||||
// const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
|
@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/badge.dart';
|
||||
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/http/dynamics.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
@ -14,18 +15,24 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
|
||||
// 视频卡片 - 垂直布局
|
||||
class VideoCardV extends StatelessWidget {
|
||||
// ignore: prefer_typing_uninitialized_variables
|
||||
final videoItem;
|
||||
final dynamic videoItem;
|
||||
final int crossAxisCount;
|
||||
final Function()? longPress;
|
||||
final Function()? longPressEnd;
|
||||
|
||||
const VideoCardV({
|
||||
Key? key,
|
||||
required this.videoItem,
|
||||
required this.crossAxisCount,
|
||||
this.longPress,
|
||||
this.longPressEnd,
|
||||
}) : super(key: key);
|
||||
|
||||
bool isStringNumeric(String str) {
|
||||
RegExp numericRegex = RegExp(r'^\d+$');
|
||||
return numericRegex.hasMatch(str);
|
||||
}
|
||||
|
||||
void onPushDetail(heroTag) async {
|
||||
String goto = videoItem.goto;
|
||||
switch (goto) {
|
||||
@ -61,6 +68,49 @@ class VideoCardV extends StatelessWidget {
|
||||
'heroTag': heroTag,
|
||||
});
|
||||
break;
|
||||
// 动态
|
||||
case 'picture':
|
||||
String dynamicType = 'picture';
|
||||
String uri = videoItem.uri;
|
||||
if (videoItem.uri.contains('bilibili://article/')) {
|
||||
dynamicType = 'article';
|
||||
RegExp regex = RegExp(r'\d+');
|
||||
Match match = regex.firstMatch(videoItem.uri)!;
|
||||
String matchedNumber = match.group(0)!;
|
||||
videoItem.param = 'cv' + matchedNumber;
|
||||
}
|
||||
if (uri.startsWith('http')) {
|
||||
String path = Uri.parse(uri).path;
|
||||
if (isStringNumeric(path.split('/')[1])) {
|
||||
// 请求接口
|
||||
var res = await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
|
||||
if (res['status']) {
|
||||
Get.toNamed('/dynamicDetail', arguments: {
|
||||
'item': res['data'],
|
||||
'floor': 1,
|
||||
'action': 'detail'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
Get.toNamed('/htmlRender', parameters: {
|
||||
'url': uri,
|
||||
'title': videoItem.title,
|
||||
'id': videoItem.param.toString(),
|
||||
'dynamicType': dynamicType
|
||||
});
|
||||
break;
|
||||
default:
|
||||
SmartDialog.showToast(videoItem.goto);
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': videoItem.uri,
|
||||
'type': 'url',
|
||||
'pageTitle': videoItem.title,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,9 +120,6 @@ class VideoCardV extends StatelessWidget {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
@ -80,59 +127,51 @@ class VideoCardV extends StatelessWidget {
|
||||
longPress!();
|
||||
}
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
if (longPressEnd != null) {
|
||||
longPressEnd!();
|
||||
}
|
||||
},
|
||||
// onLongPressEnd: (details) {
|
||||
// if (longPressEnd != null) {
|
||||
// longPressEnd!();
|
||||
// }
|
||||
// },
|
||||
child: InkWell(
|
||||
onTap: () async => onPushDetail(heroTag),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: StyleString.imgRadius,
|
||||
topRight: StyleString.imgRadius,
|
||||
bottomLeft: StyleString.imgRadius,
|
||||
bottomRight: StyleString.imgRadius,
|
||||
),
|
||||
child: 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,
|
||||
),
|
||||
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.stat.view is int &&
|
||||
// videoItem.stat.danmaku is int)
|
||||
// Positioned(
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// bottom: 0,
|
||||
// child: AnimatedOpacity(
|
||||
// opacity: 1,
|
||||
// duration: const Duration(milliseconds: 200),
|
||||
// child: VideoStat(
|
||||
// view: videoItem.stat.view,
|
||||
// danmaku: videoItem.stat.danmaku,
|
||||
// duration: videoItem.duration,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
if (videoItem.duration != null)
|
||||
if (crossAxisCount == 1) ...[
|
||||
PBadge(
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
text: videoItem.duration,
|
||||
)
|
||||
] else ...[
|
||||
PBadge(
|
||||
bottom: 6,
|
||||
right: 7,
|
||||
size: 'small',
|
||||
type: 'gray',
|
||||
text: videoItem.duration,
|
||||
)
|
||||
],
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -143,24 +182,53 @@ class VideoCardV extends StatelessWidget {
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const VideoContent({Key? key, required this.videoItem}) : super(key: key);
|
||||
final int crossAxisCount;
|
||||
const VideoContent(
|
||||
{Key? key, required this.videoItem, required this.crossAxisCount})
|
||||
: super(key: key);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
flex: crossAxisCount == 1 ? 0 : 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 8, 0, 3),
|
||||
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,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
videoItem.title,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
if (videoItem.goto == 'av' && crossAxisCount == 1) ...[
|
||||
const SizedBox(width: 10),
|
||||
WatchLater(
|
||||
size: 32,
|
||||
iconSize: 18,
|
||||
callFn: () async {
|
||||
int aid = videoItem.param;
|
||||
var res =
|
||||
await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid));
|
||||
SmartDialog.showToast(res['msg']);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
if (crossAxisCount > 1) ...[
|
||||
const SizedBox(height: 2),
|
||||
VideoStat(
|
||||
videoItem: videoItem,
|
||||
),
|
||||
],
|
||||
if (crossAxisCount == 1) const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
if (videoItem.goto == 'bangumi') ...[
|
||||
@ -181,112 +249,57 @@ class VideoContent extends StatelessWidget {
|
||||
type: 'color',
|
||||
)
|
||||
],
|
||||
if (videoItem.goto == 'picture') ...[
|
||||
const PBadge(
|
||||
text: '动态',
|
||||
stack: 'normal',
|
||||
size: 'small',
|
||||
type: 'line',
|
||||
fs: 9,
|
||||
)
|
||||
],
|
||||
Expanded(
|
||||
child: LayoutBuilder(builder:
|
||||
(BuildContext context, BoxConstraints constraints) {
|
||||
return SizedBox(
|
||||
width: constraints.maxWidth,
|
||||
child: Text(
|
||||
videoItem.owner.name,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
if (videoItem.goto == 'av')
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: '稍后再看',
|
||||
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) =>
|
||||
<PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
onTap: () async {
|
||||
int aid = videoItem.param;
|
||||
var res = await UserHttp.toViewLater(
|
||||
bvid: IdUtils.av2bv(aid));
|
||||
SmartDialog.showToast(res['msg']);
|
||||
},
|
||||
value: 'pause',
|
||||
height: 35,
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.watch_later_outlined, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text('稍后再看', style: TextStyle(fontSize: 13))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
if (videoItem.goto == 'av' && crossAxisCount != 1) ...[
|
||||
WatchLater(
|
||||
size: 24,
|
||||
iconSize: 14,
|
||||
callFn: () async {
|
||||
int aid = videoItem.param;
|
||||
var res =
|
||||
await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid));
|
||||
SmartDialog.showToast(res['msg']);
|
||||
},
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 24)
|
||||
]
|
||||
],
|
||||
),
|
||||
// Row(
|
||||
// children: [
|
||||
// const SizedBox(width: 1),
|
||||
// StatView(
|
||||
// theme: 'gray',
|
||||
// view: videoItem.stat.view,
|
||||
// ),
|
||||
// const SizedBox(width: 10),
|
||||
// StatDanMu(
|
||||
// theme: 'gray',
|
||||
// danmu: videoItem.stat.danmaku,
|
||||
// ),
|
||||
// const Spacer(),
|
||||
// SizedBox(
|
||||
// width: 24,
|
||||
// height: 24,
|
||||
// child: PopupMenuButton<String>(
|
||||
// padding: EdgeInsets.zero,
|
||||
// tooltip: '稍后再看',
|
||||
// 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) =>
|
||||
// <PopupMenuEntry<String>>[
|
||||
// PopupMenuItem<String>(
|
||||
// onTap: () async {
|
||||
// var res =
|
||||
// await UserHttp.toViewLater(bvid: videoItem.bvid);
|
||||
// SmartDialog.showToast(res['msg']);
|
||||
// },
|
||||
// value: 'pause',
|
||||
// height: 35,
|
||||
// child: const Row(
|
||||
// children: [
|
||||
// Icon(Icons.watch_later_outlined, size: 16),
|
||||
// SizedBox(width: 6),
|
||||
// Text('稍后再看', style: TextStyle(fontSize: 13))
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -295,53 +308,72 @@ class VideoContent extends StatelessWidget {
|
||||
}
|
||||
|
||||
class VideoStat extends StatelessWidget {
|
||||
final int? view;
|
||||
final int? danmaku;
|
||||
final int? duration;
|
||||
final dynamic videoItem;
|
||||
|
||||
const VideoStat(
|
||||
{Key? key,
|
||||
required this.view,
|
||||
required this.danmaku,
|
||||
required this.duration})
|
||||
: super(key: key);
|
||||
const VideoStat({
|
||||
Key? key,
|
||||
required this.videoItem,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 48,
|
||||
padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
Colors.transparent,
|
||||
Colors.black54,
|
||||
],
|
||||
tileMode: TileMode.mirror,
|
||||
return RichText(
|
||||
maxLines: 1,
|
||||
text: TextSpan(
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'white',
|
||||
view: view,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
StatDanMu(
|
||||
theme: 'white',
|
||||
danmu: danmaku,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
Utils.timeFormat(duration!),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
)
|
||||
TextSpan(text: '${videoItem.stat.view}观看'),
|
||||
TextSpan(text: ' • ${videoItem.stat.danmu}弹幕'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WatchLater extends StatelessWidget {
|
||||
final double? size;
|
||||
final double? iconSize;
|
||||
final Function? callFn;
|
||||
|
||||
const WatchLater({
|
||||
Key? key,
|
||||
required this.size,
|
||||
required this.iconSize,
|
||||
this.callFn,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: PopupMenuButton<String>(
|
||||
padding: EdgeInsets.zero,
|
||||
tooltip: '稍后再看',
|
||||
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) => <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
onTap: () => callFn!(),
|
||||
value: 'pause',
|
||||
height: 35,
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.watch_later_outlined, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text('稍后再看', style: TextStyle(fontSize: 13))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -97,6 +97,9 @@ class Api {
|
||||
// 操作用户关系
|
||||
static const String relationMod = '/x/relation/modify';
|
||||
|
||||
// 相互关系查询
|
||||
static const String relationSearch = '/x/space/wbi/acc/relation';
|
||||
|
||||
// 评论列表
|
||||
// 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';
|
||||
@ -126,12 +129,14 @@ class Api {
|
||||
static const String userFavFolder = '/x/v3/fav/folder/created/list';
|
||||
|
||||
/// 收藏夹 详情
|
||||
/// media_id int 收藏夹id
|
||||
/// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
|
||||
/// pn int 当前页
|
||||
/// ps int pageSize
|
||||
/// keyword String 搜索词
|
||||
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
|
||||
/// tid int 分区id
|
||||
/// platform web
|
||||
/// type 0 当前收藏夹 1 全部收藏夹
|
||||
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
|
||||
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
|
||||
|
||||
@ -164,6 +169,12 @@ class Api {
|
||||
// 清空历史记录
|
||||
static const String clearHistory = '/x/v2/history/clear';
|
||||
|
||||
// 删除某条历史记录
|
||||
static const String delHistory = '/x/v2/history/delete';
|
||||
|
||||
// 搜索历史记录
|
||||
static const String searchHistory = '/x/web-goblin/history/search';
|
||||
|
||||
// 热搜
|
||||
static const String hotSearchList =
|
||||
'https://s.search.bilibili.com/main/hotword';
|
||||
@ -239,6 +250,9 @@ class Api {
|
||||
// wts=1689767832
|
||||
static const String memberArchive = '/x/space/wbi/arc/search';
|
||||
|
||||
// 用户动态搜索
|
||||
static const String memberDynamicSearch = '/x/space/dynamic/search';
|
||||
|
||||
// 用户动态
|
||||
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
|
||||
|
||||
@ -248,6 +262,9 @@ class Api {
|
||||
// 移除已观看
|
||||
static const String toViewDel = '/x/v2/history/toview/del';
|
||||
|
||||
// 清空稍后再看
|
||||
static const String toViewClear = '/x/v2/history/toview/clear';
|
||||
|
||||
// 追番
|
||||
static const String bangumiAdd = '/pgc/web/follow/add';
|
||||
|
||||
@ -282,7 +299,32 @@ class Api {
|
||||
// 黑名单
|
||||
static const String blackLst = '/x/relation/blacks';
|
||||
|
||||
// 移除黑名单
|
||||
static const String removeBlack = '/x/relation/modify';
|
||||
|
||||
// github 获取最新版
|
||||
static const String latestApp =
|
||||
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
|
||||
|
||||
// 多少人在看
|
||||
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
|
||||
static const String onlineTotal = '/x/player/online/total';
|
||||
|
||||
static const String webDanmaku = '/x/v2/dm/web/seg.so';
|
||||
|
||||
// up主分组
|
||||
static const String followUpTag = '/x/relation/tags';
|
||||
|
||||
// 设置Up主分组
|
||||
// 0 添加至默认分组 否则使用,分割tagid
|
||||
static const String addUsers = '/x/relation/tags/addUsers';
|
||||
|
||||
// 获取指定分组下的up
|
||||
static const String followUpGroup = '/x/relation/tag';
|
||||
|
||||
// 获取某个动态详情
|
||||
// timezone_offset=-480
|
||||
// id=849312409672744983
|
||||
// features=itemOpusStyle
|
||||
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
|
||||
}
|
||||
|
@ -23,4 +23,31 @@ class BlackHttp {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 移除黑名单
|
||||
static Future removeBlack({required int fid}) async {
|
||||
var res = await Request().post(
|
||||
Api.removeBlack,
|
||||
queryParameters: {
|
||||
'act': 6,
|
||||
'csrf': await Request.getCsrf(),
|
||||
'fid': fid,
|
||||
'jsonp': 'jsonp',
|
||||
're_src': 116,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': [],
|
||||
'msg': '操作成功',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,37 @@ class HttpString {
|
||||
static const String baseUrl = 'https://www.bilibili.com';
|
||||
static const String baseApiUrl = 'https://api.bilibili.com';
|
||||
static const String tUrl = 'https://api.vc.bilibili.com';
|
||||
static const List<int> validateStatusCodes = [
|
||||
302,
|
||||
304,
|
||||
307,
|
||||
400,
|
||||
401,
|
||||
403,
|
||||
404,
|
||||
405,
|
||||
409,
|
||||
412,
|
||||
500,
|
||||
503,
|
||||
504,
|
||||
509,
|
||||
616,
|
||||
617,
|
||||
625,
|
||||
626,
|
||||
628,
|
||||
629,
|
||||
632,
|
||||
643,
|
||||
650,
|
||||
652,
|
||||
658,
|
||||
662,
|
||||
688,
|
||||
689,
|
||||
701,
|
||||
799,
|
||||
8888
|
||||
];
|
||||
}
|
||||
|
27
lib/http/danmaku.dart
Normal file
27
lib/http/danmaku.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:pilipala/http/index.dart';
|
||||
import 'package:pilipala/models/danmaku/dm.pb.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
|
||||
class DanmakaHttp {
|
||||
// 获取视频弹幕
|
||||
static Future queryDanmaku({
|
||||
required int cid,
|
||||
required int segmentIndex,
|
||||
}) async {
|
||||
// 构建参数对象
|
||||
Map<String, int> params = {
|
||||
'type': 1,
|
||||
'oid': cid,
|
||||
'segment_index': segmentIndex,
|
||||
};
|
||||
var response = await Request().get(
|
||||
Api.webDanmaku,
|
||||
data: params,
|
||||
extra: {'resType': ResponseType.bytes},
|
||||
);
|
||||
return DmSegMobileReply.fromBuffer(response.data);
|
||||
}
|
||||
}
|
@ -22,10 +22,19 @@ class DynamicsHttp {
|
||||
}
|
||||
var res = await Request().get(Api.followDynamic, data: data);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': DynamicsDataModel.fromJson(res.data['data']),
|
||||
};
|
||||
try {
|
||||
return {
|
||||
'status': true,
|
||||
'data': DynamicsDataModel.fromJson(res.data['data']),
|
||||
};
|
||||
} catch (err) {
|
||||
print(err);
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': err.toString(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
@ -77,4 +86,35 @@ class DynamicsHttp {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
static Future dynamicDetail({
|
||||
String? id,
|
||||
}) async {
|
||||
var res = await Request().get(Api.dynamicDetail, data: {
|
||||
'timezone_offset': -480,
|
||||
'id': id,
|
||||
'features': 'itemOpusStyle',
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
try {
|
||||
return {
|
||||
'status': true,
|
||||
'data': DynamicItemModel.fromJson(res.data['data']['item']),
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': err.toString(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
103
lib/http/html.dart
Normal file
103
lib/http/html.dart
Normal file
@ -0,0 +1,103 @@
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:pilipala/http/index.dart';
|
||||
|
||||
class HtmlHttp {
|
||||
// article
|
||||
static Future reqHtml(id, dynamicType) async {
|
||||
var response = await Request().get(
|
||||
"https://www.bilibili.com/opus/$id",
|
||||
extra: {'ua': 'pc'},
|
||||
);
|
||||
|
||||
if (response.data.contains('Redirecting to')) {
|
||||
RegExp regex = RegExp(r'//([\w\.]+)/(\w+)/(\w+)');
|
||||
Match match = regex.firstMatch(response.data)!;
|
||||
String matchedString = match.group(0)!;
|
||||
response = await Request().get(
|
||||
'https:$matchedString' + '/',
|
||||
extra: {'ua': 'pc'},
|
||||
);
|
||||
}
|
||||
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')!;
|
||||
// 头像
|
||||
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
|
||||
avatar = 'https:${avatar.split('@')[0]}';
|
||||
String uname = authorHeader
|
||||
.querySelector('.fixed-author-header__author__name')!
|
||||
.text;
|
||||
|
||||
// 动态详情
|
||||
Element opusDetail = appDom.querySelector('.opus-detail')!;
|
||||
// 发布时间
|
||||
String updateTime =
|
||||
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
|
||||
//
|
||||
String opusContent =
|
||||
opusDetail.querySelector('.opus-module-content')!.innerHtml;
|
||||
String test = opusDetail
|
||||
.querySelector('.horizontal-scroll-album__pic__img')!
|
||||
.innerHtml;
|
||||
String commentId = opusDetail
|
||||
.querySelector('.bili-comment-container')!
|
||||
.className
|
||||
.split(' ')[1]
|
||||
.split('-')[2];
|
||||
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
|
||||
return {
|
||||
'status': true,
|
||||
'avatar': avatar,
|
||||
'uname': uname,
|
||||
'updateTime': updateTime,
|
||||
'content': test + opusContent,
|
||||
'commentId': int.parse(commentId)
|
||||
};
|
||||
} catch (err) {
|
||||
print('err: $err');
|
||||
}
|
||||
}
|
||||
|
||||
// read
|
||||
static Future reqReadHtml(id, dynamicType) async {
|
||||
var response = await Request().get(
|
||||
"https://www.bilibili.com/$dynamicType/$id/",
|
||||
extra: {'ua': 'pc'},
|
||||
);
|
||||
Document rootTree = parse(response.data);
|
||||
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+');
|
||||
Iterable<Match> matches = digitRegExp.allMatches(id);
|
||||
String number = matches.first.group(0)!;
|
||||
return {
|
||||
'status': true,
|
||||
'avatar': '',
|
||||
'uname': uname,
|
||||
'updateTime': '',
|
||||
'content': opusContent,
|
||||
'commentId': int.parse(number)
|
||||
};
|
||||
}
|
||||
}
|
@ -4,32 +4,23 @@ import 'dart:io';
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:cookie_jar/cookie_jar.dart';
|
||||
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/http/constants.dart';
|
||||
import 'package:pilipala/http/interceptor.dart';
|
||||
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
|
||||
class Request {
|
||||
static final Request _instance = Request._internal();
|
||||
static late CookieManager cookieManager;
|
||||
|
||||
static late final Dio dio;
|
||||
factory Request() => _instance;
|
||||
|
||||
static Dio dio = Dio()
|
||||
..httpClientAdapter = Http2Adapter(
|
||||
ConnectionManager(
|
||||
idleTimeout: const Duration(milliseconds: 10000),
|
||||
// Ignore bad certificate
|
||||
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
|
||||
),
|
||||
);
|
||||
|
||||
/// 设置cookie
|
||||
static setCookie() async {
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
var cookiePath = await Utils.getCookiePath();
|
||||
var cookieJar = PersistCookieJar(
|
||||
ignoreExpires: true,
|
||||
@ -39,7 +30,8 @@ class Request {
|
||||
dio.interceptors.add(cookieManager);
|
||||
var cookie = await cookieManager.cookieJar
|
||||
.loadForRequest(Uri.parse(HttpString.baseUrl));
|
||||
if (user.get(UserBoxKey.userMid) != null) {
|
||||
var userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo != null && userInfo.mid != null) {
|
||||
var cookie2 = await cookieManager.cookieJar
|
||||
.loadForRequest(Uri.parse(HttpString.tUrl));
|
||||
if (cookie2.isEmpty) {
|
||||
@ -49,6 +41,7 @@ class Request {
|
||||
log("setCookie, ${e.toString()}");
|
||||
}
|
||||
}
|
||||
setOptionsHeaders(userInfo);
|
||||
}
|
||||
|
||||
if (cookie.isEmpty) {
|
||||
@ -63,23 +56,10 @@ class Request {
|
||||
dio.options.headers['cookie'] = cookieString;
|
||||
}
|
||||
|
||||
// 移除cookie
|
||||
static removeCookie() async {
|
||||
await cookieManager.cookieJar
|
||||
.saveFromResponse(Uri.parse(HttpString.baseUrl), []);
|
||||
await cookieManager.cookieJar
|
||||
.saveFromResponse(Uri.parse(HttpString.baseApiUrl), []);
|
||||
cookieManager.cookieJar.deleteAll();
|
||||
dio.interceptors.add(cookieManager);
|
||||
}
|
||||
|
||||
// 从cookie中获取 csrf token
|
||||
static Future<String> getCsrf() async {
|
||||
var cookies = await cookieManager.cookieJar
|
||||
.loadForRequest(Uri.parse(HttpString.baseApiUrl));
|
||||
// for (var i in cookies) {
|
||||
// print(i);
|
||||
// }
|
||||
String token = '';
|
||||
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
|
||||
token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
|
||||
@ -87,6 +67,15 @@ class Request {
|
||||
return token;
|
||||
}
|
||||
|
||||
static setOptionsHeaders(userInfo) {
|
||||
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
|
||||
dio.options.headers['env'] = 'prod';
|
||||
dio.options.headers['app-key'] = 'android64';
|
||||
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
|
||||
dio.options.headers['x-bili-aurora-zone'] = 'sh001';
|
||||
dio.options.headers['referer'] = 'https://www.bilibili.com/';
|
||||
}
|
||||
|
||||
/*
|
||||
* config it and create
|
||||
*/
|
||||
@ -100,21 +89,19 @@ class Request {
|
||||
//响应流上前后两次接受到数据的间隔,单位为毫秒。
|
||||
receiveTimeout: const Duration(milliseconds: 12000),
|
||||
//Http请求头.
|
||||
headers: {
|
||||
// 'cookie': '',
|
||||
},
|
||||
headers: {},
|
||||
);
|
||||
|
||||
Box user = GStrorage.user;
|
||||
if (user.get(UserBoxKey.userMid) != null) {
|
||||
options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString();
|
||||
options.headers['env'] = 'prod';
|
||||
options.headers['app-key'] = 'android64';
|
||||
options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
|
||||
options.headers['x-bili-aurora-zone'] = 'sh001';
|
||||
options.headers['referer'] = 'https://www.bilibili.com/';
|
||||
}
|
||||
dio.options = options;
|
||||
dio = Dio(options)
|
||||
|
||||
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
|
||||
..httpClientAdapter = Http2Adapter(
|
||||
ConnectionManager(
|
||||
idleTimeout: const Duration(milliseconds: 10000),
|
||||
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
|
||||
),
|
||||
);
|
||||
|
||||
//添加拦截器
|
||||
dio.interceptors.add(ApiInterceptor());
|
||||
|
||||
@ -127,30 +114,26 @@ class Request {
|
||||
|
||||
dio.transformer = BackgroundTransformer();
|
||||
dio.options.validateStatus = (status) {
|
||||
return status! >= 200 && status < 300 || status == 304 || status == 302;
|
||||
return status! >= 200 && status < 300 ||
|
||||
HttpString.validateStatusCodes.contains(status);
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* get请求
|
||||
*/
|
||||
get(url, {data, cacheOptions, options, cancelToken, extra}) async {
|
||||
get(url, {data, options, cancelToken, extra}) async {
|
||||
Response response;
|
||||
Options options;
|
||||
String ua = 'pc';
|
||||
Options options = Options();
|
||||
ResponseType resType = ResponseType.json;
|
||||
if (extra != null) {
|
||||
ua = extra!['ua'] ?? 'pc';
|
||||
resType = extra!['resType'] ?? ResponseType.json;
|
||||
if (extra['ua'] != null) {
|
||||
options.headers = {'user-agent': headerUa(type: extra['ua'])};
|
||||
}
|
||||
}
|
||||
if (cacheOptions != null) {
|
||||
cacheOptions.headers = {'user-agent': headerUa(ua)};
|
||||
options = cacheOptions;
|
||||
} else {
|
||||
options = Options();
|
||||
options.headers = {'user-agent': headerUa(ua)};
|
||||
options.responseType = resType;
|
||||
}
|
||||
options.responseType = resType;
|
||||
|
||||
try {
|
||||
response = await dio.get(
|
||||
url,
|
||||
@ -217,15 +200,19 @@ class Request {
|
||||
token.cancel("cancelled");
|
||||
}
|
||||
|
||||
String headerUa(ua) {
|
||||
String headerUa({type = 'mob'}) {
|
||||
String headerUa = '';
|
||||
if (ua == 'mob') {
|
||||
headerUa = Platform.isIOS
|
||||
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
|
||||
: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
|
||||
if (type == 'mob') {
|
||||
if (Platform.isIOS) {
|
||||
headerUa =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1';
|
||||
} else {
|
||||
headerUa =
|
||||
'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36';
|
||||
}
|
||||
} else {
|
||||
headerUa =
|
||||
'Mozilla/5.0 (MaciMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
|
||||
'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';
|
||||
}
|
||||
return headerUa;
|
||||
}
|
||||
|
@ -17,8 +17,6 @@ class ApiInterceptor extends Interceptor {
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
Box setting = GStrorage.setting;
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
try {
|
||||
@ -29,7 +27,11 @@ class ApiInterceptor extends Interceptor {
|
||||
final uri = Uri.parse(locations.first);
|
||||
final accessKey = uri.queryParameters['access_key'];
|
||||
final mid = uri.queryParameters['mid'];
|
||||
setting.put(UserBoxKey.accessKey, {'mid': mid, 'value': accessKey});
|
||||
try {
|
||||
Box localCache = GStrorage.localCache;
|
||||
localCache.put(
|
||||
LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -44,7 +46,10 @@ class ApiInterceptor extends Interceptor {
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// 处理网络请求错误
|
||||
// handler.next(err);
|
||||
SmartDialog.showToast(await dioError(err));
|
||||
SmartDialog.showToast(
|
||||
await dioError(err),
|
||||
displayType: SmartToastType.onlyRefresh,
|
||||
);
|
||||
super.onError(err, handler);
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:pilipala/http/index.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
import 'package:pilipala/models/member/archive.dart';
|
||||
import 'package:pilipala/models/member/info.dart';
|
||||
import 'package:pilipala/models/member/tags.dart';
|
||||
import 'package:pilipala/utils/wbi_sign.dart';
|
||||
|
||||
class MemberHttp {
|
||||
@ -18,6 +20,7 @@ class MemberHttp {
|
||||
var res = await Request().get(
|
||||
Api.memberInfo,
|
||||
data: params,
|
||||
extra: {'ua': 'pc'},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
@ -65,7 +68,7 @@ class MemberHttp {
|
||||
int ps = 30,
|
||||
int tid = 0,
|
||||
int? pn,
|
||||
String keyword = '',
|
||||
String? keyword,
|
||||
String order = 'pubdate',
|
||||
bool orderAvoided = true,
|
||||
}) async {
|
||||
@ -74,7 +77,7 @@ class MemberHttp {
|
||||
'ps': ps,
|
||||
'tid': tid,
|
||||
'pn': pn,
|
||||
'keyword': keyword,
|
||||
'keyword': keyword ?? '',
|
||||
'order': order,
|
||||
'platform': 'web',
|
||||
'web_location': 1550101,
|
||||
@ -83,6 +86,7 @@ class MemberHttp {
|
||||
var res = await Request().get(
|
||||
Api.memberArchive,
|
||||
data: params,
|
||||
extra: {'ua': 'pc'},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
@ -119,4 +123,96 @@ class MemberHttp {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索用户动态
|
||||
static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
|
||||
var res = await Request().get(Api.memberDynamic, data: {
|
||||
'keyword': '海拔',
|
||||
'mid': mid,
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
'platform': 'web'
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': DynamicsDataModel.fromJson(res.data['data']),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 查询分组
|
||||
static Future followUpTags() async {
|
||||
var res = await Request().get(Api.followUpTag);
|
||||
if (res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data']
|
||||
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
|
||||
.toList()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组
|
||||
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
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': [], 'msg': '操作成功'};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取某分组下的up
|
||||
static Future followUpGroup(
|
||||
int? mid,
|
||||
int? tagid,
|
||||
int? pn,
|
||||
int? ps,
|
||||
) async {
|
||||
var res = await Request().get(Api.followUpGroup, data: {
|
||||
'mid': mid,
|
||||
'tagid': tagid,
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
// FollowItemModel
|
||||
return {
|
||||
'status': true,
|
||||
'data': res.data['data']
|
||||
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
|
||||
.toList()
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ class ReplyHttp {
|
||||
Map errMap = {
|
||||
-400: '请求错误',
|
||||
-404: '无此项',
|
||||
12002: '当前页面评论功能已关闭"',
|
||||
12002: '当前页面评论功能已关闭',
|
||||
12009: '评论主体的type不合法',
|
||||
12061: 'UP主已关闭评论区',
|
||||
};
|
||||
|
@ -1,25 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/index.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/models/search/hot.dart';
|
||||
import 'package:pilipala/models/search/result.dart';
|
||||
import 'package:pilipala/models/search/suggest.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class SearchHttp {
|
||||
static Box setting = GStrorage.setting;
|
||||
static Future hotSearchList() async {
|
||||
var res = await Request().get(Api.hotSearchList);
|
||||
if (res.data['code'] == 0) {
|
||||
if (res.data is String) {
|
||||
Map<String, dynamic> resultMap = json.decode(res.data);
|
||||
if (resultMap['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': HotSearchModel.fromJson(resultMap),
|
||||
};
|
||||
}
|
||||
} else if (res.data is Map<String, dynamic> && res.data['code'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': HotSearchModel.fromJson(res.data),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': '请求错误 🙅',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': '请求错误 🙅',
|
||||
};
|
||||
}
|
||||
|
||||
// 获取搜索建议
|
||||
@ -27,10 +40,14 @@ class SearchHttp {
|
||||
var res = await Request().get(Api.serachSuggest,
|
||||
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
|
||||
if (res.data['code'] == 0) {
|
||||
res.data['result']['term'] = term;
|
||||
if (res.data['result'] is Map) {
|
||||
res.data['result']['term'] = term;
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': SearchSuggestModel.fromJson(res.data['result']),
|
||||
'data': res.data['result'] is Map
|
||||
? SearchSuggestModel.fromJson(res.data['result'])
|
||||
: [],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@ -46,39 +63,59 @@ class SearchHttp {
|
||||
required SearchType searchType,
|
||||
required String keyword,
|
||||
required page,
|
||||
String? order,
|
||||
int? duration,
|
||||
}) async {
|
||||
var res = await Request().get(Api.searchByType, data: {
|
||||
var reqData = {
|
||||
'search_type': searchType.type,
|
||||
'keyword': keyword,
|
||||
// 'order_sort': 0,
|
||||
// 'user_type': 0,
|
||||
'page': page
|
||||
});
|
||||
'page': page,
|
||||
if (order != null) 'order': order,
|
||||
if (duration != null) 'duration': duration,
|
||||
};
|
||||
var res = await Request().get(Api.searchByType, data: reqData);
|
||||
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
|
||||
Object data;
|
||||
switch (searchType) {
|
||||
case SearchType.video:
|
||||
data = SearchVideoModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.live_room:
|
||||
data = SearchLiveModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.bili_user:
|
||||
data = SearchUserModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.media_bangumi:
|
||||
data = SearchMBangumiModel.fromJson(res.data['data']);
|
||||
break;
|
||||
try {
|
||||
switch (searchType) {
|
||||
case SearchType.video:
|
||||
List<int> blackMidsList =
|
||||
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
|
||||
for (var i in res.data['data']['result']) {
|
||||
// 屏蔽推广和拉黑用户
|
||||
i['available'] = !blackMidsList.contains(i['mid']);
|
||||
}
|
||||
data = SearchVideoModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.live_room:
|
||||
data = SearchLiveModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.bili_user:
|
||||
data = SearchUserModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.media_bangumi:
|
||||
data = SearchMBangumiModel.fromJson(res.data['data']);
|
||||
break;
|
||||
case SearchType.article:
|
||||
data = SearchArticleModel.fromJson(res.data['data']);
|
||||
break;
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': data,
|
||||
};
|
||||
} catch (err) {
|
||||
print(err);
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': data,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅',
|
||||
'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
|
||||
? '没有相关数据'
|
||||
: res.data['message'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/http/api.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
@ -7,6 +8,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/models/user/history.dart';
|
||||
import 'package:pilipala/models/user/info.dart';
|
||||
import 'package:pilipala/models/user/stat.dart';
|
||||
import 'package:pilipala/utils/wbi_sign.dart';
|
||||
|
||||
class UserHttp {
|
||||
static Future<dynamic> userStat({required int mid}) async {
|
||||
@ -50,10 +52,17 @@ class UserHttp {
|
||||
'up_mid': mid,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
FavFolderData data = FavFolderData.fromJson(res.data['data']);
|
||||
return {'status': true, 'data': data};
|
||||
late FavFolderData data;
|
||||
if (res.data['data'] != null) {
|
||||
data = FavFolderData.fromJson(res.data['data']);
|
||||
return {'status': true, 'data': data};
|
||||
}
|
||||
} else {
|
||||
return {'status': false, 'data': [], 'msg': '账号未登录'};
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': res.data['message'] ?? '账号未登录'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,14 +71,15 @@ class UserHttp {
|
||||
required int pn,
|
||||
required int ps,
|
||||
String keyword = '',
|
||||
String order = 'mtime'}) async {
|
||||
String order = 'mtime',
|
||||
int type = 0}) async {
|
||||
var res = await Request().get(Api.userFavFolderDetail, data: {
|
||||
'media_id': mediaId,
|
||||
'pn': pn,
|
||||
'ps': ps,
|
||||
'keyword': keyword,
|
||||
'order': order,
|
||||
'type': 0,
|
||||
'type': type,
|
||||
'tid': 0,
|
||||
'platform': 'web'
|
||||
});
|
||||
@ -85,6 +95,12 @@ class UserHttp {
|
||||
static Future<dynamic> seeYouLater() async {
|
||||
var res = await Request().get(Api.seeYouLater);
|
||||
if (res.data['code'] == 0) {
|
||||
if (res.data['data']['count'] == 0) {
|
||||
return {
|
||||
'status': true,
|
||||
'data': {'list': [], 'count': 0}
|
||||
};
|
||||
}
|
||||
List<HotVideoItemModel> list = [];
|
||||
for (var i in res.data['data']['list']) {
|
||||
list.add(HotVideoItemModel.fromJson(i));
|
||||
@ -165,14 +181,16 @@ class UserHttp {
|
||||
}
|
||||
|
||||
// 移除已观看
|
||||
static Future toViewDel() async {
|
||||
static Future toViewDel({int? aid}) async {
|
||||
final Map<String, dynamic> params = {
|
||||
'jsonp': 'jsonp',
|
||||
'csrf': await Request.getCsrf(),
|
||||
};
|
||||
|
||||
params[aid != null ? 'aid' : 'viewed'] = aid ?? true;
|
||||
var res = await Request().post(
|
||||
Api.toViewDel,
|
||||
queryParameters: {
|
||||
'jsonp': 'jsonp',
|
||||
'viewed': true,
|
||||
'csrf': await Request.getCsrf(),
|
||||
},
|
||||
queryParameters: params,
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'msg': 'yeah!成功移除'};
|
||||
@ -191,8 +209,88 @@ class UserHttp {
|
||||
'sign': Constants.thirdSign,
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
|
||||
Request().get(res.data['data']['confirm_uri']);
|
||||
try {
|
||||
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
|
||||
Request().get(res.data['data']['confirm_uri']);
|
||||
}
|
||||
} catch (err) {
|
||||
SmartDialog.showNotify(msg: '获取用户凭证: $err', notifyType: NotifyType.error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空稍后再看
|
||||
static Future toViewClear() async {
|
||||
var res = await Request().post(
|
||||
Api.toViewClear,
|
||||
queryParameters: {
|
||||
'jsonp': 'jsonp',
|
||||
'csrf': await Request.getCsrf(),
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'msg': '操作完成'};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 删除历史记录
|
||||
static Future delHistory(kid) async {
|
||||
var res = await Request().post(
|
||||
Api.delHistory,
|
||||
queryParameters: {
|
||||
'kid': kid,
|
||||
'jsonp': 'jsonp',
|
||||
'csrf': await Request.getCsrf(),
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'msg': '已删除'};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 相互关系查询
|
||||
static Future relationSearch(int mid) async {
|
||||
Map params = await WbiSign().makSign({
|
||||
'mid': mid,
|
||||
'token': '',
|
||||
'platform': 'web',
|
||||
'web_location': 1550101,
|
||||
});
|
||||
var res = await Request().get(
|
||||
Api.relationSearch,
|
||||
data: {
|
||||
'mid': mid,
|
||||
'w_rid': params['w_rid'],
|
||||
'wts': params['wts'],
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
// relation 主动状态
|
||||
// 被动状态
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索历史记录
|
||||
static Future searchHistory(
|
||||
{required int pn, required String keyword}) async {
|
||||
var res = await Request().get(
|
||||
Api.searchHistory,
|
||||
data: {
|
||||
'pn': pn,
|
||||
'keyword': keyword,
|
||||
'business': 'all',
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
|
||||
} else {
|
||||
return {'status': false, 'msg': res.data['message']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,11 @@ import 'package:pilipala/utils/storage.dart';
|
||||
/// 返回{'status': bool, 'data': List}
|
||||
/// view层根据 status 判断渲染逻辑
|
||||
class VideoHttp {
|
||||
static Box localCache = GStrorage.localCache;
|
||||
static Box setting = GStrorage.setting;
|
||||
static bool enableRcmdDynamic =
|
||||
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
|
||||
static Box userInfoCache = GStrorage.userInfo;
|
||||
|
||||
// 首页推荐视频
|
||||
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
|
||||
@ -60,8 +64,9 @@ class VideoHttp {
|
||||
'device_name': 'vivo',
|
||||
'pull': freshIdx == 0 ? 'true' : 'false',
|
||||
'appkey': Constants.appKey,
|
||||
'access_key':
|
||||
setting.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? ''
|
||||
'access_key': localCache
|
||||
.get(LocalCacheKey.accessKey, defaultValue: {})['value'] ??
|
||||
''
|
||||
},
|
||||
);
|
||||
if (res.data['code'] == 0) {
|
||||
@ -71,6 +76,7 @@ class VideoHttp {
|
||||
for (var i in res.data['data']['items']) {
|
||||
// 屏蔽推广和拉黑用户
|
||||
if (i['card_goto'] != 'ad_av' &&
|
||||
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
|
||||
(i['args'] != null &&
|
||||
!blackMidsList.contains(i['args']['up_mid']))) {
|
||||
list.add(RecVideoItemAppModel.fromJson(i));
|
||||
@ -128,6 +134,11 @@ class VideoHttp {
|
||||
// 'platform': '',
|
||||
// 'high_quality': ''
|
||||
};
|
||||
// 免登录查看1080p
|
||||
if (userInfoCache.get('userInfoCache') == null &&
|
||||
setting.get(SettingBoxKey.p1080, defaultValue: true)) {
|
||||
data['try_look'] = 1;
|
||||
}
|
||||
try {
|
||||
var res = await Request().get(Api.videoUrl, data: data);
|
||||
if (res.data['code'] == 0) {
|
||||
@ -136,7 +147,12 @@ class VideoHttp {
|
||||
'data': PlayUrlModel.fromJson(res.data['data'])
|
||||
};
|
||||
} else {
|
||||
return {'status': false, 'data': []};
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'code': res.data['code'],
|
||||
'msg': res.data['message'],
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return {'status': false, 'data': [], 'msg': err};
|
||||
@ -153,13 +169,14 @@ class VideoHttp {
|
||||
Map errMap = {
|
||||
-400: '请求错误',
|
||||
-403: '权限不足',
|
||||
-404: '无视频',
|
||||
-404: '视频资源失效',
|
||||
62002: '稿件不可见',
|
||||
62004: '稿件审核中',
|
||||
};
|
||||
return {
|
||||
'status': false,
|
||||
'data': null,
|
||||
'code': result.code,
|
||||
'msg': errMap[result.code] ?? '请求异常',
|
||||
};
|
||||
}
|
||||
@ -391,4 +408,16 @@ class VideoHttp {
|
||||
return {'status': false, 'msg': res.data['result']['toast']};
|
||||
}
|
||||
}
|
||||
|
||||
// 查看视频同时在看人数
|
||||
static Future onlineTotal({int? aid, String? bvid, int? cid}) async {
|
||||
var res = await Request().get(Api.onlineTotal, data: {
|
||||
'aid': aid,
|
||||
'bvid': bvid,
|
||||
'cid': cid,
|
||||
});
|
||||
if (res.data['code'] == 0) {
|
||||
return {'status': true, 'data': res.data['data']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -7,11 +10,13 @@ import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/custom_toast.dart';
|
||||
import 'package:pilipala/http/init.dart';
|
||||
import 'package:pilipala/models/common/color_type.dart';
|
||||
import 'package:pilipala/models/common/theme_type.dart';
|
||||
import 'package:pilipala/pages/search/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/router/app_pages.dart';
|
||||
import 'package:pilipala/pages/main/view.dart';
|
||||
import 'package:pilipala/utils/app_scheme.dart';
|
||||
import 'package:pilipala/utils/data.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
|
||||
@ -24,9 +29,17 @@ void main() async {
|
||||
.then((_) async {
|
||||
await GStrorage.init();
|
||||
runApp(const MyApp());
|
||||
// 小白条、导航栏沉浸
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
systemNavigationBarDividerColor: Colors.transparent,
|
||||
statusBarColor: Colors.transparent,
|
||||
));
|
||||
await Request.setCookie();
|
||||
await Data.init();
|
||||
await GStrorage.lazyInit();
|
||||
Data.init();
|
||||
GStrorage.lazyInit();
|
||||
PiliSchame.init();
|
||||
});
|
||||
}
|
||||
|
||||
@ -35,15 +48,44 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color brandColor = const Color.fromARGB(255, 92, 182, 123);
|
||||
Box setting = GStrorage.setting;
|
||||
// 主题色
|
||||
Color defaultColor =
|
||||
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
|
||||
['color'];
|
||||
Color brandColor = defaultColor;
|
||||
// 主题模式
|
||||
ThemeType currentThemeValue = ThemeType.values[setting
|
||||
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
|
||||
// 是否动态取色
|
||||
bool isDynamicColor =
|
||||
setting.get(SettingBoxKey.dynamicColor, defaultValue: true);
|
||||
// 字体缩放大小
|
||||
double textScale =
|
||||
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
|
||||
|
||||
// 强制设置高帧率
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
late List modes;
|
||||
FlutterDisplayMode.supported.then((value) {
|
||||
modes = value;
|
||||
var storageDisplay = setting.get(SettingBoxKey.displayMode);
|
||||
DisplayMode f = DisplayMode.auto;
|
||||
if (storageDisplay != null) {
|
||||
f = modes.firstWhere((e) => e.toString() == storageDisplay);
|
||||
}
|
||||
DisplayMode preferred = modes.toList().firstWhere((el) => el == f);
|
||||
FlutterDisplayMode.setPreferredMode(preferred);
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return DynamicColorBuilder(
|
||||
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||
ColorScheme? lightColorScheme;
|
||||
ColorScheme? darkColorScheme;
|
||||
if (lightDynamic != null && darkDynamic != null) {
|
||||
if (lightDynamic != null && darkDynamic != null && isDynamicColor) {
|
||||
// dynamic取色成功
|
||||
lightColorScheme = lightDynamic.harmonized();
|
||||
darkColorScheme = darkDynamic.harmonized();
|
||||
@ -93,9 +135,17 @@ class MyApp extends StatelessWidget {
|
||||
fallbackLocale: const Locale("zh", "CN"),
|
||||
getPages: Routes.getPages,
|
||||
home: const MainApp(),
|
||||
builder: FlutterSmartDialog.init(
|
||||
toastBuilder: (String msg) => CustomToast(msg: msg),
|
||||
),
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
return FlutterSmartDialog(
|
||||
toastBuilder: (String msg) => CustomToast(msg: msg),
|
||||
child: MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
textScaleFactor:
|
||||
MediaQuery.of(context).textScaleFactor * textScale),
|
||||
child: child!,
|
||||
),
|
||||
);
|
||||
},
|
||||
navigatorObservers: [
|
||||
VideoDetailPage.routeObserver,
|
||||
SearchPage.routeObserver,
|
||||
|
23
lib/models/common/color_type.dart
Normal file
23
lib/models/common/color_type.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final List<Map<String, dynamic>> colorThemeTypes = [
|
||||
{'color': const Color.fromARGB(255, 92, 182, 123), 'label': '默认绿'},
|
||||
{'color': Colors.pink, 'label': '粉红色'},
|
||||
{'color': Colors.red, 'label': '红色'},
|
||||
{'color': Colors.orange, 'label': '橙色'},
|
||||
{'color': Colors.amber, 'label': '琥珀色'},
|
||||
{'color': Colors.yellow, 'label': '黄色'},
|
||||
{'color': Colors.lime, 'label': '酸橙色'},
|
||||
{'color': Colors.lightGreen, 'label': '浅绿色'},
|
||||
{'color': Colors.green, 'label': '绿色'},
|
||||
{'color': Colors.teal, 'label': '青色'},
|
||||
{'color': Colors.cyan, 'label': '蓝绿色'},
|
||||
{'color': Colors.lightBlue, 'label': '浅蓝色'},
|
||||
{'color': Colors.blue, 'label': '蓝色'},
|
||||
{'color': Colors.indigo, 'label': '靛蓝色'},
|
||||
{'color': Colors.purple, 'label': '紫色'},
|
||||
{'color': Colors.deepPurple, 'label': '深紫色'},
|
||||
{'color': Colors.blueGrey, 'label': '蓝灰色'},
|
||||
{'color': Colors.brown, 'label': '棕色'},
|
||||
{'color': Colors.grey, 'label': '灰色'},
|
||||
];
|
@ -12,18 +12,35 @@ enum SearchType {
|
||||
live_room,
|
||||
// 主播:live_user
|
||||
// live_user,
|
||||
// 专栏:article
|
||||
// article,
|
||||
// 话题:topic
|
||||
// topic,
|
||||
// 用户:bili_user
|
||||
bili_user,
|
||||
// 专栏:article
|
||||
article,
|
||||
// 相簿:photo
|
||||
// photo
|
||||
}
|
||||
|
||||
extension SearchTypeExtension on SearchType {
|
||||
String get type =>
|
||||
['video', 'media_bangumi', 'live_room', 'bili_user'][index];
|
||||
String get label => ['视频', '番剧', '直播间', '用户'][index];
|
||||
['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
|
||||
String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
|
||||
}
|
||||
|
||||
// 搜索类型为视频、专栏及相簿时
|
||||
enum ArchiveFilterType {
|
||||
totalrank,
|
||||
click,
|
||||
pubdate,
|
||||
dm,
|
||||
stow,
|
||||
scores,
|
||||
// 专栏
|
||||
// attention,
|
||||
}
|
||||
|
||||
extension ArchiveFilterTypeExtension on ArchiveFilterType {
|
||||
String get description =>
|
||||
['默认排序', '播放多', '新发布', '弹幕多', '收藏多', '评论多', '最多喜欢'][index];
|
||||
}
|
||||
|
10194
lib/models/danmaku/dm.pb.dart
Normal file
10194
lib/models/danmaku/dm.pb.dart
Normal file
File diff suppressed because it is too large
Load Diff
347
lib/models/danmaku/dm.pbenum.dart
Normal file
347
lib/models/danmaku/dm.pbenum.dart
Normal file
@ -0,0 +1,347 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: dm.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
|
||||
|
||||
// ignore_for_file: UNDEFINED_SHOWN_NAME
|
||||
import 'dart:core' as $core;
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
class AvatarType extends $pb.ProtobufEnum {
|
||||
static const AvatarType AvatarTypeNone = AvatarType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'AvatarTypeNone');
|
||||
static const AvatarType AvatarTypeNFT = AvatarType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'AvatarTypeNFT');
|
||||
|
||||
static const $core.List<AvatarType> values = <AvatarType>[
|
||||
AvatarTypeNone,
|
||||
AvatarTypeNFT,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, AvatarType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static AvatarType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const AvatarType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class BubbleType extends $pb.ProtobufEnum {
|
||||
static const BubbleType BubbleTypeNone = BubbleType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'BubbleTypeNone');
|
||||
static const BubbleType BubbleTypeClickButton = BubbleType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'BubbleTypeClickButton');
|
||||
static const BubbleType BubbleTypeDmSettingPanel = BubbleType._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'BubbleTypeDmSettingPanel');
|
||||
|
||||
static const $core.List<BubbleType> values = <BubbleType>[
|
||||
BubbleTypeNone,
|
||||
BubbleTypeClickButton,
|
||||
BubbleTypeDmSettingPanel,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, BubbleType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static BubbleType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const BubbleType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class CheckboxType extends $pb.ProtobufEnum {
|
||||
static const CheckboxType CheckboxTypeNone = CheckboxType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'CheckboxTypeNone');
|
||||
static const CheckboxType CheckboxTypeEncourage = CheckboxType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'CheckboxTypeEncourage');
|
||||
static const CheckboxType CheckboxTypeColorDM = CheckboxType._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'CheckboxTypeColorDM');
|
||||
|
||||
static const $core.List<CheckboxType> values = <CheckboxType>[
|
||||
CheckboxTypeNone,
|
||||
CheckboxTypeEncourage,
|
||||
CheckboxTypeColorDM,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, CheckboxType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static CheckboxType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const CheckboxType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class DMAttrBit extends $pb.ProtobufEnum {
|
||||
static const DMAttrBit DMAttrBitProtect = DMAttrBit._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'DMAttrBitProtect');
|
||||
static const DMAttrBit DMAttrBitFromLive = DMAttrBit._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'DMAttrBitFromLive');
|
||||
static const DMAttrBit DMAttrHighLike = DMAttrBit._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'DMAttrHighLike');
|
||||
|
||||
static const $core.List<DMAttrBit> values = <DMAttrBit>[
|
||||
DMAttrBitProtect,
|
||||
DMAttrBitFromLive,
|
||||
DMAttrHighLike,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, DMAttrBit> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static DMAttrBit? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const DMAttrBit._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class ExposureType extends $pb.ProtobufEnum {
|
||||
static const ExposureType ExposureTypeNone = ExposureType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'ExposureTypeNone');
|
||||
static const ExposureType ExposureTypeDMSend = ExposureType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'ExposureTypeDMSend');
|
||||
|
||||
static const $core.List<ExposureType> values = <ExposureType>[
|
||||
ExposureTypeNone,
|
||||
ExposureTypeDMSend,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ExposureType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static ExposureType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const ExposureType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class PostPanelBizType extends $pb.ProtobufEnum {
|
||||
static const PostPanelBizType PostPanelBizTypeNone = PostPanelBizType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostPanelBizTypeNone');
|
||||
static const PostPanelBizType PostPanelBizTypeEncourage = PostPanelBizType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostPanelBizTypeEncourage');
|
||||
static const PostPanelBizType PostPanelBizTypeColorDM = PostPanelBizType._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostPanelBizTypeColorDM');
|
||||
static const PostPanelBizType PostPanelBizTypeNFTDM = PostPanelBizType._(
|
||||
3,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostPanelBizTypeNFTDM');
|
||||
static const PostPanelBizType PostPanelBizTypeFragClose = PostPanelBizType._(
|
||||
4,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostPanelBizTypeFragClose');
|
||||
static const PostPanelBizType PostPanelBizTypeRecommend = PostPanelBizType._(
|
||||
5,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostPanelBizTypeRecommend');
|
||||
|
||||
static const $core.List<PostPanelBizType> values = <PostPanelBizType>[
|
||||
PostPanelBizTypeNone,
|
||||
PostPanelBizTypeEncourage,
|
||||
PostPanelBizTypeColorDM,
|
||||
PostPanelBizTypeNFTDM,
|
||||
PostPanelBizTypeFragClose,
|
||||
PostPanelBizTypeRecommend,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, PostPanelBizType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static PostPanelBizType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const PostPanelBizType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class PostStatus extends $pb.ProtobufEnum {
|
||||
static const PostStatus PostStatusNormal = PostStatus._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostStatusNormal');
|
||||
static const PostStatus PostStatusClosed = PostStatus._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'PostStatusClosed');
|
||||
|
||||
static const $core.List<PostStatus> values = <PostStatus>[
|
||||
PostStatusNormal,
|
||||
PostStatusClosed,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, PostStatus> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static PostStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const PostStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class RenderType extends $pb.ProtobufEnum {
|
||||
static const RenderType RenderTypeNone = RenderType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'RenderTypeNone');
|
||||
static const RenderType RenderTypeSingle = RenderType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'RenderTypeSingle');
|
||||
static const RenderType RenderTypeRotation = RenderType._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'RenderTypeRotation');
|
||||
|
||||
static const $core.List<RenderType> values = <RenderType>[
|
||||
RenderTypeNone,
|
||||
RenderTypeSingle,
|
||||
RenderTypeRotation,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, RenderType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static RenderType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const RenderType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SubtitleAiStatus extends $pb.ProtobufEnum {
|
||||
static const SubtitleAiStatus None = SubtitleAiStatus._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'None');
|
||||
static const SubtitleAiStatus Exposure = SubtitleAiStatus._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'Exposure');
|
||||
static const SubtitleAiStatus Assist = SubtitleAiStatus._(
|
||||
2,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'Assist');
|
||||
|
||||
static const $core.List<SubtitleAiStatus> values = <SubtitleAiStatus>[
|
||||
None,
|
||||
Exposure,
|
||||
Assist,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SubtitleAiStatus> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static SubtitleAiStatus? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SubtitleAiStatus._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SubtitleAiType extends $pb.ProtobufEnum {
|
||||
static const SubtitleAiType Normal = SubtitleAiType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'Normal');
|
||||
static const SubtitleAiType Translate = SubtitleAiType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'Translate');
|
||||
|
||||
static const $core.List<SubtitleAiType> values = <SubtitleAiType>[
|
||||
Normal,
|
||||
Translate,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SubtitleAiType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static SubtitleAiType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SubtitleAiType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class SubtitleType extends $pb.ProtobufEnum {
|
||||
static const SubtitleType CC = SubtitleType._(0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CC');
|
||||
static const SubtitleType AI = SubtitleType._(1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AI');
|
||||
|
||||
static const $core.List<SubtitleType> values = <SubtitleType>[
|
||||
CC,
|
||||
AI,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, SubtitleType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static SubtitleType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const SubtitleType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
||||
|
||||
class ToastFunctionType extends $pb.ProtobufEnum {
|
||||
static const ToastFunctionType ToastFunctionTypeNone = ToastFunctionType._(
|
||||
0,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'ToastFunctionTypeNone');
|
||||
static const ToastFunctionType ToastFunctionTypePostPanel =
|
||||
ToastFunctionType._(
|
||||
1,
|
||||
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
|
||||
? ''
|
||||
: 'ToastFunctionTypePostPanel');
|
||||
|
||||
static const $core.List<ToastFunctionType> values = <ToastFunctionType>[
|
||||
ToastFunctionTypeNone,
|
||||
ToastFunctionTypePostPanel,
|
||||
];
|
||||
|
||||
static final $core.Map<$core.int, ToastFunctionType> _byValue =
|
||||
$pb.ProtobufEnum.initByValue(values);
|
||||
static ToastFunctionType? valueOf($core.int value) => _byValue[value];
|
||||
|
||||
const ToastFunctionType._($core.int v, $core.String n) : super(v, n);
|
||||
}
|
2318
lib/models/danmaku/dm.pbjson.dart
Normal file
2318
lib/models/danmaku/dm.pbjson.dart
Normal file
File diff suppressed because it is too large
Load Diff
74
lib/models/danmaku/dm.pbserver.dart
Normal file
74
lib/models/danmaku/dm.pbserver.dart
Normal file
@ -0,0 +1,74 @@
|
||||
///
|
||||
// Generated code. Do not modify.
|
||||
// source: dm.proto
|
||||
//
|
||||
// @dart = 2.12
|
||||
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, avoid_renaming_method_parameters
|
||||
|
||||
import 'dart:async' as $async;
|
||||
|
||||
import 'package:protobuf/protobuf.dart' as $pb;
|
||||
|
||||
import 'dart:core' as $core;
|
||||
import 'dm.pb.dart' as $0;
|
||||
import 'dm.pbjson.dart';
|
||||
|
||||
export 'dm.pb.dart';
|
||||
|
||||
abstract class DMServiceBase extends $pb.GeneratedService {
|
||||
$async.Future<$0.DmSegMobileReply> dmSegMobile(
|
||||
$pb.ServerContext ctx, $0.DmSegMobileReq request);
|
||||
$async.Future<$0.DmViewReply> dmView(
|
||||
$pb.ServerContext ctx, $0.DmViewReq request);
|
||||
$async.Future<$0.Response> dmPlayerConfig(
|
||||
$pb.ServerContext ctx, $0.DmPlayerConfigReq request);
|
||||
$async.Future<$0.DmSegOttReply> dmSegOtt(
|
||||
$pb.ServerContext ctx, $0.DmSegOttReq request);
|
||||
$async.Future<$0.DmSegSDKReply> dmSegSDK(
|
||||
$pb.ServerContext ctx, $0.DmSegSDKReq request);
|
||||
$async.Future<$0.DmExpoReportRes> dmExpoReport(
|
||||
$pb.ServerContext ctx, $0.DmExpoReportReq request);
|
||||
|
||||
$pb.GeneratedMessage createRequest($core.String method) {
|
||||
switch (method) {
|
||||
case 'DmSegMobile':
|
||||
return $0.DmSegMobileReq();
|
||||
case 'DmView':
|
||||
return $0.DmViewReq();
|
||||
case 'DmPlayerConfig':
|
||||
return $0.DmPlayerConfigReq();
|
||||
case 'DmSegOtt':
|
||||
return $0.DmSegOttReq();
|
||||
case 'DmSegSDK':
|
||||
return $0.DmSegSDKReq();
|
||||
case 'DmExpoReport':
|
||||
return $0.DmExpoReportReq();
|
||||
default:
|
||||
throw $core.ArgumentError('Unknown method: $method');
|
||||
}
|
||||
}
|
||||
|
||||
$async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx,
|
||||
$core.String method, $pb.GeneratedMessage request) {
|
||||
switch (method) {
|
||||
case 'DmSegMobile':
|
||||
return this.dmSegMobile(ctx, request as $0.DmSegMobileReq);
|
||||
case 'DmView':
|
||||
return this.dmView(ctx, request as $0.DmViewReq);
|
||||
case 'DmPlayerConfig':
|
||||
return this.dmPlayerConfig(ctx, request as $0.DmPlayerConfigReq);
|
||||
case 'DmSegOtt':
|
||||
return this.dmSegOtt(ctx, request as $0.DmSegOttReq);
|
||||
case 'DmSegSDK':
|
||||
return this.dmSegSDK(ctx, request as $0.DmSegSDKReq);
|
||||
case 'DmExpoReport':
|
||||
return this.dmExpoReport(ctx, request as $0.DmExpoReportReq);
|
||||
default:
|
||||
throw $core.ArgumentError('Unknown method: $method');
|
||||
}
|
||||
}
|
||||
|
||||
$core.Map<$core.String, $core.dynamic> get $json => DMServiceBase$json;
|
||||
$core.Map<$core.String, $core.Map<$core.String, $core.dynamic>>
|
||||
get $messageJson => DMServiceBase$messageJson;
|
||||
}
|
893
lib/models/danmaku/dm.proto
Normal file
893
lib/models/danmaku/dm.proto
Normal file
@ -0,0 +1,893 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package bilibili.community.service.dm.v1;
|
||||
|
||||
// 说明文档
|
||||
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/danmaku_proto.md
|
||||
|
||||
//弹幕
|
||||
service DM {
|
||||
// 获取分段弹幕
|
||||
rpc DmSegMobile (DmSegMobileReq) returns (DmSegMobileReply);
|
||||
// 客户端弹幕元数据 字幕、分段、防挡蒙版等
|
||||
rpc DmView(DmViewReq) returns (DmViewReply);
|
||||
// 修改弹幕配置
|
||||
rpc DmPlayerConfig (DmPlayerConfigReq) returns (Response);
|
||||
// ott弹幕列表
|
||||
rpc DmSegOtt(DmSegOttReq) returns(DmSegOttReply);
|
||||
// SDK弹幕列表
|
||||
rpc DmSegSDK(DmSegSDKReq) returns(DmSegSDKReply);
|
||||
//
|
||||
rpc DmExpoReport(DmExpoReportReq) returns (DmExpoReportRes);
|
||||
}
|
||||
|
||||
//
|
||||
message Avatar {
|
||||
//
|
||||
string id = 1;
|
||||
//
|
||||
string url = 2;
|
||||
//
|
||||
AvatarType avatar_type = 3;
|
||||
}
|
||||
|
||||
//
|
||||
enum AvatarType {
|
||||
AvatarTypeNone = 0; //
|
||||
AvatarTypeNFT = 1; //
|
||||
}
|
||||
|
||||
//
|
||||
message Bubble {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
string url = 2;
|
||||
}
|
||||
|
||||
//
|
||||
enum BubbleType {
|
||||
BubbleTypeNone = 0; //
|
||||
BubbleTypeClickButton = 1; //
|
||||
BubbleTypeDmSettingPanel = 2; //
|
||||
}
|
||||
|
||||
//
|
||||
message BubbleV2 {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
string url = 2;
|
||||
//
|
||||
BubbleType bubble_type = 3;
|
||||
//
|
||||
bool exposure_once = 4;
|
||||
//
|
||||
ExposureType exposure_type = 5;
|
||||
}
|
||||
|
||||
//
|
||||
message Button {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
int32 action = 2;
|
||||
}
|
||||
|
||||
//
|
||||
message BuzzwordConfig {
|
||||
//
|
||||
repeated BuzzwordShowConfig keywords = 1;
|
||||
}
|
||||
|
||||
//
|
||||
message BuzzwordShowConfig {
|
||||
//
|
||||
string name = 1;
|
||||
//
|
||||
string schema = 2;
|
||||
//
|
||||
int32 source = 3;
|
||||
//
|
||||
int64 id = 4;
|
||||
//
|
||||
int64 buzzword_id = 5;
|
||||
//
|
||||
int32 schema_type = 6;
|
||||
}
|
||||
|
||||
//
|
||||
message CheckBox {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
CheckboxType type = 2;
|
||||
//
|
||||
bool default_value = 3;
|
||||
//
|
||||
bool show = 4;
|
||||
}
|
||||
|
||||
//
|
||||
enum CheckboxType {
|
||||
CheckboxTypeNone = 0; //
|
||||
CheckboxTypeEncourage = 1; //
|
||||
CheckboxTypeColorDM = 2; //
|
||||
}
|
||||
|
||||
//
|
||||
message CheckBoxV2 {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
int32 type = 2;
|
||||
//
|
||||
bool default_value = 3;
|
||||
}
|
||||
|
||||
//
|
||||
message ClickButton {
|
||||
//
|
||||
repeated string portrait_text = 1;
|
||||
//
|
||||
repeated string landscape_text = 2;
|
||||
//
|
||||
repeated string portrait_text_focus = 3;
|
||||
//
|
||||
repeated string landscape_text_focus = 4;
|
||||
//
|
||||
RenderType render_type = 5;
|
||||
//
|
||||
bool show = 6;
|
||||
//
|
||||
Bubble bubble = 7;
|
||||
}
|
||||
|
||||
//
|
||||
message ClickButtonV2 {
|
||||
//
|
||||
repeated string portrait_text = 1;
|
||||
//
|
||||
repeated string landscape_text = 2;
|
||||
//
|
||||
repeated string portrait_text_focus = 3;
|
||||
//
|
||||
repeated string landscape_text_focus = 4;
|
||||
//
|
||||
int32 render_type = 5;
|
||||
//
|
||||
bool text_input_post = 6;
|
||||
//
|
||||
bool exposure_once = 7;
|
||||
//
|
||||
int32 exposure_type = 8;
|
||||
}
|
||||
|
||||
// 互动弹幕条目信息
|
||||
message CommandDm {
|
||||
// 弹幕id
|
||||
int64 id = 1;
|
||||
// 对象视频cid
|
||||
int64 oid = 2;
|
||||
// 发送者mid
|
||||
string mid = 3;
|
||||
// 互动弹幕指令
|
||||
string command = 4;
|
||||
// 互动弹幕正文
|
||||
string content = 5;
|
||||
// 出现时间
|
||||
int32 progress = 6;
|
||||
// 创建时间
|
||||
string ctime = 7;
|
||||
// 发布时间
|
||||
string mtime = 8;
|
||||
// 扩展json数据
|
||||
string extra = 9;
|
||||
// 弹幕id str类型
|
||||
string idStr = 10;
|
||||
}
|
||||
|
||||
// 弹幕ai云屏蔽列表
|
||||
message DanmakuAIFlag {
|
||||
// 弹幕ai云屏蔽条目
|
||||
repeated DanmakuFlag dm_flags = 1;
|
||||
}
|
||||
|
||||
// 弹幕条目
|
||||
message DanmakuElem {
|
||||
// 弹幕dmid
|
||||
int64 id = 1;
|
||||
// 弹幕出现位置(单位ms)
|
||||
int32 progress = 2;
|
||||
// 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
|
||||
int32 mode = 3;
|
||||
// 弹幕字号
|
||||
int32 fontsize = 4;
|
||||
// 弹幕颜色
|
||||
uint32 color = 5;
|
||||
// 发送者mid hash
|
||||
string midHash = 6;
|
||||
// 弹幕正文
|
||||
string content = 7;
|
||||
// 发送时间
|
||||
int64 ctime = 8;
|
||||
// 权重 用于屏蔽等级 区间:[1,10]
|
||||
int32 weight = 9;
|
||||
// 动作
|
||||
string action = 10;
|
||||
// 弹幕池 0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕)
|
||||
int32 pool = 11;
|
||||
// 弹幕dmid str
|
||||
string idStr = 12;
|
||||
// 弹幕属性位(bin求AND)
|
||||
// bit0:保护 bit1:直播 bit2:高赞
|
||||
int32 attr = 13;
|
||||
//
|
||||
string animation = 22;
|
||||
// 大会员专属颜色
|
||||
DmColorfulType colorful = 24;
|
||||
}
|
||||
|
||||
// 弹幕ai云屏蔽条目
|
||||
message DanmakuFlag {
|
||||
// 弹幕dmid
|
||||
int64 dmid = 1;
|
||||
// 评分
|
||||
uint32 flag = 2;
|
||||
}
|
||||
|
||||
// 云屏蔽配置信息
|
||||
message DanmakuFlagConfig {
|
||||
// 云屏蔽等级
|
||||
int32 rec_flag = 1;
|
||||
// 云屏蔽文案
|
||||
string rec_text = 2;
|
||||
// 云屏蔽开关
|
||||
int32 rec_switch = 3;
|
||||
}
|
||||
|
||||
// 弹幕默认配置
|
||||
message DanmuDefaultPlayerConfig {
|
||||
bool player_danmaku_use_default_config = 1; // 是否使用推荐弹幕设置
|
||||
bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽
|
||||
int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级
|
||||
bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕
|
||||
bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕
|
||||
bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕
|
||||
bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕
|
||||
bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕
|
||||
bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕
|
||||
float player_danmaku_opacity = 12; // 弹幕不透明度
|
||||
float player_danmaku_scalingfactor = 13; // 弹幕缩放比例
|
||||
float player_danmaku_domain = 14; // 弹幕显示区域
|
||||
int32 player_danmaku_speed = 15; // 弹幕速度
|
||||
bool inline_player_danmaku_switch = 16; // 是否开启弹幕
|
||||
int32 player_danmaku_senior_mode_switch = 17; //
|
||||
int32 player_danmaku_ai_recommended_level_v2 = 18; //
|
||||
map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 19; //
|
||||
}
|
||||
|
||||
// 弹幕配置
|
||||
message DanmuPlayerConfig {
|
||||
bool player_danmaku_switch = 1; // 是否开启弹幕
|
||||
bool player_danmaku_switch_save = 2; // 是否记录弹幕开关设置
|
||||
bool player_danmaku_use_default_config = 3; // 是否使用推荐弹幕设置
|
||||
bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽
|
||||
int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级
|
||||
bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕
|
||||
bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕
|
||||
bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕
|
||||
bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕
|
||||
bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕
|
||||
bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕
|
||||
float player_danmaku_opacity = 12; // 弹幕不透明度
|
||||
float player_danmaku_scalingfactor = 13; // 弹幕缩放比例
|
||||
float player_danmaku_domain = 14; // 弹幕显示区域
|
||||
int32 player_danmaku_speed = 15; // 弹幕速度
|
||||
bool player_danmaku_enableblocklist = 16; // 是否开启屏蔽列表
|
||||
bool inline_player_danmaku_switch = 17; // 是否开启弹幕
|
||||
int32 inline_player_danmaku_config = 18; //
|
||||
int32 player_danmaku_ios_switch_save = 19; //
|
||||
int32 player_danmaku_senior_mode_switch = 20; //
|
||||
int32 player_danmaku_ai_recommended_level_v2 = 21; //
|
||||
map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 22; //
|
||||
}
|
||||
|
||||
//
|
||||
message DanmuPlayerConfigPanel {
|
||||
//
|
||||
string selection_text = 1;
|
||||
}
|
||||
|
||||
// 弹幕显示区域自动配置
|
||||
message DanmuPlayerDynamicConfig {
|
||||
// 时间
|
||||
int32 progress = 1;
|
||||
// 弹幕显示区域
|
||||
float player_danmaku_domain = 14;
|
||||
}
|
||||
|
||||
// 弹幕配置信息
|
||||
message DanmuPlayerViewConfig {
|
||||
// 弹幕默认配置
|
||||
DanmuDefaultPlayerConfig danmuku_default_player_config = 1;
|
||||
// 弹幕用户配置
|
||||
DanmuPlayerConfig danmuku_player_config = 2;
|
||||
// 弹幕显示区域自动配置列表
|
||||
repeated DanmuPlayerDynamicConfig danmuku_player_dynamic_config = 3;
|
||||
//
|
||||
DanmuPlayerConfigPanel danmuku_player_config_panel = 4;
|
||||
}
|
||||
|
||||
// web端用户弹幕配置
|
||||
message DanmuWebPlayerConfig {
|
||||
bool dm_switch = 1; // 是否开启弹幕
|
||||
bool ai_switch = 2; // 是否开启智能云屏蔽
|
||||
int32 ai_level = 3; // 智能云屏蔽等级
|
||||
bool blocktop = 4; // 是否屏蔽顶端弹幕
|
||||
bool blockscroll = 5; // 是否屏蔽滚动弹幕
|
||||
bool blockbottom = 6; // 是否屏蔽底端弹幕
|
||||
bool blockcolor = 7; // 是否屏蔽彩色弹幕
|
||||
bool blockspecial = 8; // 是否屏蔽重复弹幕
|
||||
bool preventshade = 9; //
|
||||
bool dmask = 10; //
|
||||
float opacity = 11; //
|
||||
int32 dmarea = 12; //
|
||||
float speedplus = 13; //
|
||||
float fontsize = 14; // 弹幕字号
|
||||
bool screensync = 15; //
|
||||
bool speedsync = 16; //
|
||||
string fontfamily = 17; //
|
||||
bool bold = 18; // 是否使用加粗
|
||||
int32 fontborder = 19; //
|
||||
string draw_type = 20; // 弹幕渲染类型
|
||||
int32 senior_mode_switch = 21; //
|
||||
int32 ai_level_v2 = 22; //
|
||||
map<int32, int32> ai_level_v2_map = 23; //
|
||||
}
|
||||
|
||||
// 弹幕属性位值
|
||||
enum DMAttrBit {
|
||||
DMAttrBitProtect = 0; // 保护弹幕
|
||||
DMAttrBitFromLive = 1; // 直播弹幕
|
||||
DMAttrHighLike = 2; // 高赞弹幕
|
||||
}
|
||||
|
||||
message DmColorful {
|
||||
DmColorfulType type = 1; // 颜色类型
|
||||
string src = 2; //
|
||||
}
|
||||
|
||||
enum DmColorfulType {
|
||||
NoneType = 0; // 无
|
||||
VipGradualColor = 60001; // 渐变色
|
||||
}
|
||||
|
||||
//
|
||||
message DmExpoReportReq {
|
||||
//
|
||||
string session_id = 1;
|
||||
//
|
||||
int64 oid = 2;
|
||||
//
|
||||
string spmid = 4;
|
||||
}
|
||||
|
||||
//
|
||||
message DmExpoReportRes {}
|
||||
|
||||
// 修改弹幕配置-请求
|
||||
message DmPlayerConfigReq {
|
||||
int64 ts = 1; //
|
||||
PlayerDanmakuSwitch switch = 2; // 是否开启弹幕
|
||||
PlayerDanmakuSwitchSave switch_save = 3; // 是否记录弹幕开关设置
|
||||
PlayerDanmakuUseDefaultConfig use_default_config = 4; // 是否使用推荐弹幕设置
|
||||
PlayerDanmakuAiRecommendedSwitch ai_recommended_switch = 5; // 是否开启智能云屏蔽
|
||||
PlayerDanmakuAiRecommendedLevel ai_recommended_level = 6; // 智能云屏蔽等级
|
||||
PlayerDanmakuBlocktop blocktop = 7; // 是否屏蔽顶端弹幕
|
||||
PlayerDanmakuBlockscroll blockscroll = 8; // 是否屏蔽滚动弹幕
|
||||
PlayerDanmakuBlockbottom blockbottom = 9; // 是否屏蔽底端弹幕
|
||||
PlayerDanmakuBlockcolorful blockcolorful = 10; // 是否屏蔽彩色弹幕
|
||||
PlayerDanmakuBlockrepeat blockrepeat = 11; // 是否屏蔽重复弹幕
|
||||
PlayerDanmakuBlockspecial blockspecial = 12; // 是否屏蔽高级弹幕
|
||||
PlayerDanmakuOpacity opacity = 13; // 弹幕不透明度
|
||||
PlayerDanmakuScalingfactor scalingfactor = 14; // 弹幕缩放比例
|
||||
PlayerDanmakuDomain domain = 15; // 弹幕显示区域
|
||||
PlayerDanmakuSpeed speed = 16; // 弹幕速度
|
||||
PlayerDanmakuEnableblocklist enableblocklist = 17; // 是否开启屏蔽列表
|
||||
InlinePlayerDanmakuSwitch inlinePlayerDanmakuSwitch = 18; // 是否开启弹幕
|
||||
PlayerDanmakuSeniorModeSwitch senior_mode_switch = 19; //
|
||||
PlayerDanmakuAiRecommendedLevelV2 ai_recommended_level_v2 = 20; //
|
||||
}
|
||||
|
||||
//
|
||||
message DmSegConfig {
|
||||
//
|
||||
int64 page_size = 1;
|
||||
//
|
||||
int64 total = 2;
|
||||
}
|
||||
|
||||
// 获取弹幕-响应
|
||||
message DmSegMobileReply {
|
||||
// 弹幕列表
|
||||
repeated DanmakuElem elems = 1;
|
||||
// 是否已关闭弹幕
|
||||
// 0:未关闭 1:已关闭
|
||||
int32 state = 2;
|
||||
// 弹幕云屏蔽ai评分值
|
||||
DanmakuAIFlag ai_flag = 3;
|
||||
repeated DmColorful colorfulSrc = 5;
|
||||
}
|
||||
|
||||
// 获取弹幕-请求
|
||||
message DmSegMobileReq {
|
||||
// 稿件avid/漫画epid
|
||||
int64 pid = 1;
|
||||
// 视频cid/漫画cid
|
||||
int64 oid = 2;
|
||||
// 弹幕类型
|
||||
// 1:视频 2:漫画
|
||||
int32 type = 3;
|
||||
// 分段(6min)
|
||||
int64 segment_index = 4;
|
||||
// 是否青少年模式
|
||||
int32 teenagers_mode = 5;
|
||||
//
|
||||
int64 ps = 6;
|
||||
//
|
||||
int64 pe = 7;
|
||||
//
|
||||
int32 pull_mode = 8;
|
||||
//
|
||||
int32 from_scene = 9;
|
||||
}
|
||||
|
||||
// ott弹幕列表-响应
|
||||
message DmSegOttReply {
|
||||
// 是否已关闭弹幕
|
||||
// 0:未关闭 1:已关闭
|
||||
bool closed = 1;
|
||||
// 弹幕列表
|
||||
repeated DanmakuElem elems = 2;
|
||||
}
|
||||
|
||||
// ott弹幕列表-请求
|
||||
message DmSegOttReq {
|
||||
// 稿件avid/漫画epid
|
||||
int64 pid = 1;
|
||||
// 视频cid/漫画cid
|
||||
int64 oid = 2;
|
||||
// 弹幕类型
|
||||
// 1:视频 2:漫画
|
||||
int32 type = 3;
|
||||
// 分段(6min)
|
||||
int64 segment_index = 4;
|
||||
}
|
||||
|
||||
// 弹幕SDK-响应
|
||||
message DmSegSDKReply {
|
||||
// 是否已关闭弹幕
|
||||
// 0:未关闭 1:已关闭
|
||||
bool closed = 1;
|
||||
// 弹幕列表
|
||||
repeated DanmakuElem elems = 2;
|
||||
}
|
||||
|
||||
// 弹幕SDK-请求
|
||||
message DmSegSDKReq {
|
||||
// 稿件avid/漫画epid
|
||||
int64 pid = 1;
|
||||
// 视频cid/漫画cid
|
||||
int64 oid = 2;
|
||||
// 弹幕类型
|
||||
// 1:视频 2:漫画
|
||||
int32 type = 3;
|
||||
// 分段(6min)
|
||||
int64 segment_index = 4;
|
||||
}
|
||||
|
||||
// 客户端弹幕元数据-响应
|
||||
message DmViewReply {
|
||||
// 是否已关闭弹幕
|
||||
// 0:未关闭 1:已关闭
|
||||
bool closed = 1;
|
||||
// 智能防挡弹幕蒙版信息
|
||||
VideoMask mask = 2;
|
||||
// 视频字幕
|
||||
VideoSubtitle subtitle = 3;
|
||||
// 高级弹幕专包url(bfs)
|
||||
repeated string special_dms = 4;
|
||||
// 云屏蔽配置信息
|
||||
DanmakuFlagConfig ai_flag = 5;
|
||||
// 弹幕配置信息
|
||||
DanmuPlayerViewConfig player_config = 6;
|
||||
// 弹幕发送框样式
|
||||
int32 send_box_style = 7;
|
||||
// 是否允许
|
||||
bool allow = 8;
|
||||
// check box 是否展示
|
||||
string check_box = 9;
|
||||
// check box 展示文本
|
||||
string check_box_show_msg = 10;
|
||||
// 展示文案
|
||||
string text_placeholder = 11;
|
||||
// 弹幕输入框文案
|
||||
string input_placeholder = 12;
|
||||
// 用户举报弹幕 cid维度屏蔽的正则规则
|
||||
repeated string report_filter_content = 13;
|
||||
//
|
||||
ExpoReport expo_report = 14;
|
||||
//
|
||||
BuzzwordConfig buzzword_config = 15;
|
||||
//
|
||||
repeated Expressions expressions = 16;
|
||||
//
|
||||
repeated PostPanel post_panel = 17;
|
||||
//
|
||||
repeated string activity_meta = 18;
|
||||
//
|
||||
repeated PostPanelV2 post_panel2 = 19;
|
||||
}
|
||||
|
||||
// 客户端弹幕元数据-请求
|
||||
message DmViewReq {
|
||||
// 稿件avid/漫画epid
|
||||
int64 pid = 1;
|
||||
// 视频cid/漫画cid
|
||||
int64 oid = 2;
|
||||
// 弹幕类型
|
||||
// 1:视频 2:漫画
|
||||
int32 type = 3;
|
||||
// 页面spm
|
||||
string spmid = 4;
|
||||
// 是否冷启
|
||||
int32 is_hard_boot = 5;
|
||||
}
|
||||
|
||||
// web端弹幕元数据-响应
|
||||
// https://api.bilibili.com/x/v2/dm/web/view
|
||||
message DmWebViewReply {
|
||||
// 是否已关闭弹幕
|
||||
// 0:未关闭 1:已关闭
|
||||
int32 state = 1;
|
||||
//
|
||||
string text = 2;
|
||||
//
|
||||
string text_side = 3;
|
||||
// 分段弹幕配置
|
||||
DmSegConfig dm_sge = 4;
|
||||
// 云屏蔽配置信息
|
||||
DanmakuFlagConfig flag = 5;
|
||||
// 高级弹幕专包url(bfs)
|
||||
repeated string special_dms = 6;
|
||||
// check box 是否展示
|
||||
bool check_box = 7;
|
||||
// 弹幕数
|
||||
int64 count = 8;
|
||||
// 互动弹幕
|
||||
repeated CommandDm commandDms = 9;
|
||||
// 用户弹幕配置
|
||||
DanmuWebPlayerConfig player_config = 10;
|
||||
// 用户举报弹幕 cid维度屏蔽
|
||||
repeated string report_filter_content = 11;
|
||||
//
|
||||
repeated Expressions expressions = 12;
|
||||
//
|
||||
repeated PostPanel post_panel = 13;
|
||||
//
|
||||
repeated string activity_meta = 14;
|
||||
}
|
||||
|
||||
//
|
||||
message ExpoReport {
|
||||
//
|
||||
bool should_report_at_end = 1;
|
||||
}
|
||||
|
||||
//
|
||||
enum ExposureType {
|
||||
ExposureTypeNone = 0; //
|
||||
ExposureTypeDMSend = 1; //
|
||||
}
|
||||
|
||||
//
|
||||
message Expression {
|
||||
//
|
||||
repeated string keyword = 1;
|
||||
//
|
||||
string url = 2;
|
||||
//
|
||||
repeated Period period = 3;
|
||||
}
|
||||
|
||||
//
|
||||
message Expressions {
|
||||
//
|
||||
repeated Expression data = 1;
|
||||
}
|
||||
|
||||
// 是否开启弹幕
|
||||
message InlinePlayerDanmakuSwitch {
|
||||
//
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
//
|
||||
message Label {
|
||||
//
|
||||
string title = 1;
|
||||
//
|
||||
repeated string content = 2;
|
||||
}
|
||||
|
||||
//
|
||||
message LabelV2 {
|
||||
//
|
||||
string title = 1;
|
||||
//
|
||||
repeated string content = 2;
|
||||
//
|
||||
bool exposure_once = 3;
|
||||
//
|
||||
int32 exposure_type = 4;
|
||||
}
|
||||
|
||||
//
|
||||
message Period {
|
||||
//
|
||||
int64 start = 1;
|
||||
//
|
||||
int64 end = 2;
|
||||
}
|
||||
|
||||
message PlayerDanmakuAiRecommendedLevel {bool value = 1;} // 智能云屏蔽等级
|
||||
message PlayerDanmakuAiRecommendedLevelV2 {int32 value = 1;} //
|
||||
message PlayerDanmakuAiRecommendedSwitch {bool value = 1;} // 是否开启智能云屏蔽
|
||||
message PlayerDanmakuBlockbottom {bool value = 1;} // 是否屏蔽底端弹幕
|
||||
message PlayerDanmakuBlockcolorful {bool value = 1;} // 是否屏蔽彩色弹幕
|
||||
message PlayerDanmakuBlockrepeat {bool value = 1;} // 是否屏蔽重复弹幕
|
||||
message PlayerDanmakuBlockscroll {bool value = 1;} // 是否屏蔽滚动弹幕
|
||||
message PlayerDanmakuBlockspecial {bool value = 1;} // 是否屏蔽高级弹幕
|
||||
message PlayerDanmakuBlocktop {bool value = 1;} // 是否屏蔽顶端弹幕
|
||||
message PlayerDanmakuDomain {float value = 1;} // 弹幕显示区域
|
||||
message PlayerDanmakuEnableblocklist {bool value = 1;} // 是否开启屏蔽列表
|
||||
message PlayerDanmakuOpacity {float value = 1;} // 弹幕不透明度
|
||||
message PlayerDanmakuScalingfactor {float value = 1;} // 弹幕缩放比例
|
||||
message PlayerDanmakuSeniorModeSwitch {int32 value = 1;} //
|
||||
message PlayerDanmakuSpeed {int32 value = 1;} // 弹幕速度
|
||||
message PlayerDanmakuSwitch {bool value = 1; bool can_ignore = 2;} // 是否开启弹幕
|
||||
message PlayerDanmakuSwitchSave {bool value = 1;} // 是否记录弹幕开关设置
|
||||
message PlayerDanmakuUseDefaultConfig {bool value = 1;} // 是否使用推荐弹幕设置
|
||||
|
||||
//
|
||||
message PostPanel {
|
||||
//
|
||||
int64 start = 1;
|
||||
//
|
||||
int64 end = 2;
|
||||
//
|
||||
int64 priority = 3;
|
||||
//
|
||||
int64 biz_id = 4;
|
||||
//
|
||||
PostPanelBizType biz_type = 5;
|
||||
//
|
||||
ClickButton click_button = 6;
|
||||
//
|
||||
TextInput text_input = 7;
|
||||
//
|
||||
CheckBox check_box = 8;
|
||||
//
|
||||
Toast toast = 9;
|
||||
}
|
||||
|
||||
//
|
||||
enum PostPanelBizType {
|
||||
PostPanelBizTypeNone = 0; //
|
||||
PostPanelBizTypeEncourage = 1; //
|
||||
PostPanelBizTypeColorDM = 2; //
|
||||
PostPanelBizTypeNFTDM = 3; //
|
||||
PostPanelBizTypeFragClose = 4; //
|
||||
PostPanelBizTypeRecommend = 5; //
|
||||
}
|
||||
|
||||
//
|
||||
message PostPanelV2 {
|
||||
//
|
||||
int64 start = 1;
|
||||
//
|
||||
int64 end = 2;
|
||||
//
|
||||
int32 biz_type = 3;
|
||||
//
|
||||
ClickButtonV2 click_button = 4;
|
||||
//
|
||||
TextInputV2 text_input = 5;
|
||||
//
|
||||
CheckBoxV2 check_box = 6;
|
||||
//
|
||||
ToastV2 toast = 7;
|
||||
//
|
||||
BubbleV2 bubble = 8;
|
||||
//
|
||||
LabelV2 label = 9;
|
||||
//
|
||||
int32 post_status = 10;
|
||||
}
|
||||
|
||||
//
|
||||
enum PostStatus {
|
||||
PostStatusNormal = 0; //
|
||||
PostStatusClosed = 1; //
|
||||
}
|
||||
|
||||
//
|
||||
enum RenderType {
|
||||
RenderTypeNone = 0; //
|
||||
RenderTypeSingle = 1; //
|
||||
RenderTypeRotation = 2; //
|
||||
}
|
||||
|
||||
// 修改弹幕配置-响应
|
||||
message Response {
|
||||
//
|
||||
int32 code = 1;
|
||||
//
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
//
|
||||
enum SubtitleAiStatus {
|
||||
None = 0; //
|
||||
Exposure = 1; //
|
||||
Assist = 2; //
|
||||
}
|
||||
|
||||
//
|
||||
enum SubtitleAiType {
|
||||
Normal = 0; //
|
||||
Translate = 1; //
|
||||
}
|
||||
|
||||
// 单个字幕信息
|
||||
message SubtitleItem {
|
||||
// 字幕id
|
||||
int64 id = 1;
|
||||
// 字幕id str
|
||||
string id_str = 2;
|
||||
// 字幕语言代码
|
||||
string lan = 3;
|
||||
// 字幕语言
|
||||
string lan_doc = 4;
|
||||
// 字幕文件url
|
||||
string subtitle_url = 5;
|
||||
// 字幕作者信息
|
||||
UserInfo author = 6;
|
||||
// 字幕类型
|
||||
SubtitleType type = 7;
|
||||
//
|
||||
string lan_doc_brief = 8;
|
||||
//
|
||||
SubtitleAiType ai_type = 9;
|
||||
//
|
||||
SubtitleAiStatus ai_status = 10;
|
||||
}
|
||||
|
||||
enum SubtitleType {
|
||||
CC = 0; // CC字幕
|
||||
AI = 1; // AI生成字幕
|
||||
}
|
||||
|
||||
//
|
||||
message TextInput {
|
||||
//
|
||||
repeated string portrait_placeholder = 1;
|
||||
//
|
||||
repeated string landscape_placeholder = 2;
|
||||
//
|
||||
RenderType render_type = 3;
|
||||
//
|
||||
bool placeholder_post = 4;
|
||||
//
|
||||
bool show = 5;
|
||||
//
|
||||
repeated Avatar avatar = 6;
|
||||
//
|
||||
PostStatus post_status = 7;
|
||||
//
|
||||
Label label = 8;
|
||||
}
|
||||
|
||||
//
|
||||
message TextInputV2 {
|
||||
//
|
||||
repeated string portrait_placeholder = 1;
|
||||
//
|
||||
repeated string landscape_placeholder = 2;
|
||||
//
|
||||
RenderType render_type = 3;
|
||||
//
|
||||
bool placeholder_post = 4;
|
||||
//
|
||||
repeated Avatar avatar = 5;
|
||||
//
|
||||
int32 text_input_limit = 6;
|
||||
}
|
||||
|
||||
//
|
||||
message Toast {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
int32 duration = 2;
|
||||
//
|
||||
bool show = 3;
|
||||
//
|
||||
Button button = 4;
|
||||
}
|
||||
|
||||
//
|
||||
message ToastButtonV2 {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
int32 action = 2;
|
||||
}
|
||||
|
||||
//
|
||||
enum ToastFunctionType {
|
||||
ToastFunctionTypeNone = 0; //
|
||||
ToastFunctionTypePostPanel = 1; //
|
||||
}
|
||||
|
||||
//
|
||||
message ToastV2 {
|
||||
//
|
||||
string text = 1;
|
||||
//
|
||||
int32 duration = 2;
|
||||
//
|
||||
ToastButtonV2 toast_button_v2 = 3;
|
||||
}
|
||||
|
||||
// 字幕作者信息
|
||||
message UserInfo {
|
||||
// 用户mid
|
||||
int64 mid = 1;
|
||||
// 用户昵称
|
||||
string name = 2;
|
||||
// 用户性别
|
||||
string sex = 3;
|
||||
// 用户头像url
|
||||
string face = 4;
|
||||
// 用户签名
|
||||
string sign = 5;
|
||||
// 用户等级
|
||||
int32 rank = 6;
|
||||
}
|
||||
|
||||
// 智能防挡弹幕蒙版信息
|
||||
message VideoMask {
|
||||
// 视频cid
|
||||
int64 cid = 1;
|
||||
// 平台
|
||||
// 0:web端 1:客户端
|
||||
int32 plat = 2;
|
||||
// 帧率
|
||||
int32 fps = 3;
|
||||
// 间隔时间
|
||||
int64 time = 4;
|
||||
// 蒙版url
|
||||
string mask_url = 5;
|
||||
}
|
||||
|
||||
// 视频字幕信息
|
||||
message VideoSubtitle {
|
||||
// 视频原语言代码
|
||||
string lan = 1;
|
||||
// 视频原语言
|
||||
string lanDoc = 2;
|
||||
// 视频字幕列表
|
||||
repeated SubtitleItem subtitles = 3;
|
||||
}
|
@ -244,7 +244,9 @@ class Vote {
|
||||
choiceCnt = json['choice_cnt'];
|
||||
share = json['share'];
|
||||
defaultShare = json['default_share'];
|
||||
endTime = json['end_time'];
|
||||
endTime = json['end_time'] is int
|
||||
? json['end_time']
|
||||
: int.parse(json['end_time']);
|
||||
joinNum = json['join_num'];
|
||||
status = json['status'];
|
||||
type = json['type'];
|
||||
@ -360,7 +362,7 @@ class GoodItem {
|
||||
|
||||
String? brief;
|
||||
String? cover;
|
||||
String? id;
|
||||
dynamic id;
|
||||
String? jumpDesc;
|
||||
String? jumpUrl;
|
||||
String? name;
|
||||
@ -408,6 +410,7 @@ class DynamicMajorModel {
|
||||
this.live,
|
||||
this.none,
|
||||
this.type,
|
||||
this.courses,
|
||||
});
|
||||
|
||||
DynamicArchiveModel? archive;
|
||||
@ -422,6 +425,7 @@ class DynamicMajorModel {
|
||||
// MAJOR_TYPE_ARCHIVE 视频
|
||||
// MAJOR_TYPE_OPUS 图文/文章
|
||||
String? type;
|
||||
Map? courses;
|
||||
|
||||
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
|
||||
archive = json['archive'] != null
|
||||
@ -444,6 +448,7 @@ class DynamicMajorModel {
|
||||
none =
|
||||
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
|
||||
type = json['type'];
|
||||
courses = json['courses'] ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,6 +483,8 @@ class DynamicArchiveModel {
|
||||
this.stat,
|
||||
this.title,
|
||||
this.type,
|
||||
this.epid,
|
||||
this.seasonId,
|
||||
});
|
||||
|
||||
int? aid;
|
||||
@ -491,6 +498,8 @@ class DynamicArchiveModel {
|
||||
Stat? stat;
|
||||
String? title;
|
||||
int? type;
|
||||
int? epid;
|
||||
int? seasonId;
|
||||
|
||||
DynamicArchiveModel.fromJson(Map<String, dynamic> json) {
|
||||
aid = json['aid'] is String ? int.parse(json['aid']) : json['aid'];
|
||||
@ -503,6 +512,8 @@ class DynamicArchiveModel {
|
||||
stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null;
|
||||
title = json['title'];
|
||||
type = json['type'];
|
||||
epid = json['epid'];
|
||||
seasonId = json['season_id'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,9 @@ class FollowUpModel {
|
||||
List<UpItem>? upList;
|
||||
|
||||
FollowUpModel.fromJson(Map<String, dynamic> json) {
|
||||
liveUsers = LiveUsers.fromJson(json['live_users']);
|
||||
liveUsers = json['live_users'] != null
|
||||
? LiveUsers.fromJson(json['live_users'])
|
||||
: null;
|
||||
upList = json['up_list'] != null
|
||||
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()
|
||||
: [];
|
||||
|
@ -8,7 +8,7 @@ class FollowDataModel {
|
||||
List<FollowItemModel>? list;
|
||||
|
||||
FollowDataModel.fromJson(Map<String, dynamic> json) {
|
||||
total = json['total'];
|
||||
total = json['total'] ?? 0;
|
||||
list = json['list']
|
||||
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
|
||||
.toList();
|
||||
@ -19,7 +19,7 @@ class FollowItemModel {
|
||||
FollowItemModel({
|
||||
this.mid,
|
||||
this.attribute,
|
||||
this.mtime,
|
||||
// this.mtime,
|
||||
this.tag,
|
||||
this.special,
|
||||
this.uname,
|
||||
@ -30,7 +30,7 @@ class FollowItemModel {
|
||||
|
||||
int? mid;
|
||||
int? attribute;
|
||||
int? mtime;
|
||||
// int? mtime;
|
||||
List? tag;
|
||||
int? special;
|
||||
String? uname;
|
||||
@ -41,7 +41,7 @@ class FollowItemModel {
|
||||
FollowItemModel.fromJson(Map<String, dynamic> json) {
|
||||
mid = json['mid'];
|
||||
attribute = json['attribute'];
|
||||
mtime = json['mtime'];
|
||||
// mtime = json['mtime'];
|
||||
tag = json['tag'];
|
||||
special = json['special'];
|
||||
uname = json['uname'];
|
||||
|
@ -4,12 +4,14 @@ class LatestDataModel {
|
||||
this.tagName,
|
||||
this.createdAt,
|
||||
this.assets,
|
||||
this.body,
|
||||
});
|
||||
|
||||
String? url;
|
||||
String? tagName;
|
||||
String? createdAt;
|
||||
List? assets;
|
||||
String? body;
|
||||
|
||||
LatestDataModel.fromJson(Map<String, dynamic> json) {
|
||||
url = json['url'];
|
||||
@ -17,6 +19,7 @@ class LatestDataModel {
|
||||
createdAt = json['created_at'];
|
||||
assets =
|
||||
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList();
|
||||
body = json['body'];
|
||||
}
|
||||
}
|
||||
|
||||
|
23
lib/models/member/tags.dart
Normal file
23
lib/models/member/tags.dart
Normal file
@ -0,0 +1,23 @@
|
||||
class MemberTagItemModel {
|
||||
MemberTagItemModel({
|
||||
this.count,
|
||||
this.name,
|
||||
this.tagid,
|
||||
this.tip,
|
||||
this.checked,
|
||||
});
|
||||
|
||||
int? count;
|
||||
String? name;
|
||||
int? tagid;
|
||||
String? tip;
|
||||
bool? checked;
|
||||
|
||||
MemberTagItemModel.fromJson(Map<String, dynamic> json) {
|
||||
count = json['count'];
|
||||
name = json['name'];
|
||||
tagid = json['tagid'];
|
||||
tip = json['tip'];
|
||||
checked = false;
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ class SearchVideoModel {
|
||||
List<SearchVideoItemModel>? list;
|
||||
SearchVideoModel.fromJson(Map<String, dynamic> json) {
|
||||
list = json['result']
|
||||
.where((e) => e['available'] == true)
|
||||
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
@ -17,7 +18,7 @@ class SearchVideoItemModel {
|
||||
this.id,
|
||||
this.cid,
|
||||
// this.author,
|
||||
// this.mid,
|
||||
this.mid,
|
||||
// this.typeid,
|
||||
// this.typename,
|
||||
this.arcurl,
|
||||
@ -47,7 +48,7 @@ class SearchVideoItemModel {
|
||||
int? id;
|
||||
int? cid;
|
||||
// String? author;
|
||||
// String? mid;
|
||||
int? mid;
|
||||
// String? typeid;
|
||||
// String? typename;
|
||||
String? arcurl;
|
||||
@ -80,6 +81,7 @@ class SearchVideoItemModel {
|
||||
arcurl = json['arcurl'];
|
||||
aid = json['aid'];
|
||||
bvid = json['bvid'];
|
||||
mid = json['mid'];
|
||||
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
|
||||
title = Em.regTitle(json['title']);
|
||||
description = json['description'];
|
||||
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
|
||||
indexShow = json['index_show'];
|
||||
}
|
||||
}
|
||||
|
||||
class SearchArticleModel {
|
||||
SearchArticleModel({this.list});
|
||||
|
||||
List<SearchArticleItemModel>? list;
|
||||
|
||||
SearchArticleModel.fromJson(Map<String, dynamic> json) {
|
||||
list = json['result'] != null
|
||||
? json['result']
|
||||
.map<SearchArticleItemModel>(
|
||||
(e) => SearchArticleItemModel.fromJson(e))
|
||||
.toList()
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
class SearchArticleItemModel {
|
||||
SearchArticleItemModel({
|
||||
this.pubTime,
|
||||
this.like,
|
||||
this.title,
|
||||
this.subTitle,
|
||||
this.rankOffset,
|
||||
this.mid,
|
||||
this.imageUrls,
|
||||
this.id,
|
||||
this.categoryId,
|
||||
this.view,
|
||||
this.reply,
|
||||
this.desc,
|
||||
this.rankScore,
|
||||
this.type,
|
||||
this.templateId,
|
||||
this.categoryName,
|
||||
});
|
||||
|
||||
int? pubTime;
|
||||
int? like;
|
||||
List? title;
|
||||
String? subTitle;
|
||||
int? rankOffset;
|
||||
int? mid;
|
||||
List? imageUrls;
|
||||
int? id;
|
||||
int? categoryId;
|
||||
int? view;
|
||||
int? reply;
|
||||
String? desc;
|
||||
int? rankScore;
|
||||
String? type;
|
||||
int? templateId;
|
||||
String? categoryName;
|
||||
|
||||
SearchArticleItemModel.fromJson(Map<String, dynamic> json) {
|
||||
pubTime = json['pub_time'];
|
||||
like = json['like'];
|
||||
title = Em.regTitle(json['title']);
|
||||
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
|
||||
rankOffset = json['rank_offset'];
|
||||
mid = json['mid'];
|
||||
imageUrls = json['image_urls'];
|
||||
id = json['id'];
|
||||
categoryId = json['category_id'];
|
||||
view = json['view'];
|
||||
reply = json['reply'];
|
||||
desc = json['desc'];
|
||||
rankScore = json['rank_score'];
|
||||
type = json['type'];
|
||||
templateId = json['templateId'];
|
||||
categoryName = json['category_name'];
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class SearchSuggestModel {
|
||||
SearchSuggestModel({
|
||||
this.tag,
|
||||
@ -19,32 +22,74 @@ class SearchSuggestItem {
|
||||
SearchSuggestItem({
|
||||
this.value,
|
||||
this.term,
|
||||
this.name,
|
||||
this.spid,
|
||||
this.textRich,
|
||||
});
|
||||
|
||||
String? value;
|
||||
String? term;
|
||||
List? name;
|
||||
int? spid;
|
||||
Widget? textRich;
|
||||
|
||||
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
|
||||
value = json['value'];
|
||||
term = json['term'];
|
||||
String reg = '<em class="suggest_high_light">$inputTerm</em>';
|
||||
try {
|
||||
if (json['name'].indexOf(inputTerm) != -1) {
|
||||
String str = json['name'].replaceAll(reg, '^');
|
||||
List arr = str.split('^');
|
||||
arr.insert(arr.length - 1, inputTerm);
|
||||
name = arr;
|
||||
} else {
|
||||
name = ['', '', json['term']];
|
||||
}
|
||||
} catch (err) {
|
||||
name = ['', '', json['term']];
|
||||
}
|
||||
|
||||
spid = json['spid'];
|
||||
textRich = highlightText(json['name']);
|
||||
}
|
||||
}
|
||||
|
||||
Widget highlightText(String str) {
|
||||
// 创建正则表达式,匹配 <em class="suggest_high_light">...</em> 格式的文本
|
||||
RegExp regex = RegExp(r'<em class="suggest_high_light">(.*?)<\/em>');
|
||||
|
||||
// 用于存储每个匹配项的列表
|
||||
List<InlineSpan> children = [];
|
||||
|
||||
// 获取所有匹配项
|
||||
Iterable<Match> matches = regex.allMatches(str);
|
||||
|
||||
// 当前索引位置
|
||||
int currentIndex = 0;
|
||||
|
||||
// 遍历每个匹配项
|
||||
for (var match in matches) {
|
||||
// 获取当前匹配项之前的普通文本部分
|
||||
String normalText = str.substring(currentIndex, match.start);
|
||||
|
||||
// 获取需要高亮显示的文本部分
|
||||
String highlightedText = match.group(1)!;
|
||||
|
||||
// 如果普通文本部分不为空,则将其添加到 children 列表中
|
||||
if (normalText.isNotEmpty) {
|
||||
children.add(TextSpan(
|
||||
text: normalText,
|
||||
style: DefaultTextStyle.of(Get.context!).style,
|
||||
));
|
||||
}
|
||||
|
||||
// 将需要高亮显示的文本部分添加到 children 列表中,并设置相应样式
|
||||
children.add(TextSpan(
|
||||
text: highlightedText,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(Get.context!).colorScheme.primary),
|
||||
));
|
||||
|
||||
// 更新当前索引位置
|
||||
currentIndex = match.end;
|
||||
}
|
||||
|
||||
// 如果当前索引位置小于文本长度,表示还有剩余的普通文本部分
|
||||
if (currentIndex < str.length) {
|
||||
String remainingText = str.substring(currentIndex);
|
||||
|
||||
// 将剩余的普通文本部分添加到 children 列表中
|
||||
children.add(TextSpan(
|
||||
text: remainingText,
|
||||
style: DefaultTextStyle.of(Get.context!).style,
|
||||
));
|
||||
}
|
||||
|
||||
// 使用 Text.rich 创建包含高亮显示的富文本小部件,并返回
|
||||
return Text.rich(TextSpan(children: children));
|
||||
}
|
||||
|
@ -3,17 +3,23 @@ class HistoryData {
|
||||
this.cursor,
|
||||
this.tab,
|
||||
this.list,
|
||||
this.page,
|
||||
});
|
||||
|
||||
Cursor? cursor;
|
||||
List<HisTabItem>? tab;
|
||||
List<HisListItem>? list;
|
||||
Map? page;
|
||||
|
||||
HistoryData.fromJson(Map<String, dynamic> json) {
|
||||
cursor = Cursor.fromJson(json['cursor']);
|
||||
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList();
|
||||
list =
|
||||
json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList();
|
||||
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
|
||||
tab = json['tab'] != null
|
||||
? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()
|
||||
: [];
|
||||
list = json['list'] != null
|
||||
? json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList()
|
||||
: [];
|
||||
page = json['page'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +85,7 @@ class HisListItem {
|
||||
this.kid,
|
||||
this.tagName,
|
||||
this.liveStatus,
|
||||
this.checked,
|
||||
});
|
||||
|
||||
String? title;
|
||||
@ -105,6 +112,7 @@ class HisListItem {
|
||||
int? kid;
|
||||
String? tagName;
|
||||
int? liveStatus;
|
||||
bool? checked;
|
||||
|
||||
HisListItem.fromJson(Map<String, dynamic> json) {
|
||||
title = json['title'];
|
||||
@ -121,7 +129,7 @@ class HisListItem {
|
||||
viewAt = json['view_at'];
|
||||
progress = json['progress'];
|
||||
badge = json['badge'];
|
||||
showTitle = json['show_title'];
|
||||
showTitle = json['show_title'] == '' ? null : json['show_title'];
|
||||
duration = json['duration'];
|
||||
current = json['current'];
|
||||
total = json['total'];
|
||||
@ -131,6 +139,7 @@ class HisListItem {
|
||||
kid = json['kid'];
|
||||
tagName = json['tag_name'];
|
||||
liveStatus = json['live_status'];
|
||||
checked = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ class UserInfoData {
|
||||
@HiveField(5)
|
||||
int? mobileVerified;
|
||||
@HiveField(6)
|
||||
int? money;
|
||||
double? money;
|
||||
@HiveField(7)
|
||||
int? moral;
|
||||
@HiveField(8)
|
||||
@ -88,7 +88,7 @@ class UserInfoData {
|
||||
: LevelInfo();
|
||||
mid = json['mid'];
|
||||
mobileVerified = json['mobile_verified'];
|
||||
money = json['money'];
|
||||
money = json['money'] is int ? json['money'].toDouble() : json['money'];
|
||||
moral = json['moral'];
|
||||
official = json['official'];
|
||||
officialVerify = json['officialVerify'];
|
||||
@ -130,6 +130,7 @@ class LevelInfo {
|
||||
currentLevel = json['current_level'];
|
||||
currentMin = json['current_min'];
|
||||
currentExp = json['current_exp'];
|
||||
nextExp = json['next_exp'];
|
||||
nextExp =
|
||||
json['current_level'] == 6 ? json['current_exp'] : json['next_exp'];
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class UserInfoDataAdapter extends TypeAdapter<UserInfoData> {
|
||||
levelInfo: fields[3] as LevelInfo?,
|
||||
mid: fields[4] as int?,
|
||||
mobileVerified: fields[5] as int?,
|
||||
money: fields[6] as int?,
|
||||
money: fields[6] as double?,
|
||||
moral: fields[7] as int?,
|
||||
official: (fields[8] as Map?)?.cast<dynamic, dynamic>(),
|
||||
officialVerify: (fields[9] as Map?)?.cast<dynamic, dynamic>(),
|
||||
|
@ -93,26 +93,19 @@ extension AudioQualityDesc on AudioQuality {
|
||||
}
|
||||
|
||||
enum VideoDecodeFormats {
|
||||
DVH1,
|
||||
AV1,
|
||||
HEVC,
|
||||
AVC,
|
||||
}
|
||||
|
||||
extension VideoDecodeFormatsDesc on VideoDecodeFormats {
|
||||
static final List<String> _descList = [
|
||||
'AV1',
|
||||
'HEVC',
|
||||
'AVC',
|
||||
];
|
||||
static final List<String> _descList = ['DVH1', 'AV1', 'HEVC', 'AVC'];
|
||||
get description => _descList[index];
|
||||
}
|
||||
|
||||
extension VideoDecodeFormatsCode on VideoDecodeFormats {
|
||||
static final List<String> _codeList = [
|
||||
'av01',
|
||||
'hev1',
|
||||
'avc1',
|
||||
];
|
||||
static final List<String> _codeList = ['dvh1', 'av01', 'hev1', 'avc1'];
|
||||
get code => _codeList[index];
|
||||
|
||||
static VideoDecodeFormats? fromCode(String code) {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:pilipala/models/video/play/quality.dart';
|
||||
|
||||
class PlayUrlModel {
|
||||
@ -77,8 +79,8 @@ class Dash {
|
||||
double? minBufferTime;
|
||||
List<VideoItem>? video;
|
||||
List<AudioItem>? audio;
|
||||
Map? dolby;
|
||||
Map? flac;
|
||||
Dolby? dolby;
|
||||
Flac? flac;
|
||||
|
||||
Dash.fromJson(Map<String, dynamic> json) {
|
||||
duration = json['duration'];
|
||||
@ -87,8 +89,8 @@ class Dash {
|
||||
audio = json['audio'] != null
|
||||
? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()
|
||||
: [];
|
||||
dolby = json['dolby'];
|
||||
flac = json['flac'] ?? {};
|
||||
dolby = json['dolby'] != null ? Dolby.fromJson(json['dolby']) : null;
|
||||
flac = json['flac'] != null ? Flac.fromJson(json['flac']) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +130,8 @@ class VideoItem {
|
||||
VideoItem.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
baseUrl = json['baseUrl'];
|
||||
backupUrl = json['backupUrl'].toList().first;
|
||||
backupUrl =
|
||||
json['backupUrl'] != null ? json['backupUrl'].toList().first : '';
|
||||
bandWidth = json['bandWidth'];
|
||||
mimeType = json['mime_type'];
|
||||
codecs = json['codecs'];
|
||||
@ -179,7 +182,8 @@ class AudioItem {
|
||||
AudioItem.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
baseUrl = json['baseUrl'];
|
||||
backupUrl = json['backupUrl'].toList().first;
|
||||
backupUrl =
|
||||
json['backupUrl'] != null ? json['backupUrl'].toList().first : '';
|
||||
bandWidth = json['bandWidth'];
|
||||
mimeType = json['mime_type'];
|
||||
codecs = json['codecs'];
|
||||
@ -218,3 +222,33 @@ class FormatItem {
|
||||
codecs = json['codecs'];
|
||||
}
|
||||
}
|
||||
|
||||
class Dolby {
|
||||
Dolby({
|
||||
this.type,
|
||||
this.audio,
|
||||
});
|
||||
|
||||
// 1:普通杜比音效 2:全景杜比音效
|
||||
int? type;
|
||||
List<AudioItem>? audio;
|
||||
|
||||
Dolby.fromJson(Map<String, dynamic> json) {
|
||||
type = json['type'];
|
||||
audio = json['audio'] != null
|
||||
? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
class Flac {
|
||||
Flac({this.display, this.audio});
|
||||
|
||||
bool? display;
|
||||
AudioItem? audio;
|
||||
|
||||
Flac.fromJson(Map<String, dynamic> json) {
|
||||
display = json['display'];
|
||||
audio = json['audio'] != null ? AudioItem.fromJson(json['audio']) : null;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:pilipala/http/index.dart';
|
||||
import 'package:pilipala/models/github/latest.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
@ -48,6 +47,11 @@ class _AboutPageState extends State<AboutPage> {
|
||||
'PiliPala',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'使用Flutter开发的哔哩哔哩第三方客户端',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Obx(
|
||||
() => ListTile(
|
||||
@ -83,16 +87,6 @@ class _AboutPageState extends State<AboutPage> {
|
||||
height: 30,
|
||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
title: const Text('作者'),
|
||||
trailing: Text('guozhigq', style: subTitleStyle),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () {},
|
||||
title: const Text('酷安'),
|
||||
trailing: Text('影若风', style: subTitleStyle),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => _aboutController.githubUrl(),
|
||||
title: const Text('Github'),
|
||||
@ -101,6 +95,17 @@ class _AboutPageState extends State<AboutPage> {
|
||||
style: subTitleStyle,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => _aboutController.panDownload(),
|
||||
title: const Text('网盘下载'),
|
||||
trailing: Text(
|
||||
'提取码:pili',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => _aboutController.feedback(),
|
||||
title: const Text('问题反馈'),
|
||||
@ -112,7 +117,7 @@ class _AboutPageState extends State<AboutPage> {
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => _aboutController.qqChanel(),
|
||||
title: const Text('QQ频道'),
|
||||
title: const Text('QQ群'),
|
||||
trailing: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 16,
|
||||
@ -124,6 +129,11 @@ class _AboutPageState extends State<AboutPage> {
|
||||
title: const Text('TG频道'),
|
||||
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
|
||||
),
|
||||
ListTile(
|
||||
onTap: () => _aboutController.aPay(),
|
||||
title: const Text('赞助'),
|
||||
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
|
||||
),
|
||||
Divider(
|
||||
thickness: 8,
|
||||
height: 30,
|
||||
@ -142,11 +152,12 @@ class AboutController extends GetxController {
|
||||
late LatestDataModel remoteAppInfo;
|
||||
RxBool isUpdate = true.obs;
|
||||
RxBool isLoading = true.obs;
|
||||
late LatestDataModel data;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
init();
|
||||
// init();
|
||||
// 获取当前版本
|
||||
getCurrentApp();
|
||||
// 获取最新的版本
|
||||
@ -154,18 +165,18 @@ class AboutController extends GetxController {
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
Future init() async {
|
||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
if (Platform.isAndroid) {
|
||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
print(androidInfo.supportedAbis);
|
||||
} else if (Platform.isIOS) {
|
||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||
print(iosInfo);
|
||||
}
|
||||
}
|
||||
// Future init() async {
|
||||
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||
// if (Platform.isAndroid) {
|
||||
// AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||
// print(androidInfo.supportedAbis);
|
||||
// } else if (Platform.isIOS) {
|
||||
// IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||
// print(iosInfo);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 获取啊当前版本
|
||||
// 获取当前版本
|
||||
Future getCurrentApp() async {
|
||||
var result = await PackageInfo.fromPlatform();
|
||||
currentVersion.value = result.version;
|
||||
@ -173,8 +184,8 @@ class AboutController extends GetxController {
|
||||
|
||||
// 获取远程版本
|
||||
Future getRemoteApp() async {
|
||||
var result = await Request().get(Api.latestApp);
|
||||
LatestDataModel data = LatestDataModel.fromJson(result.data);
|
||||
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
|
||||
data = LatestDataModel.fromJson(result.data);
|
||||
remoteAppInfo = data;
|
||||
remoteVersion.value = data.tagName!;
|
||||
isUpdate.value =
|
||||
@ -184,15 +195,7 @@ class AboutController extends GetxController {
|
||||
|
||||
// 跳转下载/本地更新
|
||||
Future onUpdate() async {
|
||||
// final dir = await getApplicationSupportDirectory();
|
||||
// final path = '${dir.path}/pilipala.apk';
|
||||
// var result = await Request()
|
||||
// .downloadFile(remoteAppInfo.assets!.first.downloadUrl, path);
|
||||
// print(result);
|
||||
launchUrl(
|
||||
Uri.parse('https://github.com/guozhigq/pilipala/releases'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
Utils.matchVersion(data);
|
||||
}
|
||||
|
||||
// 跳转github
|
||||
@ -203,6 +206,14 @@ class AboutController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
// 从网盘下载
|
||||
panDownload() {
|
||||
launchUrl(
|
||||
Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
// 问题反馈
|
||||
feedback() {
|
||||
launchUrl(
|
||||
@ -215,17 +226,9 @@ class AboutController extends GetxController {
|
||||
// qq频道
|
||||
qqChanel() {
|
||||
Clipboard.setData(
|
||||
const ClipboardData(text: 'https://pd.qq.com/s/css9rdwga'),
|
||||
);
|
||||
SmartDialog.showToast(
|
||||
'已复制,即将在浏览器打开',
|
||||
displayTime: const Duration(milliseconds: 500),
|
||||
).then(
|
||||
(value) => launchUrl(
|
||||
Uri.parse('https://pd.qq.com/s/css9rdwga'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
const ClipboardData(text: '489981949'),
|
||||
);
|
||||
SmartDialog.showToast('已复制QQ群号');
|
||||
}
|
||||
|
||||
// tg频道
|
||||
@ -243,4 +246,16 @@ class AboutController extends GetxController {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
aPay() {
|
||||
try {
|
||||
launchUrl(
|
||||
Uri.parse(
|
||||
'alipayqr://platformapi/startapp?saId=10000007&qrcode=https://qr.alipay.com/fkx14623ddwl1ping3ddd73'),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,17 +11,19 @@ class BangumiController extends GetxController {
|
||||
RxList<BangumiListItemModel> bangumiFollowList = [BangumiListItemModel()].obs;
|
||||
int _currentPage = 1;
|
||||
bool isLoadingMore = true;
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
RxBool userLogin = false.obs;
|
||||
late int mid;
|
||||
var userInfo;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
if (user.get(UserBoxKey.userMid) != null) {
|
||||
mid = int.parse(user.get(UserBoxKey.userMid).toString());
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo != null) {
|
||||
mid = userInfo.mid;
|
||||
}
|
||||
userLogin.value = user.get(UserBoxKey.userLogin) != null;
|
||||
userLogin.value = userInfo != null;
|
||||
}
|
||||
|
||||
Future queryBangumiListFeed({type = 'init'}) async {
|
||||
@ -48,7 +50,11 @@ class BangumiController extends GetxController {
|
||||
|
||||
// 我的订阅
|
||||
Future queryBangumiFollow() async {
|
||||
var result = await BangumiHttp.bangumiFollow(mid: 17340771);
|
||||
userInfo = userInfo ?? userInfoCache.get('userInfoCache');
|
||||
if (userInfo == null) {
|
||||
return;
|
||||
}
|
||||
var result = await BangumiHttp.bangumiFollow(mid: userInfo.mid);
|
||||
if (result['status']) {
|
||||
bangumiFollowList.value = result['data'].list;
|
||||
} else {}
|
||||
|
@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
|
||||
? int.parse(Get.parameters['seasonId']!)
|
||||
: null;
|
||||
var epId = Get.parameters['epId'] != null
|
||||
? int.parse(Get.parameters['epId']!)
|
||||
? int.tryParse(Get.parameters['epId']!)
|
||||
: null;
|
||||
|
||||
// 是否预渲染 骨架屏
|
||||
@ -49,7 +50,7 @@ class BangumiIntroController extends GetxController {
|
||||
RxBool hasCoin = false.obs;
|
||||
// 是否收藏
|
||||
RxBool hasFav = false.obs;
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
bool userLogin = false;
|
||||
Rx<FavFolderData> favFolderData = FavFolderData().obs;
|
||||
List addMediaIdsNew = [];
|
||||
@ -57,6 +58,7 @@ class BangumiIntroController extends GetxController {
|
||||
// 关注状态 默认未关注
|
||||
RxMap followStatus = {}.obs;
|
||||
int _tempThemeValue = -1;
|
||||
var userInfo;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -82,7 +84,8 @@ class BangumiIntroController extends GetxController {
|
||||
// videoItem!['owner'] = args.owner;
|
||||
}
|
||||
}
|
||||
userLogin = user.get(UserBoxKey.userLogin) != null;
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
userLogin = userInfo != null;
|
||||
}
|
||||
|
||||
// 获取番剧简介&选集
|
||||
@ -142,7 +145,7 @@ class BangumiIntroController extends GetxController {
|
||||
|
||||
// 投币
|
||||
Future actionCoinVideo() async {
|
||||
if (user.get(UserBoxKey.userMid) == null) {
|
||||
if (userInfo == null) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
@ -255,7 +258,8 @@ class BangumiIntroController extends GetxController {
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
videoDetailCtr.bvid = bvid;
|
||||
videoDetailCtr.cid = cid;
|
||||
videoDetailCtr.cid.value = cid;
|
||||
videoDetailCtr.danmakuCid.value = cid;
|
||||
videoDetailCtr.queryVideoUrl();
|
||||
// 重新请求评论
|
||||
try {
|
||||
@ -283,10 +287,37 @@ class BangumiIntroController extends GetxController {
|
||||
|
||||
Future queryVideoInFolder() async {
|
||||
var result = await VideoHttp.videoInFolder(
|
||||
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
|
||||
mid: userInfo.mid, rid: IdUtils.bv2av(bvid));
|
||||
if (result['status']) {
|
||||
favFolderData.value = result['data'];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 列表循环或者顺序播放时,自动播放下一个
|
||||
void nextPlay() {
|
||||
late List episodes;
|
||||
if (bangumiDetail.value.episodes != null) {
|
||||
episodes = bangumiDetail.value.episodes!;
|
||||
}
|
||||
VideoDetailController videoDetailCtr =
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
int currentIndex =
|
||||
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
|
||||
int nextIndex = currentIndex + 1;
|
||||
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
|
||||
// 列表循环
|
||||
if (platRepeat == PlayRepeat.listCycle) {
|
||||
if (nextIndex == episodes.length - 1) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
}
|
||||
if (nextIndex <= episodes.length - 1 &&
|
||||
platRepeat == PlayRepeat.listOrder) {}
|
||||
|
||||
int cid = episodes[nextIndex].cid!;
|
||||
String bvid = episodes[nextIndex].bvid!;
|
||||
int aid = episodes[nextIndex].aid!;
|
||||
changeSeasonOrbangu(bvid, cid, aid);
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,11 @@ import 'controller.dart';
|
||||
import 'widgets/intro_detail.dart';
|
||||
|
||||
class BangumiIntroPanel extends StatefulWidget {
|
||||
const BangumiIntroPanel({super.key});
|
||||
final int? cid;
|
||||
const BangumiIntroPanel({
|
||||
Key? key,
|
||||
this.cid,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<BangumiIntroPanel> createState() => _BangumiIntroPanelState();
|
||||
@ -30,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
|
||||
|
||||
class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final BangumiIntroController bangumiIntroController =
|
||||
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
|
||||
late BangumiIntroController bangumiIntroController;
|
||||
late VideoDetailController videoDetailCtr;
|
||||
BangumiInfoModel? bangumiDetail;
|
||||
late Future _futureBuilderFuture;
|
||||
late int cid;
|
||||
late String heroTag;
|
||||
|
||||
// 添加页面缓存
|
||||
@override
|
||||
@ -42,10 +48,19 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
heroTag = Get.arguments['heroTag'];
|
||||
cid = widget.cid!;
|
||||
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
bangumiIntroController.bangumiDetail.listen((value) {
|
||||
bangumiDetail = value;
|
||||
});
|
||||
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
|
||||
videoDetailCtr.cid.listen((p0) {
|
||||
print('🐶🐶$p0');
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -57,19 +72,26 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
|
||||
return BangumiInfo(
|
||||
loadingStatus: false,
|
||||
bangumiDetail: bangumiDetail,
|
||||
cid: cid,
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: snapshot.data['msg'],
|
||||
fn: () => Get.back(),
|
||||
);
|
||||
// return HttpError(
|
||||
// errMsg: snapshot.data['msg'],
|
||||
// fn: () => Get.back(),
|
||||
// );
|
||||
return SizedBox();
|
||||
}
|
||||
} else {
|
||||
return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
|
||||
return BangumiInfo(
|
||||
loadingStatus: true,
|
||||
bangumiDetail: bangumiDetail,
|
||||
cid: cid,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -79,11 +101,13 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
class BangumiInfo extends StatefulWidget {
|
||||
final bool loadingStatus;
|
||||
final BangumiInfoModel? bangumiDetail;
|
||||
final int? cid;
|
||||
|
||||
const BangumiInfo({
|
||||
Key? key,
|
||||
this.loadingStatus = false,
|
||||
this.bangumiDetail,
|
||||
this.cid,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -97,6 +121,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
Box localCache = GStrorage.localCache;
|
||||
late final BangumiInfoModel? bangumiItem;
|
||||
late double sheetHeight;
|
||||
int? cid;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -105,11 +130,18 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
bangumiItem = bangumiIntroController.bangumiItem;
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
cid = widget.cid!;
|
||||
print('cid: $cid');
|
||||
videoDetailCtr.cid.listen((p0) {
|
||||
cid = p0;
|
||||
print('cid: $cid');
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
// 收藏
|
||||
showFavBottomSheet() {
|
||||
if (bangumiIntroController.user.get(UserBoxKey.userMid) == null) {
|
||||
if (bangumiIntroController.userInfo.mid == null) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
@ -248,9 +280,15 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
children: [
|
||||
Text(
|
||||
!widget.loadingStatus
|
||||
? widget.bangumiDetail!.areas!
|
||||
.first['name']
|
||||
: bangumiItem!.areas!.first['name'],
|
||||
? (widget.bangumiDetail!.areas!
|
||||
.isNotEmpty
|
||||
? widget.bangumiDetail!.areas!
|
||||
.first['name']
|
||||
: '')
|
||||
: (bangumiItem!.areas!.isNotEmpty
|
||||
? bangumiItem!
|
||||
.areas!.first['name']
|
||||
: ''),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: t.colorScheme.outline,
|
||||
@ -320,9 +358,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
pages: bangumiItem != null
|
||||
? bangumiItem!.episodes!
|
||||
: widget.bangumiDetail!.episodes!,
|
||||
cid: bangumiItem != null
|
||||
? bangumiItem!.episodes!.first.cid
|
||||
: widget.bangumiDetail!.episodes!.first.cid,
|
||||
cid: cid ??
|
||||
(bangumiItem != null
|
||||
? bangumiItem!.episodes!.first.cid
|
||||
: widget.bangumiDetail!.episodes!.first.cid),
|
||||
sheetHeight: sheetHeight,
|
||||
changeFuc: (bvid, cid, aid) => bangumiIntroController
|
||||
.changeSeasonOrbangu(bvid, cid, aid),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
@ -22,24 +23,28 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
final BangumiController _bangumidController = Get.put(BangumiController());
|
||||
late Future? _futureBuilderFuture;
|
||||
late Future? _futureBuilderFutureFollow;
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ScrollController scrollController = _bangumidController.scrollController;
|
||||
scrollController = _bangumidController.scrollController;
|
||||
StreamController<bool> mainStream =
|
||||
Get.find<MainController>().bottomBarStream;
|
||||
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
|
||||
_futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (!_bangumidController.isLoadingMore) {
|
||||
EasyThrottle.throttle('my-throttler', const Duration(seconds: 1), () {
|
||||
_bangumidController.isLoadingMore = true;
|
||||
await _bangumidController.onLoad();
|
||||
}
|
||||
_bangumidController.onLoad();
|
||||
});
|
||||
}
|
||||
|
||||
final ScrollDirection direction =
|
||||
@ -53,6 +58,12 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
@ -80,43 +91,64 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
'最近追番',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_futureBuilderFutureFollow =
|
||||
_bangumidController.queryBangumiFollow();
|
||||
});
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.refresh,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 258,
|
||||
child: FutureBuilder(
|
||||
future: _bangumidController.queryBangumiFollow(),
|
||||
future: _futureBuilderFutureFollow,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SizedBox();
|
||||
}
|
||||
Map data = snapshot.data as Map;
|
||||
List list = _bangumidController.bangumiFollowList;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _bangumidController
|
||||
.bangumiFollowList.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
width: Get.size.width / 3,
|
||||
height: 254,
|
||||
margin: EdgeInsets.only(
|
||||
left: StyleString.safeSpace,
|
||||
right: index ==
|
||||
_bangumidController
|
||||
.bangumiFollowList
|
||||
.length -
|
||||
1
|
||||
? StyleString.safeSpace
|
||||
: 0),
|
||||
child: BangumiCardV(
|
||||
bangumiItem: _bangumidController
|
||||
.bangumiFollowList[index],
|
||||
() => list.isNotEmpty
|
||||
? ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Container(
|
||||
width: Get.size.width / 3,
|
||||
height: 254,
|
||||
margin: EdgeInsets.only(
|
||||
left: StyleString.safeSpace,
|
||||
right: index ==
|
||||
_bangumidController
|
||||
.bangumiFollowList
|
||||
.length -
|
||||
1
|
||||
? StyleString.safeSpace
|
||||
: 0),
|
||||
child: BangumiCardV(
|
||||
bangumiItem: _bangumidController
|
||||
.bangumiFollowList[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox(
|
||||
child: Center(
|
||||
child: Text('还没有追番'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
@ -184,7 +216,8 @@ class _BangumiPageState extends State<BangumiPage>
|
||||
crossAxisSpacing: StyleString.cardSpace,
|
||||
// 列数
|
||||
crossAxisCount: 3,
|
||||
mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
|
||||
mainAxisExtent: Get.size.width / 3 / 0.65 +
|
||||
32 * MediaQuery.of(context).textScaleFactor,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
|
@ -1,6 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/pages/video/detail/index.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class BangumiPanel extends StatefulWidget {
|
||||
final List<EpisodeItem> pages;
|
||||
@ -22,82 +26,131 @@ class BangumiPanel extends StatefulWidget {
|
||||
|
||||
class _BangumiPanelState extends State<BangumiPanel> {
|
||||
late int currentIndex;
|
||||
final ScrollController listViewScrollCtr = ScrollController();
|
||||
final ScrollController listViewScrollCtr_2 = ScrollController();
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
dynamic userInfo;
|
||||
// 默认未开通
|
||||
int vipStatus = 0;
|
||||
late int cid;
|
||||
String heroTag = Get.arguments['heroTag'];
|
||||
late final VideoDetailController videoDetailCtr;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
|
||||
cid = widget.cid!;
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo != null) {
|
||||
vipStatus = userInfo.vipStatus;
|
||||
}
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
|
||||
videoDetailCtr.cid.listen((p0) {
|
||||
cid = p0;
|
||||
setState(() {});
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||
scrollToIndex();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
listViewScrollCtr.dispose();
|
||||
listViewScrollCtr_2.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void showBangumiPanel() {
|
||||
showBottomSheet(
|
||||
context: context,
|
||||
builder: (_) => Container(
|
||||
height: widget.sheetHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 200));
|
||||
listViewScrollCtr_2.animateTo(currentIndex * 56,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
curve: Curves.easeInOut);
|
||||
});
|
||||
// 在这里使用 setState 更新状态
|
||||
return Container(
|
||||
height: widget.sheetHeight,
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'合集(${widget.pages.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
AppBar(
|
||||
toolbarHeight: 45,
|
||||
automaticallyImplyLeading: false,
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'合集(${widget.pages.length})',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
titleSpacing: 10,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView.builder(
|
||||
controller: listViewScrollCtr_2,
|
||||
itemCount: widget.pages.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
changeFucCall(widget.pages[index], index);
|
||||
});
|
||||
},
|
||||
dense: false,
|
||||
leading: index == currentIndex
|
||||
? Image.asset(
|
||||
'assets/images/live.gif',
|
||||
color:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
height: 12,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
'第${index + 1}话 ${widget.pages[index].longTitle!}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: index == currentIndex
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: widget.pages[index].badge != null
|
||||
? Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
height: 20,
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
titleSpacing: 10,
|
||||
),
|
||||
Expanded(
|
||||
child: Material(
|
||||
child: ListView.builder(
|
||||
itemCount: widget.pages.length,
|
||||
itemBuilder: (context, index) {
|
||||
return ListTile(
|
||||
onTap: () => changeFucCall(widget.pages[index], index),
|
||||
dense: false,
|
||||
leading: index == currentIndex
|
||||
? Image.asset(
|
||||
'assets/images/live.gif',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 12,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
'第${index + 1}话 ${widget.pages[index].longTitle!}',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: index == currentIndex
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
trailing: widget.pages[index].badge != null
|
||||
? Image.asset(
|
||||
'assets/images/big-vip.png',
|
||||
height: 20,
|
||||
)
|
||||
: const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void changeFucCall(item, i) async {
|
||||
if (item.badge != null) {
|
||||
if (item.badge != null && vipStatus != 1) {
|
||||
SmartDialog.showToast('需要大会员');
|
||||
return;
|
||||
}
|
||||
@ -108,6 +161,15 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
||||
);
|
||||
currentIndex = i;
|
||||
setState(() {});
|
||||
scrollToIndex();
|
||||
}
|
||||
|
||||
void scrollToIndex() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// 在回调函数中获取更新后的状态
|
||||
listViewScrollCtr.animateTo(currentIndex * 150,
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@ -150,6 +212,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
||||
SizedBox(
|
||||
height: 60,
|
||||
child: ListView.builder(
|
||||
controller: listViewScrollCtr,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: widget.pages.length,
|
||||
itemExtent: 150,
|
||||
@ -222,87 +285,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
||||
);
|
||||
})),
|
||||
)
|
||||
// SingleChildScrollView(
|
||||
// padding: const EdgeInsets.only(top: 7, bottom: 7),
|
||||
// scrollDirection: Axis.horizontal,
|
||||
// child: ConstrainedBox(
|
||||
// constraints: BoxConstraints(
|
||||
// minWidth: MediaQuery.of(context).size.width,
|
||||
// ),
|
||||
// child: Row(
|
||||
// children: [
|
||||
// for (int i = 0; i < widget.pages.length; i++) ...[
|
||||
// Container(
|
||||
// width: 150,
|
||||
// margin: const EdgeInsets.only(right: 10),
|
||||
// child: Material(
|
||||
// color: Theme.of(context).colorScheme.onInverseSurface,
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// clipBehavior: Clip.hardEdge,
|
||||
// child: InkWell(
|
||||
// onTap: () => changeFucCall(widget.pages[i], i),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.symmetric(
|
||||
// vertical: 8, horizontal: 10),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Row(
|
||||
// children: [
|
||||
// if (i == currentIndex) ...[
|
||||
// Image.asset(
|
||||
// 'assets/images/live.gif',
|
||||
// color:
|
||||
// Theme.of(context).colorScheme.primary,
|
||||
// height: 12,
|
||||
// ),
|
||||
// const SizedBox(width: 6)
|
||||
// ],
|
||||
// Text(
|
||||
// '第${i + 1}话',
|
||||
// style: TextStyle(
|
||||
// fontSize: 13,
|
||||
// color: i == currentIndex
|
||||
// ? Theme.of(context)
|
||||
// .colorScheme
|
||||
// .primary
|
||||
// : Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onSurface),
|
||||
// ),
|
||||
// const SizedBox(width: 2),
|
||||
// if (widget.pages[i].badge != null) ...[
|
||||
// Image.asset(
|
||||
// 'assets/images/big-vip.png',
|
||||
// height: 16,
|
||||
// ),
|
||||
// ],
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 3),
|
||||
// Text(
|
||||
// widget.pages[i].longTitle!,
|
||||
// maxLines: 1,
|
||||
// style: TextStyle(
|
||||
// fontSize: 13,
|
||||
// color: i == currentIndex
|
||||
// ? Theme.of(context).colorScheme.primary
|
||||
// : Theme.of(context)
|
||||
// .colorScheme
|
||||
// .onSurface),
|
||||
// overflow: TextOverflow.ellipsis,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ]
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -29,9 +29,6 @@ class BangumiCardV extends StatelessWidget {
|
||||
return Card(
|
||||
elevation: 0,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: StyleString.mdRadius,
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
child: GestureDetector(
|
||||
// onLongPress: () {
|
||||
@ -149,7 +146,6 @@ class BangumiContent extends StatelessWidget {
|
||||
bangumiItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
@ -158,6 +154,7 @@ class BangumiContent extends StatelessWidget {
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
if (bangumiItem.indexShow != null)
|
||||
Text(
|
||||
bangumiItem.indexShow,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
@ -46,6 +47,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
List<int> blackMidsList =
|
||||
_blackListController.blackList.map<int>((e) => e.mid!).toList();
|
||||
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -59,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
centerTitle: false,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'黑名单管理 (${_blackListController.blackList.length} / 5000)',
|
||||
'黑名单管理 - ${_blackListController.total.value}',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
@ -103,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
dense: true,
|
||||
// trailing: TextButton(
|
||||
// onPressed: () {},
|
||||
// child: const Text('移除'),
|
||||
// ),
|
||||
trailing: TextButton(
|
||||
onPressed: () => _blackListController
|
||||
.removeBlack(list[index].mid),
|
||||
child: const Text('移除'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -135,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
||||
class BlackListController extends GetxController {
|
||||
int currentPage = 1;
|
||||
int pageSize = 50;
|
||||
RxInt total = 0.obs;
|
||||
RxList<BlackListItem> blackList = [BlackListItem()].obs;
|
||||
|
||||
Future queryBlacklist({type = 'init'}) async {
|
||||
@ -145,6 +149,7 @@ class BlackListController extends GetxController {
|
||||
if (result['status']) {
|
||||
if (type == 'init') {
|
||||
blackList.value = result['data'].list;
|
||||
total.value = result['data'].total;
|
||||
} else {
|
||||
blackList.addAll(result['data'].list);
|
||||
}
|
||||
@ -153,4 +158,13 @@ class BlackListController extends GetxController {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future removeBlack(mid) async {
|
||||
var result = await BlackHttp.removeBlack(fid: mid);
|
||||
if (result['status']) {
|
||||
blackList.removeWhere((e) => e.mid == mid);
|
||||
total.value = total.value - 1;
|
||||
SmartDialog.showToast(result['msg']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
74
lib/pages/danmaku/controller.dart
Normal file
74
lib/pages/danmaku/controller.dart
Normal file
@ -0,0 +1,74 @@
|
||||
import 'package:pilipala/http/danmaku.dart';
|
||||
import 'package:pilipala/models/danmaku/dm.pb.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
|
||||
class PlDanmakuController {
|
||||
PlDanmakuController(this.cid, this.playerController);
|
||||
final int cid;
|
||||
final PlPlayerController playerController;
|
||||
late Duration videoDuration;
|
||||
// 按 6min 分段
|
||||
int segCount = 0;
|
||||
List<DmSegMobileReply> dmSegList = [];
|
||||
int currentSegIndex = 1;
|
||||
int currentDmIndex = 0;
|
||||
|
||||
void calcSegment() {
|
||||
dmSegList.clear();
|
||||
// 视频分段数
|
||||
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
|
||||
dmSegList = List<DmSegMobileReply>.generate(
|
||||
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
|
||||
// 当前分段
|
||||
try {
|
||||
currentSegIndex =
|
||||
(playerController.position.value.inSeconds / (60 * 6)).ceil();
|
||||
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
Future<List<DmSegMobileReply>> queryDanmaku() async {
|
||||
// dmSegList.clear();
|
||||
DmSegMobileReply result =
|
||||
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex);
|
||||
if (result.elems.isNotEmpty) {
|
||||
result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
|
||||
// dmSegList.add(result);
|
||||
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
|
||||
dmSegList[currentSegIndex - 1] = result;
|
||||
}
|
||||
if (dmSegList.isNotEmpty) {
|
||||
findClosestPositionIndex(playerController.position.value.inMilliseconds);
|
||||
}
|
||||
return dmSegList;
|
||||
}
|
||||
|
||||
/// 查询当前最接近的弹幕
|
||||
void findClosestPositionIndex(int position) {
|
||||
int segIndex = (position / (6 * 60 * 1000)).ceil() - 1;
|
||||
if (segIndex < 0) segIndex = 0;
|
||||
List elems = dmSegList[segIndex].elems;
|
||||
|
||||
if (segIndex < dmSegList.length) {
|
||||
int left = 0;
|
||||
int right = elems.length;
|
||||
|
||||
while (left < right) {
|
||||
int mid = (right + left) ~/ 2;
|
||||
var midPosition = elems[mid].progress;
|
||||
|
||||
if (midPosition >= position) {
|
||||
right = mid;
|
||||
} else {
|
||||
left = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
currentSegIndex = segIndex;
|
||||
currentDmIndex = right;
|
||||
} else {
|
||||
currentSegIndex = segIndex;
|
||||
currentDmIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
4
lib/pages/danmaku/index.dart
Normal file
4
lib/pages/danmaku/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library pldanmaku;
|
||||
|
||||
export './controller.dart';
|
||||
export 'view.dart';
|
178
lib/pages/danmaku/view.dart
Normal file
178
lib/pages/danmaku/view.dart
Normal file
@ -0,0 +1,178 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||
import 'package:pilipala/pages/danmaku/index.dart';
|
||||
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||
import 'package:pilipala/utils/danmaku.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
/// 传入播放器控制器,监听播放进度,加载对应弹幕
|
||||
class PlDanmaku extends StatefulWidget {
|
||||
final int cid;
|
||||
final PlPlayerController playerController;
|
||||
|
||||
const PlDanmaku({
|
||||
super.key,
|
||||
required this.cid,
|
||||
required this.playerController,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PlDanmaku> createState() => _PlDanmakuState();
|
||||
}
|
||||
|
||||
class _PlDanmakuState extends State<PlDanmaku> {
|
||||
late PlPlayerController playerController;
|
||||
late PlDanmakuController _plDanmakuController;
|
||||
DanmakuController? _controller;
|
||||
bool danmuPlayStatus = true;
|
||||
Box setting = GStrorage.setting;
|
||||
late bool enableShowDanmaku;
|
||||
late List blockTypes;
|
||||
late double showArea;
|
||||
late double opacityVal;
|
||||
late double fontSizeVal;
|
||||
late double danmakuSpeedVal;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
enableShowDanmaku =
|
||||
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
||||
_plDanmakuController =
|
||||
PlDanmakuController(widget.cid, widget.playerController);
|
||||
if (mounted) {
|
||||
playerController = widget.playerController;
|
||||
_plDanmakuController.videoDuration = playerController.duration.value;
|
||||
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
|
||||
_plDanmakuController
|
||||
..calcSegment()
|
||||
..queryDanmaku();
|
||||
}
|
||||
playerController
|
||||
..addStatusLister(playerListener)
|
||||
..addPositionListener(videoPositionListen);
|
||||
}
|
||||
playerController.isOpenDanmu.listen((p0) {
|
||||
if (p0) {
|
||||
if (_plDanmakuController.dmSegList.isEmpty) {
|
||||
_plDanmakuController
|
||||
..calcSegment()
|
||||
..queryDanmaku();
|
||||
}
|
||||
}
|
||||
});
|
||||
blockTypes = playerController.blockTypes;
|
||||
showArea = playerController.showArea;
|
||||
opacityVal = playerController.opacityVal;
|
||||
fontSizeVal = playerController.fontSizeVal;
|
||||
danmakuSpeedVal = playerController.danmakuSpeedVal;
|
||||
}
|
||||
|
||||
// 播放器状态监听
|
||||
void playerListener(PlayerStatus? status) {
|
||||
if (status == PlayerStatus.paused) {
|
||||
_controller!.pause();
|
||||
}
|
||||
if (status == PlayerStatus.playing) {
|
||||
_controller!.onResume();
|
||||
}
|
||||
}
|
||||
|
||||
void videoPositionListen(Duration position) {
|
||||
if (!danmuPlayStatus) {
|
||||
_controller!.onResume();
|
||||
danmuPlayStatus = true;
|
||||
}
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
return;
|
||||
}
|
||||
PlDanmakuController ctr = _plDanmakuController;
|
||||
int currentPosition = position.inMilliseconds;
|
||||
blockTypes = playerController.blockTypes;
|
||||
// 根据position判断是否有已缓存弹幕。没有则请求对应段
|
||||
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
|
||||
segIndex = segIndex < 1 ? 1 : segIndex;
|
||||
if (ctr.dmSegList[segIndex - 1].elems.isEmpty) {
|
||||
ctr.currentSegIndex = segIndex;
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
ctr.queryDanmaku();
|
||||
});
|
||||
}
|
||||
// 超出分段数返回
|
||||
if (ctr.currentSegIndex >= ctr.dmSegList.length) {
|
||||
return;
|
||||
}
|
||||
if (ctr.dmSegList.isEmpty ||
|
||||
ctr.dmSegList[ctr.currentSegIndex].elems.isEmpty) {
|
||||
return;
|
||||
}
|
||||
// 超出当前分段的弹幕总数返回
|
||||
if (ctr.currentDmIndex >= ctr.dmSegList[ctr.currentSegIndex].elems.length) {
|
||||
ctr.currentDmIndex = 0;
|
||||
ctr.currentSegIndex++;
|
||||
return;
|
||||
}
|
||||
var element = ctr.dmSegList[ctr.currentSegIndex].elems[ctr.currentDmIndex];
|
||||
var delta = currentPosition - element.progress;
|
||||
|
||||
if (delta >= 0 && delta < 200) {
|
||||
// 屏蔽彩色弹幕
|
||||
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
|
||||
_controller!.addItems([
|
||||
DanmakuItem(
|
||||
element.content,
|
||||
color: DmUtils.decimalToColor(element.color),
|
||||
time: element.progress,
|
||||
type: DmUtils.getPosition(element.mode),
|
||||
)
|
||||
]);
|
||||
}
|
||||
ctr.currentDmIndex++;
|
||||
} else {
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
_controller!.pause();
|
||||
danmuPlayStatus = false;
|
||||
return;
|
||||
}
|
||||
ctr.findClosestPositionIndex(position.inMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
playerController.removePositionListener(videoPositionListen);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, box) {
|
||||
double initDuration = box.maxWidth / 12;
|
||||
return Obx(
|
||||
() => AnimatedOpacity(
|
||||
opacity: playerController.isOpenDanmu.value ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child: DanmakuView(
|
||||
createdController: (DanmakuController e) async {
|
||||
widget.playerController.danmakuController = _controller = e;
|
||||
},
|
||||
option: DanmakuOption(
|
||||
fontSize: 15 * fontSizeVal,
|
||||
area: showArea,
|
||||
opacity: opacityVal,
|
||||
hideTop: blockTypes.contains(5),
|
||||
hideScroll: blockTypes.contains(2),
|
||||
hideBottom: blockTypes.contains(4),
|
||||
duration: initDuration /
|
||||
(danmakuSpeedVal * widget.playerController.playbackSpeed),
|
||||
),
|
||||
statusChanged: (isPlaying) {},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -3,13 +3,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/dynamics.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/common/dynamics_type.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
import 'package:pilipala/models/live/item.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class DynamicsController extends GetxController {
|
||||
int page = 1;
|
||||
@ -45,19 +51,47 @@ class DynamicsController extends GetxController {
|
||||
},
|
||||
];
|
||||
bool flag = false;
|
||||
RxInt initialValue = 1.obs;
|
||||
RxInt initialValue = 0.obs;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
RxBool userLogin = false.obs;
|
||||
var userInfo;
|
||||
RxBool isLoadingDynamic = false.obs;
|
||||
Box setting = GStrorage.setting;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
userLogin.value = userInfo != null;
|
||||
super.onInit();
|
||||
initialValue.value =
|
||||
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
|
||||
dynamicsType = DynamicsType.values[initialValue.value].obs;
|
||||
}
|
||||
|
||||
Future queryFollowDynamic({type = 'init'}) async {
|
||||
if (!userLogin.value) {
|
||||
return {'status': false, 'msg': '账号未登录'};
|
||||
}
|
||||
if (type == 'init') {
|
||||
dynamicsList.clear();
|
||||
}
|
||||
// 下拉刷新数据渲染时会触发onLoad
|
||||
if (type == 'onLoad' && page == 1) {
|
||||
return;
|
||||
}
|
||||
isLoadingDynamic.value = true;
|
||||
var res = await DynamicsHttp.followDynamic(
|
||||
page: type == 'init' ? 1 : page,
|
||||
type: dynamicsType.value.values,
|
||||
offset: offset,
|
||||
mid: mid.value,
|
||||
);
|
||||
isLoadingDynamic.value = false;
|
||||
if (res['status']) {
|
||||
if (type == 'onLoad' && res['data'].items.isEmpty) {
|
||||
SmartDialog.showToast('没有更多了');
|
||||
return;
|
||||
}
|
||||
if (type == 'init') {
|
||||
dynamicsList.value = res['data'].items;
|
||||
} else {
|
||||
@ -70,7 +104,7 @@ class DynamicsController extends GetxController {
|
||||
}
|
||||
|
||||
onSelectType(value) async {
|
||||
dynamicsType.value = filterTypeList[value - 1]['value'];
|
||||
dynamicsType.value = filterTypeList[value]['value'];
|
||||
dynamicsList.value = [DynamicItemModel()];
|
||||
page = 1;
|
||||
initialValue.value = value;
|
||||
@ -80,16 +114,21 @@ class DynamicsController extends GetxController {
|
||||
|
||||
pushDetail(item, floor, {action = 'all'}) async {
|
||||
feedBack();
|
||||
|
||||
/// 点击评论action 直接查看评论
|
||||
if (action == 'comment') {
|
||||
Get.toNamed('/dynamicDetail',
|
||||
arguments: {'item': item, 'floor': floor, 'action': action});
|
||||
return false;
|
||||
}
|
||||
switch (item!.type) {
|
||||
/// 转发的动态
|
||||
case 'DYNAMIC_TYPE_FORWARD':
|
||||
Get.toNamed('/dynamicDetail',
|
||||
arguments: {'item': item, 'floor': floor});
|
||||
break;
|
||||
|
||||
/// 图文动态查看
|
||||
case 'DYNAMIC_TYPE_DRAW':
|
||||
Get.toNamed('/dynamicDetail',
|
||||
arguments: {'item': item, 'floor': floor});
|
||||
@ -105,17 +144,42 @@ class DynamicsController extends GetxController {
|
||||
SmartDialog.showToast(err.toString());
|
||||
}
|
||||
break;
|
||||
|
||||
/// 专栏文章查看
|
||||
case 'DYNAMIC_TYPE_ARTICLE':
|
||||
String title = item.modules.moduleDynamic.major.opus.title;
|
||||
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title},
|
||||
);
|
||||
if (url.contains('opus') || url.contains('read')) {
|
||||
RegExp digitRegExp = RegExp(r'\d+');
|
||||
Iterable<Match> matches = digitRegExp.allMatches(url);
|
||||
String number = matches.first.group(0)!;
|
||||
if (url.contains('read')) {
|
||||
number = 'cv$number';
|
||||
}
|
||||
Get.toNamed('/htmlRender', parameters: {
|
||||
'url': url.startsWith('//') ? url.split('//').last : url,
|
||||
'title': title,
|
||||
'id': number,
|
||||
'dynamicType': url.split('//').last.split('/')[1]
|
||||
});
|
||||
} else {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': 'https:$url',
|
||||
'type': 'note',
|
||||
'pageTitle': title
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'DYNAMIC_TYPE_PGC':
|
||||
print('番剧');
|
||||
SmartDialog.showToast('暂未支持的类型,请联系开发者');
|
||||
break;
|
||||
|
||||
/// 纯文字动态查看
|
||||
case 'DYNAMIC_TYPE_WORD':
|
||||
print('纯文本');
|
||||
Get.toNamed('/dynamicDetail',
|
||||
@ -139,16 +203,61 @@ class DynamicsController extends GetxController {
|
||||
});
|
||||
break;
|
||||
|
||||
/// TODO
|
||||
/// 合集查看
|
||||
case 'DYNAMIC_TYPE_UGC_SEASON':
|
||||
print('合集');
|
||||
DynamicArchiveModel ugcSeason =
|
||||
item.modules.moduleDynamic.major.ugcSeason;
|
||||
int aid = ugcSeason.aid!;
|
||||
String bvid = IdUtils.av2bv(aid);
|
||||
String cover = ugcSeason.cover!;
|
||||
int cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
Get.toNamed('/video?bvid=$bvid&cid=$cid',
|
||||
arguments: {'pic': cover, 'heroTag': bvid});
|
||||
break;
|
||||
|
||||
/// 番剧查看
|
||||
case 'DYNAMIC_TYPE_PGC_UNION':
|
||||
print('DYNAMIC_TYPE_PGC_UNION 番剧');
|
||||
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
|
||||
if (pgc.epid != null) {
|
||||
SmartDialog.showLoading(msg: '获取中...');
|
||||
var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
|
||||
SmartDialog.dismiss();
|
||||
if (res['status']) {
|
||||
EpisodeItem episode = res['data'].episodes.first;
|
||||
String bvid = episode.bvid!;
|
||||
int cid = episode.cid!;
|
||||
String pic = episode.cover!;
|
||||
String heroTag = Utils.makeHeroTag(cid);
|
||||
Get.toNamed(
|
||||
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
|
||||
arguments: {
|
||||
'pic': pic,
|
||||
'heroTag': heroTag,
|
||||
'videoType': SearchType.media_bangumi,
|
||||
'bangumiItem': res['data'],
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future queryFollowUp() async {
|
||||
Future queryFollowUp({type = 'init'}) async {
|
||||
if (!userLogin.value) {
|
||||
return {'status': false, 'msg': '账号未登录'};
|
||||
}
|
||||
if (type == 'init') {
|
||||
upData.value.upList = [];
|
||||
upData.value.liveUsers = LiveUsers();
|
||||
}
|
||||
var res = await DynamicsHttp.followUp();
|
||||
if (res['status']) {
|
||||
upData.value = res['data'];
|
||||
if (upData.value.upList!.isEmpty) {
|
||||
mid.value = -1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@ -162,7 +271,8 @@ class DynamicsController extends GetxController {
|
||||
|
||||
onRefresh() async {
|
||||
page = 1;
|
||||
queryFollowUp();
|
||||
print('onRefresh');
|
||||
await queryFollowUp();
|
||||
await queryFollowDynamic();
|
||||
}
|
||||
|
||||
@ -181,8 +291,8 @@ class DynamicsController extends GetxController {
|
||||
void resetSearch() {
|
||||
mid.value = -1;
|
||||
dynamicsType.value = DynamicsType.values[0];
|
||||
initialValue.value = 1;
|
||||
SmartDialog.showToast('还原默认加载', alignment: Alignment.topCenter);
|
||||
initialValue.value = 0;
|
||||
SmartDialog.showToast('还原默认加载');
|
||||
dynamicsList.value = [DynamicItemModel()];
|
||||
queryFollowDynamic();
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/reply.dart';
|
||||
import 'package:pilipala/models/common/reply_sort_type.dart';
|
||||
import 'package:pilipala/models/video/reply/item.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class DynamicDetailController extends GetxController {
|
||||
DynamicDetailController(this.oid, this.type);
|
||||
@ -15,10 +18,12 @@ class DynamicDetailController extends GetxController {
|
||||
RxString noMore = ''.obs;
|
||||
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
|
||||
RxInt acount = 0.obs;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
||||
ReplySortType sortType = ReplySortType.time;
|
||||
ReplySortType _sortType = ReplySortType.time;
|
||||
RxString sortTypeTitle = ReplySortType.time.titles.obs;
|
||||
RxString sortTypeLabel = ReplySortType.time.labels.obs;
|
||||
Box setting = GStrorage.setting;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -29,6 +34,11 @@ class DynamicDetailController extends GetxController {
|
||||
acount.value =
|
||||
int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');
|
||||
}
|
||||
int deaultReplySortIndex =
|
||||
setting.get(SettingBoxKey.replySortType, defaultValue: 0);
|
||||
_sortType = ReplySortType.values[deaultReplySortIndex];
|
||||
sortTypeTitle.value = _sortType.titles;
|
||||
sortTypeLabel.value = _sortType.labels;
|
||||
}
|
||||
|
||||
Future queryReplyList({reqType = 'init'}) async {
|
||||
@ -39,7 +49,7 @@ class DynamicDetailController extends GetxController {
|
||||
oid: oid!,
|
||||
pageNum: currentPage + 1,
|
||||
type: type!,
|
||||
sort: sortType.index,
|
||||
sort: _sortType.index,
|
||||
);
|
||||
if (res['status']) {
|
||||
List<ReplyItemModel> replies = res['data'].replies;
|
||||
@ -76,20 +86,20 @@ class DynamicDetailController extends GetxController {
|
||||
// 排序搜索评论
|
||||
queryBySort() {
|
||||
feedBack();
|
||||
switch (sortType) {
|
||||
switch (_sortType) {
|
||||
case ReplySortType.time:
|
||||
sortType = ReplySortType.like;
|
||||
_sortType = ReplySortType.like;
|
||||
break;
|
||||
case ReplySortType.like:
|
||||
sortType = ReplySortType.reply;
|
||||
_sortType = ReplySortType.reply;
|
||||
break;
|
||||
case ReplySortType.reply:
|
||||
sortType = ReplySortType.time;
|
||||
_sortType = ReplySortType.time;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
sortTypeTitle.value = sortType.titles;
|
||||
sortTypeLabel.value = sortType.labels;
|
||||
sortTypeTitle.value = _sortType.titles;
|
||||
sortTypeLabel.value = _sortType.labels;
|
||||
replyList.clear();
|
||||
queryReplyList(reqType: 'init');
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_reply.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
@ -8,7 +10,10 @@ import 'package:pilipala/models/common/reply_type.dart';
|
||||
import 'package:pilipala/pages/dynamics/deatil/index.dart';
|
||||
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
|
||||
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
|
||||
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
|
||||
import '../widgets/dynamic_panel.dart';
|
||||
|
||||
@ -20,15 +25,18 @@ class DynamicDetailPage extends StatefulWidget {
|
||||
State<DynamicDetailPage> createState() => _DynamicDetailPageState();
|
||||
}
|
||||
|
||||
class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
late DynamicDetailController? _dynamicDetailController;
|
||||
class _DynamicDetailPageState extends State<DynamicDetailPage>
|
||||
with TickerProviderStateMixin {
|
||||
late DynamicDetailController _dynamicDetailController;
|
||||
late AnimationController fabAnimationCtr;
|
||||
Future? _futureBuilderFuture;
|
||||
late StreamController<bool> titleStreamC; // appBar title
|
||||
final ScrollController scrollController = ScrollController();
|
||||
late ScrollController scrollController;
|
||||
bool _visibleTitle = false;
|
||||
String? action;
|
||||
// 回复类型
|
||||
late int type;
|
||||
bool _isFabVisible = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -37,38 +45,42 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
// floor 1原创 2转发
|
||||
if (Get.arguments['floor'] == 1) {
|
||||
oid = int.parse(Get.arguments['item'].basic!['comment_id_str']);
|
||||
print(oid);
|
||||
} else {
|
||||
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
|
||||
try {
|
||||
String type = Get.arguments['item'].modules.moduleDynamic.major.type;
|
||||
|
||||
/// TODO
|
||||
if (type == 'MAJOR_TYPE_OPUS') {
|
||||
} else {
|
||||
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
type = Get.arguments['item'].basic!['comment_type'];
|
||||
int commentType = 11;
|
||||
try {
|
||||
commentType = Get.arguments['item'].basic!['comment_type'];
|
||||
} catch (_) {}
|
||||
type = (commentType == 0) ? 11 : commentType;
|
||||
|
||||
action =
|
||||
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
|
||||
_dynamicDetailController = Get.put(DynamicDetailController(oid, type));
|
||||
_futureBuilderFuture = _dynamicDetailController!.queryReplyList();
|
||||
_dynamicDetailController =
|
||||
Get.put(DynamicDetailController(oid, type), tag: oid.toString());
|
||||
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
|
||||
titleStreamC = StreamController<bool>();
|
||||
scrollController.addListener(_listen);
|
||||
if (action == 'comment') {
|
||||
_visibleTitle = true;
|
||||
titleStreamC.add(true);
|
||||
}
|
||||
}
|
||||
|
||||
void _listen() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
if (!_dynamicDetailController!.isLoadingMore) {
|
||||
_dynamicDetailController!.isLoadingMore = true;
|
||||
await _dynamicDetailController!.queryReplyList(reqType: 'onLoad');
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollController.offset > 55 && !_visibleTitle) {
|
||||
_visibleTitle = true;
|
||||
titleStreamC.add(true);
|
||||
} else if (scrollController.offset <= 55 && _visibleTitle) {
|
||||
_visibleTitle = false;
|
||||
titleStreamC.add(false);
|
||||
}
|
||||
fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
fabAnimationCtr.forward();
|
||||
// 滚动事件监听
|
||||
scrollListener();
|
||||
}
|
||||
|
||||
void replyReply(replyItem) {
|
||||
@ -95,6 +107,61 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
void scrollListener() {
|
||||
scrollController = _dynamicDetailController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
// 分页加载
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_dynamicDetailController.queryReplyList(reqType: 'onLoad');
|
||||
});
|
||||
}
|
||||
|
||||
// 标题
|
||||
if (scrollController.offset > 55 && !_visibleTitle) {
|
||||
_visibleTitle = true;
|
||||
titleStreamC.add(true);
|
||||
} else if (scrollController.offset <= 55 && _visibleTitle) {
|
||||
_visibleTitle = false;
|
||||
titleStreamC.add(false);
|
||||
}
|
||||
|
||||
// fab按钮
|
||||
final ScrollDirection direction =
|
||||
scrollController.position.userScrollDirection;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
_showFab();
|
||||
} else if (direction == ScrollDirection.reverse) {
|
||||
_hideFab();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showFab() {
|
||||
if (!_isFabVisible) {
|
||||
_isFabVisible = true;
|
||||
fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void _hideFab() {
|
||||
if (_isFabVisible) {
|
||||
_isFabVisible = false;
|
||||
fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
fabAnimationCtr.dispose();
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -110,7 +177,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
return AnimatedOpacity(
|
||||
opacity: snapshot.data ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: author(_dynamicDetailController!.item, context),
|
||||
child: AuthorPanel(item: _dynamicDetailController.item),
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -118,150 +185,206 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await _dynamicDetailController!.queryReplyList();
|
||||
await _dynamicDetailController.queryReplyList();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
if (action != 'comment')
|
||||
SliverToBoxAdapter(
|
||||
child: DynamicPanel(
|
||||
item: _dynamicDetailController!.item,
|
||||
source: 'detail',
|
||||
),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
delegate: _MySliverPersistentHeaderDelegate(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 0.6,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.05),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
if (action != 'comment')
|
||||
SliverToBoxAdapter(
|
||||
child: DynamicPanel(
|
||||
item: _dynamicDetailController.item,
|
||||
source: 'detail',
|
||||
),
|
||||
),
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${_dynamicDetailController!.acount.value}',
|
||||
key: ValueKey<int>(
|
||||
_dynamicDetailController!.acount.value),
|
||||
SliverPersistentHeader(
|
||||
delegate: _MySliverPersistentHeaderDelegate(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 0.6,
|
||||
color: Theme.of(context)
|
||||
.dividerColor
|
||||
.withOpacity(0.05),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('条回复'),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
_dynamicDetailController!.queryBySort(),
|
||||
icon: const Icon(Icons.sort, size: 16),
|
||||
label: Obx(() => Text(
|
||||
_dynamicDetailController!.sortTypeLabel.value,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => _dynamicDetailController!.replyList.isEmpty &&
|
||||
_dynamicDetailController!.isLoadingMore
|
||||
? SliverList(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index ==
|
||||
_dynamicDetailController!
|
||||
.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_dynamicDetailController!
|
||||
.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ReplyItem(
|
||||
replyItem: _dynamicDetailController!
|
||||
.replyList[index],
|
||||
showReplyRow: true,
|
||||
replyLevel: '1',
|
||||
replyReply: (replyItem) =>
|
||||
replyReply(replyItem),
|
||||
replyType: ReplyType.values[type],
|
||||
);
|
||||
}
|
||||
},
|
||||
childCount:
|
||||
_dynamicDetailController!.replyList.length +
|
||||
1,
|
||||
height: 45,
|
||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Obx(
|
||||
() => AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder:
|
||||
(Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
'${_dynamicDetailController.acount.value}',
|
||||
key: ValueKey<int>(
|
||||
_dynamicDetailController.acount.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Text('条回复'),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
height: 35,
|
||||
child: TextButton.icon(
|
||||
onPressed: () =>
|
||||
_dynamicDetailController.queryBySort(),
|
||||
icon: const Icon(Icons.sort, size: 16),
|
||||
label: Obx(() => Text(
|
||||
_dynamicDetailController
|
||||
.sortTypeLabel.value,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
pinned: true,
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (snapshot.data['status']) {
|
||||
// 请求成功
|
||||
return Obx(
|
||||
() => _dynamicDetailController.replyList.isEmpty &&
|
||||
_dynamicDetailController.isLoadingMore
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index ==
|
||||
_dynamicDetailController
|
||||
.replyList.length) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
100,
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_dynamicDetailController
|
||||
.noMore.value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return ReplyItem(
|
||||
replyItem: _dynamicDetailController
|
||||
.replyList[index],
|
||||
showReplyRow: true,
|
||||
replyLevel: '1',
|
||||
replyReply: (replyItem) =>
|
||||
replyReply(replyItem),
|
||||
replyType: ReplyType.values[type],
|
||||
addReply: (replyItem) {
|
||||
_dynamicDetailController
|
||||
.replyList[index].replies!
|
||||
.add(replyItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
childCount: _dynamicDetailController
|
||||
.replyList.length +
|
||||
1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 14,
|
||||
right: 14,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 2),
|
||||
end: const Offset(0, 0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: fabAnimationCtr,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
return VideoReplyNewDialog(
|
||||
oid: _dynamicDetailController.oid ??
|
||||
IdUtils.bv2av(Get.parameters['bvid']!),
|
||||
root: 0,
|
||||
parent: 0,
|
||||
replyType: ReplyType.values[type],
|
||||
);
|
||||
},
|
||||
).then(
|
||||
(value) => {
|
||||
// 完成评论,数据添加
|
||||
if (value != null && value['data'] != null)
|
||||
{
|
||||
_dynamicDetailController.replyList
|
||||
.add(value['data']),
|
||||
_dynamicDetailController.acount.value++
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
// 请求错误
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoReplySkeleton();
|
||||
}, childCount: 8),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
tooltip: '评论动态',
|
||||
child: const Icon(Icons.reply),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -1,12 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/skeleton/dynamic_card.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/pages/main/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
@ -28,8 +30,8 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
final DynamicsController _dynamicsController = Get.put(DynamicsController());
|
||||
late Future _futureBuilderFuture;
|
||||
late Future _futureBuilderFutureUp;
|
||||
bool _isLoadingMore = false;
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
@ -39,18 +41,17 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
super.initState();
|
||||
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
|
||||
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
|
||||
ScrollController scrollController = _dynamicsController.scrollController;
|
||||
scrollController = _dynamicsController.scrollController;
|
||||
StreamController<bool> mainStream =
|
||||
Get.find<MainController>().bottomBarStream;
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (!_isLoadingMore) {
|
||||
_isLoadingMore = true;
|
||||
await _dynamicsController.queryFollowDynamic(type: 'onLoad');
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
EasyThrottle.throttle(
|
||||
'queryFollowDynamic', const Duration(seconds: 1), () {
|
||||
_dynamicsController.queryFollowDynamic(type: 'onLoad');
|
||||
});
|
||||
}
|
||||
|
||||
final ScrollDirection direction =
|
||||
@ -62,6 +63,21 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_dynamicsController.userLogin.listen((status) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
|
||||
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -107,73 +123,82 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
return const SizedBox();
|
||||
}
|
||||
}),
|
||||
Obx(() => Visibility(
|
||||
visible: _dynamicsController.mid.value == -1,
|
||||
child: CustomSlidingSegmentedControl<int>(
|
||||
initialValue: _dynamicsController.initialValue.value,
|
||||
children: {
|
||||
1: Text(
|
||||
'全部',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
Obx(
|
||||
() => _dynamicsController.userLogin.value
|
||||
? Visibility(
|
||||
visible: _dynamicsController.mid.value == -1,
|
||||
child: CustomSlidingSegmentedControl<int>(
|
||||
initialValue:
|
||||
_dynamicsController.initialValue.value,
|
||||
children: {
|
||||
0: Text(
|
||||
'全部',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
),
|
||||
1: Text('投稿',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize)),
|
||||
2: Text('番剧',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize)),
|
||||
3: Text('专栏',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize)),
|
||||
},
|
||||
padding: 13.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
thumbDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
onValueChanged: (v) {
|
||||
feedBack();
|
||||
_dynamicsController.onSelectType(v);
|
||||
},
|
||||
),
|
||||
2: Text('投稿',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize)),
|
||||
3: Text('番剧',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize)),
|
||||
// 4: Text(
|
||||
// '专栏',
|
||||
// style: TextStyle(
|
||||
// fontSize: Theme.of(context)
|
||||
// .textTheme
|
||||
// .labelMedium!
|
||||
// .fontSize),
|
||||
// ),
|
||||
},
|
||||
padding: 13.0,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.surfaceVariant
|
||||
.withOpacity(0.7),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
thumbDecoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
onValueChanged: (v) {
|
||||
feedBack();
|
||||
_dynamicsController.onSelectType(v);
|
||||
},
|
||||
),
|
||||
))
|
||||
)
|
||||
: Text('动态',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () =>
|
||||
{feedBack(), _dynamicsController.resetSearch()},
|
||||
icon: const Icon(Icons.history, size: 21),
|
||||
),
|
||||
),
|
||||
// Obx(
|
||||
// () => Visibility(
|
||||
// visible: _dynamicsController.userLogin.value,
|
||||
// child: Positioned(
|
||||
// right: 4,
|
||||
// top: 0,
|
||||
// bottom: 0,
|
||||
// child: IconButton(
|
||||
// padding: EdgeInsets.zero,
|
||||
// onPressed: () =>
|
||||
// {feedBack(), _dynamicsController.resetSearch()},
|
||||
// icon: const Icon(Icons.history, size: 21),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -187,6 +212,9 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
future: _futureBuilderFutureUp,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
return Obx(() => UpPanel(_dynamicsController.upData.value));
|
||||
@ -207,24 +235,44 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
List<DynamicItemModel> list =
|
||||
_dynamicsController.dynamicsList;
|
||||
return Obx(
|
||||
() => list.isEmpty
|
||||
? skeleton()
|
||||
: SliverList(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((context, index) {
|
||||
() {
|
||||
if (list.isEmpty) {
|
||||
if (_dynamicsController.isLoadingDynamic.value) {
|
||||
return skeleton();
|
||||
} else {
|
||||
return const NoData();
|
||||
}
|
||||
} else {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return DynamicPanel(item: list[index]);
|
||||
}, childCount: list.length),
|
||||
},
|
||||
childCount: list.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => _dynamicsController.onRefresh(),
|
||||
fn: () {
|
||||
setState(() {
|
||||
_futureBuilderFuture =
|
||||
_dynamicsController.queryFollowDynamic();
|
||||
_futureBuilderFutureUp =
|
||||
_dynamicsController.queryFollowUp();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -233,6 +281,7 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
}
|
||||
},
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 40))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user