Compare commits
200 Commits
v1.0.1.081
...
feature-bu
Author | SHA1 | Date | |
---|---|---|---|
2ece96df21 | |||
c11c5695a2 | |||
93581c2932 | |||
fd43a8cb31 | |||
6f34bacb64 | |||
90314f89ed | |||
45c53de2c2 | |||
5c07cb4545 | |||
844053b138 | |||
6af8b91f63 | |||
8aa02f7450 | |||
38a1f2e1f7 | |||
b2e1d98f51 | |||
e19cf92992 | |||
80e10aeaad | |||
b1a9152a49 | |||
935b7577b3 | |||
fd5c4463d2 | |||
7fcbe4dd9d | |||
9a0c9f4021 | |||
6b3773a074 | |||
7be8ebaa7e | |||
092b1cee3d | |||
252f39e8c7 | |||
e631ca04a0 | |||
3665d6a0f6 | |||
e3c9e8c13b | |||
39995bae23 | |||
f42d0d01ea | |||
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 | |||
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>
|
||||
|
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 |
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上查看
|
||||
问题反馈、功能建议请查看「关于」页面。
|
@ -1,10 +1,14 @@
|
||||
PODS:
|
||||
- appscheme (1.0.4):
|
||||
- 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)
|
||||
@ -31,6 +35,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,9 +51,11 @@ PODS:
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/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`)
|
||||
@ -56,6 +66,8 @@ DEPENDENCIES:
|
||||
- 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,12 +80,16 @@ SPEC REPOS:
|
||||
- ReachabilitySwift
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
appscheme:
|
||||
:path: ".symlinks/plugins/appscheme/ios"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_volume_controller:
|
||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||
image_gallery_saver:
|
||||
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
||||
media_kit_libs_ios_video:
|
||||
@ -94,6 +110,10 @@ EXTERNAL SOURCES:
|
||||
: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,9 +126,11 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
||||
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
|
||||
@ -121,6 +143,8 @@ SPEC CHECKSUMS:
|
||||
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;
|
||||
}
|
136
lib/common/widgets/html_render.dart
Normal file
136
lib/common/widgets/html_render.dart
Normal file
@ -0,0 +1,136 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_html/flutter_html.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,
|
||||
// tagsList: Html.tags..addAll(["form", "label", "input"]),
|
||||
onLinkTap: (url, buildContext, attributes) => {},
|
||||
extensions: [
|
||||
TagExtension(
|
||||
tagsToExtend: {"img"},
|
||||
builder: (extensionContext) {
|
||||
String? imgUrl = extensionContext.attributes['src'];
|
||||
if (imgUrl!.startsWith('//')) {
|
||||
imgUrl = 'https:$imgUrl';
|
||||
}
|
||||
if (imgUrl.startsWith('http://')) {
|
||||
imgUrl = imgUrl.replaceAll('http://', 'https://');
|
||||
}
|
||||
|
||||
print(imgUrl);
|
||||
bool isEmote = imgUrl.contains('/emote/');
|
||||
bool isMall = imgUrl.contains('/mall/');
|
||||
if (isMall) {
|
||||
return 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,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
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: 0),
|
||||
),
|
||||
"span": Style(
|
||||
fontSize: FontSize.medium,
|
||||
),
|
||||
"li > p": Style(
|
||||
display: Display.inline,
|
||||
),
|
||||
"li": Style(
|
||||
padding: HtmlPaddings.only(bottom: 4),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
"image": Style(margin: Margins.only(top: 4, bottom: 4)),
|
||||
"p > img": Style(margin: Margins.only(top: 4, bottom: 4)),
|
||||
"code": Style(
|
||||
backgroundColor: Theme.of(context).colorScheme.onInverseSurface),
|
||||
"code > span": Style(textAlign: TextAlign.start),
|
||||
"hr": Style(
|
||||
margin: Margins.zero,
|
||||
padding: HtmlPaddings.zero,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
'table': Style(
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
width: 0.5,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
|
||||
),
|
||||
bottom: BorderSide(
|
||||
width: 0.5,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
'tr': Style(
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
width: 1.0,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
|
||||
),
|
||||
left: BorderSide(
|
||||
width: 1.0,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
),
|
||||
'thead': Style(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
),
|
||||
'th': Style(
|
||||
padding: HtmlPaddings.only(left: 3, right: 3),
|
||||
),
|
||||
'td': Style(
|
||||
padding: HtmlPaddings.all(4.0),
|
||||
alignment: Alignment.center,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/pages/member/index.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
|
||||
@ -33,15 +34,16 @@ class VideoCardH extends StatelessWidget {
|
||||
String heroTag = Utils.makeHeroTag(aid);
|
||||
return GestureDetector(
|
||||
onLongPress: () {
|
||||
if (longPress != null) {
|
||||
longPress!();
|
||||
}
|
||||
},
|
||||
onLongPressEnd: (details) {
|
||||
if (longPressEnd != null) {
|
||||
longPressEnd!();
|
||||
}
|
||||
// if (longPress != null) {
|
||||
// longPress!();
|
||||
// }
|
||||
MemberController().blockUser(videoItem.mid);
|
||||
},
|
||||
// onLongPressEnd: (details) {
|
||||
// if (longPressEnd != null) {
|
||||
// longPressEnd!();
|
||||
// }
|
||||
// },
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
try {
|
||||
@ -55,11 +57,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 +128,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 +137,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 2,
|
||||
@ -147,7 +151,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 +180,7 @@ class VideoContent extends StatelessWidget {
|
||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||
// ),
|
||||
// ),
|
||||
const SizedBox(height: 4),
|
||||
// const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
@ -187,46 +190,7 @@ class VideoContent extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: videoItem.stat.view,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(
|
||||
theme: 'gray',
|
||||
danmu: videoItem.stat.danmaku,
|
||||
),
|
||||
// Text(
|
||||
// Utils.dateFormat(videoItem.pubdate!),
|
||||
// style: TextStyle(
|
||||
// fontSize: 11,
|
||||
// color: Theme.of(context).colorScheme.outline),
|
||||
// )
|
||||
const Spacer(),
|
||||
// SizedBox(
|
||||
// width: 20,
|
||||
// height: 20,
|
||||
// child: IconButton(
|
||||
// tooltip: '稍后再看',
|
||||
// style: ButtonStyle(
|
||||
// padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
// ),
|
||||
// onPressed: () async {
|
||||
// var res =
|
||||
// await UserHttp.toViewLater(bvid: videoItem.bvid);
|
||||
// SmartDialog.showToast(res['msg']);
|
||||
// },
|
||||
// icon: Icon(
|
||||
// Icons.more_vert_outlined,
|
||||
// color: Theme.of(context).colorScheme.outline,
|
||||
// size: 14,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
if (source == 'normal')
|
||||
SizedBox(
|
||||
width: 24,
|
||||
@ -260,6 +224,20 @@ class VideoContent extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
// PopupMenuItem<String>(
|
||||
// onTap: () async {
|
||||
// MemberController().blockUser(videoItem.mid);
|
||||
// },
|
||||
// value: 'block',
|
||||
// height: 35,
|
||||
// child: const Row(
|
||||
// children: [
|
||||
// Icon(Icons.block, size: 16),
|
||||
// SizedBox(width: 6),
|
||||
// Text('拉黑up', style: TextStyle(fontSize: 13))
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -14,14 +14,15 @@ 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);
|
||||
@ -47,7 +48,7 @@ class VideoCardV extends StatelessWidget {
|
||||
arguments: {
|
||||
'pic': videoItem.pic,
|
||||
'heroTag': heroTag,
|
||||
'videoType': SearchType.media_bangumi,
|
||||
// 'videoType': SearchType.media_bangumi,
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -61,6 +62,16 @@ class VideoCardV extends StatelessWidget {
|
||||
'heroTag': heroTag,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
SmartDialog.showToast(videoItem.goto);
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': videoItem.uri,
|
||||
'type': 'url',
|
||||
'pageTitle': videoItem.title,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,9 +81,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 +88,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 +143,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 +210,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 +269,77 @@ 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,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'white',
|
||||
view: view,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
StatDanMu(
|
||||
theme: 'white',
|
||||
danmu: danmaku,
|
||||
),
|
||||
],
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'${videoItem.stat.view}观看',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
' • ${videoItem.stat.danmu}弹幕',
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
Utils.timeFormat(duration!),
|
||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -164,6 +164,9 @@ class Api {
|
||||
// 清空历史记录
|
||||
static const String clearHistory = '/x/v2/history/clear';
|
||||
|
||||
// 删除某条历史记录
|
||||
static const String delHistory = '/x/v2/history/delete';
|
||||
|
||||
// 热搜
|
||||
static const String hotSearchList =
|
||||
'https://s.search.bilibili.com/main/hotword';
|
||||
@ -239,6 +242,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 +254,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 +291,19 @@ 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';
|
||||
|
||||
// 搜索历史记录
|
||||
static const String searchHistory = '/x/web-goblin/history/search';
|
||||
}
|
||||
|
@ -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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
lib/http/danmaku.dart
Normal file
33
lib/http/danmaku.dart
Normal file
@ -0,0 +1,33 @@
|
||||
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,
|
||||
};
|
||||
|
||||
// 计算函数
|
||||
Future<DmSegMobileReply> computeTask(Map<String, int> params) async {
|
||||
var response = await Request().get(
|
||||
Api.webDanmaku,
|
||||
data: params,
|
||||
extra: {'resType': ResponseType.bytes},
|
||||
);
|
||||
return DmSegMobileReply.fromBuffer(response.data);
|
||||
}
|
||||
|
||||
return await compute(computeTask, params);
|
||||
}
|
||||
}
|
@ -22,10 +22,18 @@ 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) {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': err.toString(),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
|
40
lib/http/html.dart
Normal file
40
lib/http/html.dart
Normal file
@ -0,0 +1,40 @@
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:html/parser.dart';
|
||||
import 'package:pilipala/http/index.dart';
|
||||
|
||||
class HtmlHttp {
|
||||
static Future reqHtml(id) async {
|
||||
var response = await Request().get("https://www.bilibili.com/opus/$id");
|
||||
Document rootTree = parse(response.data);
|
||||
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 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': opusContent,
|
||||
'commentId': commentId
|
||||
};
|
||||
}
|
||||
}
|
@ -9,27 +9,17 @@ 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 +29,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 +40,7 @@ class Request {
|
||||
log("setCookie, ${e.toString()}");
|
||||
}
|
||||
}
|
||||
setOptionsHeaders(userInfo);
|
||||
}
|
||||
|
||||
if (cookie.isEmpty) {
|
||||
@ -63,16 +55,6 @@ 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
|
||||
@ -87,6 +69,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
|
||||
*/
|
||||
@ -101,20 +92,15 @@ class Request {
|
||||
receiveTimeout: const Duration(milliseconds: 12000),
|
||||
//Http请求头.
|
||||
headers: {
|
||||
// 'cookie': '',
|
||||
'keep-alive': true,
|
||||
'user-agent': headerUa('pc'),
|
||||
'Accept-Encoding': 'gzip'
|
||||
},
|
||||
persistentConnection: true,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
//添加拦截器
|
||||
dio.interceptors.add(ApiInterceptor());
|
||||
|
||||
@ -225,7 +211,7 @@ class Request {
|
||||
: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 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 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 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 (_) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class MemberHttp {
|
||||
int ps = 30,
|
||||
int tid = 0,
|
||||
int? pn,
|
||||
String keyword = '',
|
||||
String? keyword,
|
||||
String order = 'pubdate',
|
||||
bool orderAvoided = true,
|
||||
}) async {
|
||||
@ -74,7 +74,7 @@ class MemberHttp {
|
||||
'ps': ps,
|
||||
'tid': tid,
|
||||
'pn': pn,
|
||||
'keyword': keyword,
|
||||
'keyword': keyword ?? '',
|
||||
'order': order,
|
||||
'platform': 'web',
|
||||
'web_location': 1550101,
|
||||
@ -119,4 +119,27 @@ 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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,63 @@
|
||||
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': '请求错误 🙅',
|
||||
};
|
||||
}
|
||||
|
||||
// 获取搜索建议
|
||||
static Future searchSuggest({required term}) async {
|
||||
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;
|
||||
return {
|
||||
'status': true,
|
||||
'data': SearchSuggestModel.fromJson(res.data['result']),
|
||||
};
|
||||
if (res.data is String) {
|
||||
Map<String, dynamic> resultMap = json.decode(res.data);
|
||||
if (resultMap['code'] == 0) {
|
||||
if (resultMap['result'] is Map) {
|
||||
resultMap['result']['term'] = term;
|
||||
}
|
||||
return {
|
||||
'status': true,
|
||||
'data': resultMap['result'] is Map
|
||||
? SearchSuggestModel.fromJson(resultMap['result'])
|
||||
: [],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
'data': [],
|
||||
'msg': '请求错误 🙅',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
'status': false,
|
||||
@ -46,39 +72,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';
|
||||
@ -50,10 +51,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'] ?? '账号未登录'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +93,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 +179,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 +207,63 @@ 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 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,10 @@ 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 Future rcmdVideoList({required int ps, required int freshIdx}) async {
|
||||
@ -60,8 +63,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 +75,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));
|
||||
@ -136,7 +141,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 +163,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 +402,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']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,11 +7,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 +26,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 +45,27 @@ 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);
|
||||
|
||||
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 +115,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;
|
||||
}
|
@ -360,7 +360,7 @@ class GoodItem {
|
||||
|
||||
String? brief;
|
||||
String? cover;
|
||||
String? id;
|
||||
dynamic id;
|
||||
String? jumpDesc;
|
||||
String? jumpUrl;
|
||||
String? name;
|
||||
@ -408,6 +408,7 @@ class DynamicMajorModel {
|
||||
this.live,
|
||||
this.none,
|
||||
this.type,
|
||||
this.courses,
|
||||
});
|
||||
|
||||
DynamicArchiveModel? archive;
|
||||
@ -422,6 +423,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 +446,7 @@ class DynamicMajorModel {
|
||||
none =
|
||||
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
|
||||
type = json['type'];
|
||||
courses = json['courses'] ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,6 +481,8 @@ class DynamicArchiveModel {
|
||||
this.stat,
|
||||
this.title,
|
||||
this.type,
|
||||
this.epid,
|
||||
this.seasonId,
|
||||
});
|
||||
|
||||
int? aid;
|
||||
@ -491,6 +496,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 +510,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()
|
||||
: [];
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -174,7 +185,7 @@ class AboutController extends GetxController {
|
||||
// 获取远程版本
|
||||
Future getRemoteApp() async {
|
||||
var result = await Request().get(Api.latestApp);
|
||||
LatestDataModel data = LatestDataModel.fromJson(result.data);
|
||||
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';
|
||||
@ -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;
|
||||
}
|
||||
@ -256,6 +259,7 @@ class BangumiIntroController extends GetxController {
|
||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||
videoDetailCtr.bvid = bvid;
|
||||
videoDetailCtr.cid = 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();
|
||||
@ -69,7 +73,11 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
|
||||
return BangumiInfo(
|
||||
loadingStatus: true,
|
||||
bangumiDetail: bangumiDetail,
|
||||
cid: widget.cid,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -79,11 +87,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 +107,7 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
Box localCache = GStrorage.localCache;
|
||||
late final BangumiInfoModel? bangumiItem;
|
||||
late double sheetHeight;
|
||||
int? cid;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -105,11 +116,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
||||
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||
bangumiItem = bangumiIntroController.bangumiItem;
|
||||
sheetHeight = localCache.get('sheetHeight');
|
||||
cid = widget.cid!;
|
||||
}
|
||||
|
||||
// 收藏
|
||||
showFavBottomSheet() {
|
||||
if (bangumiIntroController.user.get(UserBoxKey.userMid) == null) {
|
||||
if (bangumiIntroController.userInfo.mid == null) {
|
||||
SmartDialog.showToast('账号未登录');
|
||||
return;
|
||||
}
|
||||
@ -320,9 +332,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,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class BangumiPanel extends StatefulWidget {
|
||||
final List<EpisodeItem> pages;
|
||||
@ -22,82 +24,119 @@ 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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
|
||||
scrollToIndex();
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo != null) {
|
||||
vipStatus = userInfo.vipStatus;
|
||||
}
|
||||
}
|
||||
|
||||
@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 +147,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 +198,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 +271,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: () {
|
||||
@ -65,7 +62,7 @@ class BangumiCardV extends StatelessWidget {
|
||||
arguments: {
|
||||
'pic': pic,
|
||||
'heroTag': heroTag,
|
||||
'videoType': SearchType.media_bangumi,
|
||||
// 'videoType': SearchType.media_bangumi,
|
||||
'bangumiItem': res['data'],
|
||||
},
|
||||
);
|
||||
@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
64
lib/pages/danmaku/controller.dart
Normal file
64
lib/pages/danmaku/controller.dart
Normal file
@ -0,0 +1,64 @@
|
||||
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 = 0;
|
||||
int currentDmIndex = 0;
|
||||
|
||||
void calcSegment() {
|
||||
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
|
||||
}
|
||||
|
||||
Future<List<DmSegMobileReply>> queryDanmaku() async {
|
||||
dmSegList.clear();
|
||||
for (int segIndex = 1; segIndex <= segCount; segIndex++) {
|
||||
DmSegMobileReply result =
|
||||
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: segIndex);
|
||||
if (result.elems.isNotEmpty) {
|
||||
result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
|
||||
dmSegList.add(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';
|
162
lib/pages/danmaku/view.dart
Normal file
162
lib/pages/danmaku/view.dart
Normal file
@ -0,0 +1,162 @@
|
||||
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;
|
||||
}
|
||||
PlDanmakuController ctr = _plDanmakuController;
|
||||
int currentPosition = position.inMilliseconds;
|
||||
blockTypes = playerController.blockTypes;
|
||||
|
||||
if (!playerController.isOpenDanmu.value) {
|
||||
return;
|
||||
}
|
||||
// 超出分段数返回
|
||||
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 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,
|
||||
duration: 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,6 +144,8 @@ 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;
|
||||
@ -115,7 +156,10 @@ class DynamicsController extends GetxController {
|
||||
break;
|
||||
case 'DYNAMIC_TYPE_PGC':
|
||||
print('番剧');
|
||||
SmartDialog.showToast('暂未支持的类型,请联系开发者');
|
||||
break;
|
||||
|
||||
/// 纯文字动态查看
|
||||
case 'DYNAMIC_TYPE_WORD':
|
||||
print('纯文本');
|
||||
Get.toNamed('/dynamicDetail',
|
||||
@ -139,16 +183,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 +251,8 @@ class DynamicsController extends GetxController {
|
||||
|
||||
onRefresh() async {
|
||||
page = 1;
|
||||
queryFollowUp();
|
||||
print('onRefresh');
|
||||
await queryFollowUp();
|
||||
await queryFollowDynamic();
|
||||
}
|
||||
|
||||
@ -181,8 +271,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,10 @@
|
||||
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);
|
||||
@ -16,9 +18,10 @@ class DynamicDetailController extends GetxController {
|
||||
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
|
||||
RxInt acount = 0.obs;
|
||||
|
||||
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 +32,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 +47,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 +84,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,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_reply.dart';
|
||||
@ -37,13 +38,25 @@ 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 = Get.arguments['item'].basic!['comment_type'] ?? 11;
|
||||
type = (commentType == 0) ? 11 : commentType;
|
||||
|
||||
action =
|
||||
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
|
||||
_dynamicDetailController = Get.put(DynamicDetailController(oid, type));
|
||||
_dynamicDetailController =
|
||||
Get.put(DynamicDetailController(oid, type), tag: oid.toString());
|
||||
_futureBuilderFuture = _dynamicDetailController!.queryReplyList();
|
||||
titleStreamC = StreamController<bool>();
|
||||
scrollController.addListener(_listen);
|
||||
@ -56,10 +69,9 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
void _listen() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
if (!_dynamicDetailController!.isLoadingMore) {
|
||||
_dynamicDetailController!.isLoadingMore = true;
|
||||
await _dynamicDetailController!.queryReplyList(reqType: 'onLoad');
|
||||
}
|
||||
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
|
||||
_dynamicDetailController!.queryReplyList(reqType: 'onLoad');
|
||||
});
|
||||
}
|
||||
|
||||
if (scrollController.offset > 55 && !_visibleTitle) {
|
||||
@ -95,6 +107,12 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -236,6 +254,11 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
|
||||
replyReply: (replyItem) =>
|
||||
replyReply(replyItem),
|
||||
replyType: ReplyType.values[type],
|
||||
addReply: (replyItem) {
|
||||
_dynamicDetailController!
|
||||
.replyList[index].replies!
|
||||
.add(replyItem);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -37,7 +37,7 @@ class _ActionPanelState extends State<ActionPanel> {
|
||||
String dynamicId = item.idStr!;
|
||||
// 1 已点赞 2 不喜欢 0 未操作
|
||||
Like like = item.modules.moduleStat.like;
|
||||
int count = int.parse(like.count!);
|
||||
int count = like.count == '点赞' ? 0 : int.parse(like.count ?? '0');
|
||||
bool status = like.status!;
|
||||
int up = status ? 2 : 1;
|
||||
var res = await DynamicsHttp.likeDynamic(dynamicId: dynamicId, up: up);
|
||||
@ -47,7 +47,11 @@ class _ActionPanelState extends State<ActionPanel> {
|
||||
item.modules.moduleStat.like.count = (count + 1).toString();
|
||||
item.modules.moduleStat.like.status = true;
|
||||
} else {
|
||||
item.modules.moduleStat.like.count = (count - 1).toString();
|
||||
if (count == 1) {
|
||||
item.modules.moduleStat.like.count = '点赞';
|
||||
} else {
|
||||
item.modules.moduleStat.like.count = (count - 1).toString();
|
||||
}
|
||||
item.modules.moduleStat.like.status = false;
|
||||
}
|
||||
setState(() {});
|
||||
@ -63,54 +67,63 @@ class _ActionPanelState extends State<ActionPanel> {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.shareFromSquare,
|
||||
size: 16,
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.shareFromSquare,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
label: Text(stat.forward!.count ?? '转发'),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
label: Text(stat.forward!.count ?? '转发'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () =>
|
||||
_dynamicsController.pushDetail(widget.item, 1, action: 'comment'),
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.comment,
|
||||
size: 16,
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextButton.icon(
|
||||
onPressed: () => _dynamicsController.pushDetail(widget.item, 1,
|
||||
action: 'comment'),
|
||||
icon: const Icon(
|
||||
FontAwesomeIcons.comment,
|
||||
size: 16,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
label: Text(stat.comment!.count ?? '评论'),
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
label: Text(stat.comment!.count ?? '评论'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => onLikeDynamic(),
|
||||
icon: Icon(
|
||||
stat.like!.status!
|
||||
? FontAwesomeIcons.solidThumbsUp
|
||||
: FontAwesomeIcons.thumbsUp,
|
||||
size: 16,
|
||||
color: stat.like!.status! ? primary : color,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
label: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
stat.like!.count ?? '点赞',
|
||||
key: ValueKey<String>(stat.like!.count ?? '点赞'),
|
||||
style: TextStyle(
|
||||
color: stat.like!.status! ? primary : color,
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextButton.icon(
|
||||
onPressed: () => onLikeDynamic(),
|
||||
icon: Icon(
|
||||
stat.like!.status!
|
||||
? FontAwesomeIcons.solidThumbsUp
|
||||
: FontAwesomeIcons.thumbsUp,
|
||||
size: 16,
|
||||
color: stat.like!.status! ? primary : color,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
|
||||
foregroundColor: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
label: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(scale: animation, child: child);
|
||||
},
|
||||
child: Text(
|
||||
stat.like!.count ?? '点赞',
|
||||
key: ValueKey<String>(stat.like!.count ?? '点赞'),
|
||||
style: TextStyle(
|
||||
color: stat.like!.status! ? primary : color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -60,43 +60,47 @@ Widget addWidget(item, context, type, {floor = 1}) {
|
||||
),
|
||||
);
|
||||
case 'ADDITIONAL_TYPE_RESERVE':
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding:
|
||||
const EdgeInsets.only(left: 12, top: 10, right: 12, bottom: 10),
|
||||
color: bgColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
dynamicProperty[type].title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize),
|
||||
return dynamicProperty[type].state != -1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: InkWell(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12, top: 10, right: 12, bottom: 10),
|
||||
color: bgColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TextSpan(text: dynamicProperty[type].desc1['text']),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(text: dynamicProperty[type].desc2['text']),
|
||||
Text(
|
||||
dynamicProperty[type].title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelMedium!
|
||||
.fontSize),
|
||||
children: [
|
||||
TextSpan(text: dynamicProperty[type].desc1['text']),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(text: dynamicProperty[type].desc2['text']),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
// TextButton(onPressed: () {}, child: Text('123'))
|
||||
),
|
||||
),
|
||||
);
|
||||
// TextButton(onPressed: () {}, child: Text('123'))
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox();
|
||||
case 'ADDITIONAL_TYPE_GOODS':
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
|
@ -42,13 +42,17 @@ Widget articlePanel(item, context, {floor = 1}) {
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (item.modules.moduleDynamic.major.opus.summary.text != 'undefined')
|
||||
if (item.modules.moduleDynamic.major.opus.summary.text !=
|
||||
'undefined') ...[
|
||||
Text(
|
||||
item.modules.moduleDynamic.major.opus.summary.richTextNodes.first
|
||||
.text,
|
||||
maxLines: 4,
|
||||
style: const TextStyle(height: 1.55),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
picWidget(item, context)
|
||||
],
|
||||
),
|
||||
|
@ -36,7 +36,8 @@ Widget author(item, context) {
|
||||
Text(
|
||||
item.modules.moduleAuthor.name,
|
||||
style: TextStyle(
|
||||
color: item.modules.moduleAuthor!.vip!['status'] > 0
|
||||
color: item.modules.moduleAuthor!.vip != null &&
|
||||
item.modules.moduleAuthor!.vip['status'] > 0
|
||||
? const Color.fromARGB(255, 251, 100, 163)
|
||||
: Theme.of(context).colorScheme.onBackground,
|
||||
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
|
||||
|
@ -28,6 +28,8 @@ Widget content(item, context, source) {
|
||||
focusNode: FocusNode(),
|
||||
selectionControls: MaterialTextSelectionControls(),
|
||||
child: Text.rich(
|
||||
/// fix 默认20px高度
|
||||
style: const TextStyle(height: 0),
|
||||
richNode(item, context),
|
||||
maxLines: source == 'detail' ? 999 : 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -41,7 +41,8 @@ class DynamicPanel extends StatelessWidget {
|
||||
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
|
||||
child: author(item, context),
|
||||
),
|
||||
if (item!.modules!.moduleDynamic!.desc != null)
|
||||
if (item!.modules!.moduleDynamic!.desc != null ||
|
||||
item!.modules!.moduleDynamic!.major != null)
|
||||
content(item, context, source),
|
||||
forWard(item, context, _dynamicsController, source),
|
||||
const SizedBox(height: 2),
|
||||
|
@ -44,19 +44,21 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
if (item.modules.moduleDynamic.topic != null) ...[
|
||||
Padding(
|
||||
padding: floor == 2
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
'#${item.modules.moduleDynamic.topic.name}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
/// fix #话题跟content重复
|
||||
// if (item.modules.moduleDynamic.topic != null) ...[
|
||||
// Padding(
|
||||
// padding: floor == 2
|
||||
// ? EdgeInsets.zero
|
||||
// : const EdgeInsets.only(left: 12, right: 12),
|
||||
// child: GestureDetector(
|
||||
// child: Text(
|
||||
// '#${item.modules.moduleDynamic.topic.name}',
|
||||
// style: authorStyle,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
Text.rich(
|
||||
richNode(item, context),
|
||||
// 被转发状态(floor=2) 隐藏
|
||||
@ -71,6 +73,8 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: picWidget(item, context),
|
||||
),
|
||||
|
||||
/// 附加内容 商品信息、直播预约等等
|
||||
if (item.modules.moduleDynamic.additional != null)
|
||||
addWidget(
|
||||
item,
|
||||
@ -100,6 +104,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
// 直播
|
||||
case 'DYNAMIC_TYPE_LIVE_RCMD':
|
||||
return liveRcmdPanel(item, context, floor: floor);
|
||||
// 直播
|
||||
case 'DYNAMIC_TYPE_LIVE':
|
||||
return livePanel(item, context, floor: floor);
|
||||
// 合集
|
||||
@ -132,7 +137,12 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(item.modules.moduleDynamic.desc.text)
|
||||
Text.rich(
|
||||
richNode(item, context),
|
||||
// 被转发状态(floor=2) 隐藏
|
||||
maxLines: source == 'detail' && floor != 2 ? 999 : 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
)
|
||||
: item.modules.moduleDynamic.additional != null
|
||||
@ -147,6 +157,7 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
return videoSeasonWidget(item, context, 'pgc', floor: floor);
|
||||
case 'DYNAMIC_TYPE_PGC_UNION':
|
||||
return videoSeasonWidget(item, context, 'pgc', floor: floor);
|
||||
// 直播结束
|
||||
case 'DYNAMIC_TYPE_NONE':
|
||||
return Row(
|
||||
children: [
|
||||
@ -158,7 +169,23 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
|
||||
Text(item.modules.moduleDynamic.major.none.tips)
|
||||
],
|
||||
);
|
||||
// 课堂
|
||||
case 'DYNAMIC_TYPE_COURSES_SEASON':
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"课堂💪:${item.modules.moduleDynamic.major.courses['title']}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
default:
|
||||
return const SizedBox(height: 0);
|
||||
return const SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text('🙏 暂未支持的类型,请联系开发者反馈 '),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/badge.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
|
||||
Widget picWidget(item, context) {
|
||||
String type = item.modules.moduleDynamic.major.type;
|
||||
List pictures = [];
|
||||
if (type == 'MAJOR_TYPE_OPUS') {
|
||||
pictures = item.modules.moduleDynamic.major.opus.pics;
|
||||
/// fix 图片跟rich_node_panel重复
|
||||
// pictures = item.modules.moduleDynamic.major.opus.pics;
|
||||
return const SizedBox();
|
||||
}
|
||||
if (type == 'MAJOR_TYPE_DRAW') {
|
||||
pictures = item.modules.moduleDynamic.major.draw.items;
|
||||
}
|
||||
int len = pictures.length;
|
||||
List picList = [];
|
||||
List<String> picList = [];
|
||||
List<Widget> list = [];
|
||||
for (var i = 0; i < len; i++) {
|
||||
picList.add(pictures[i].src ?? pictures[i].url);
|
||||
@ -23,11 +25,14 @@ Widget picWidget(item, context) {
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed('/preview',
|
||||
arguments: {'initialPage': i, 'imgList': picList});
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: i, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
// child: Hero(
|
||||
// tag: pictures[i].src ?? pictures[i].url,
|
||||
child: NetworkImgLayer(
|
||||
src: pictures[i].src ?? pictures[i].url,
|
||||
width: box.maxWidth,
|
||||
@ -44,27 +49,25 @@ Widget picWidget(item, context) {
|
||||
double maxWidth = box.maxWidth;
|
||||
double aspectRatio = 1.0;
|
||||
double origAspectRatio = 0.0;
|
||||
double crossCount = len == 1
|
||||
? 1
|
||||
: len < 3
|
||||
? 2
|
||||
: 3;
|
||||
double crossCount = 3;
|
||||
|
||||
double height = 0.0;
|
||||
if (len == 1) {
|
||||
origAspectRatio =
|
||||
aspectRatio = pictures.first.width / pictures.first.height;
|
||||
try {
|
||||
origAspectRatio =
|
||||
aspectRatio = pictures.first.width / pictures.first.height;
|
||||
} catch (_) {}
|
||||
if (aspectRatio < 0.4) {
|
||||
aspectRatio = 0.4;
|
||||
}
|
||||
height = pictures.first.height * maxWidth / pictures.first.width;
|
||||
if (origAspectRatio < 0.5 || pictures.first.width < 1920) {
|
||||
crossCount = 2;
|
||||
height = maxWidth / 2 / aspectRatio;
|
||||
}
|
||||
} else {
|
||||
aspectRatio = 1;
|
||||
height = maxWidth / crossCount * ((len / crossCount).ceil()) + 6;
|
||||
height =
|
||||
maxWidth / crossCount * ((len + crossCount - 1) ~/ crossCount) + 6;
|
||||
}
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
|
@ -1,175 +1,319 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/pages/preview/index.dart';
|
||||
|
||||
// 富文本
|
||||
InlineSpan richNode(item, context) {
|
||||
TextStyle authorStyle =
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary);
|
||||
List<InlineSpan> spanChilds = [];
|
||||
for (var i in item.modules.moduleDynamic.desc.richTextNodes) {
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
|
||||
spanChilds.add(
|
||||
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
|
||||
final spacer = _VerticalSpaceSpan(0.0);
|
||||
try {
|
||||
TextStyle authorStyle =
|
||||
TextStyle(color: Theme.of(context).colorScheme.primary);
|
||||
List<InlineSpan> spanChilds = [];
|
||||
String contentType = 'desc';
|
||||
|
||||
dynamic richTextNodes;
|
||||
if (item.modules.moduleDynamic.desc != null) {
|
||||
richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
|
||||
} else if (item.modules.moduleDynamic.major != null) {
|
||||
contentType = 'major';
|
||||
// 动态页面 richTextNodes 层级可能与主页动态层级不同
|
||||
richTextNodes =
|
||||
item.modules.moduleDynamic.major.opus.summary.richTextNodes;
|
||||
}
|
||||
// @用户
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.toNamed('/member?mid=${i.rid}',
|
||||
arguments: {'face': null}),
|
||||
if (richTextNodes == null || richTextNodes.isEmpty) {
|
||||
return spacer;
|
||||
} else {
|
||||
for (var i in richTextNodes) {
|
||||
/// fix 渲染专栏时内容会重复
|
||||
if (item.modules.moduleDynamic.major.opus.title == null &&
|
||||
i.type == 'RICH_TEXT_NODE_TYPE_TEXT') {
|
||||
spanChilds.add(
|
||||
TextSpan(text: i.origText, style: const TextStyle(height: 1.65)));
|
||||
}
|
||||
// @用户
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_AT') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.toNamed('/member?mid=${i.rid}',
|
||||
arguments: {'face': null}),
|
||||
child: Text(
|
||||
' ${i.text}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 话题
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
' ${i.text}',
|
||||
'${i.origText}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 话题
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_TOPIC') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.origText}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 网页链接
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.link,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {'url': i.origText, 'type': 'url', 'pageTitle': ''},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
i.text,
|
||||
style: authorStyle,
|
||||
);
|
||||
}
|
||||
// 网页链接
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_WEB') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.link,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 投票
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
String dynamicId = item.basic['comment_id_str'];
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
|
||||
'type': 'vote',
|
||||
'pageTitle': '投票'
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url': i.origText,
|
||||
'type': 'url',
|
||||
'pageTitle': ''
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'投票:${i.text}',
|
||||
style: authorStyle,
|
||||
child: Text(
|
||||
i.text,
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 表情
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: i.emoji.iconUrl,
|
||||
type: 'emote',
|
||||
width: i.emoji.size * 20,
|
||||
height: i.emoji.size * 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 抽奖
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.redeem_rounded,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.origText} ',
|
||||
style: authorStyle,
|
||||
);
|
||||
}
|
||||
// 投票
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_VOTE') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
String dynamicId = item.basic['comment_id_str'];
|
||||
Get.toNamed(
|
||||
'/webview',
|
||||
parameters: {
|
||||
'url':
|
||||
'https://t.bilibili.com/vote/h5/index/#/result?vote_id=${i.rid}&dynamic_id=$dynamicId&isWeb=1',
|
||||
'type': 'vote',
|
||||
'pageTitle': '投票'
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
'投票:${i.text}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
// 表情
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_EMOJI') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: NetworkImgLayer(
|
||||
src: i.emoji.iconUrl,
|
||||
type: 'emote',
|
||||
width: i.emoji.size * 20,
|
||||
height: i.emoji.size * 20,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// 抽奖
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_LOTTERY') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.redeem_rounded,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.origText} ',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// TODO 商品
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.text} ',
|
||||
style: authorStyle,
|
||||
/// TODO 商品
|
||||
if (i.type == 'RICH_TEXT_NODE_TYPE_GOODS') {
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Icon(
|
||||
Icons.shopping_bag_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: Text(
|
||||
'${i.text} ',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (contentType == 'major' &&
|
||||
item.modules.moduleDynamic.major.opus.pics.isNotEmpty) {
|
||||
// 图片可能跟其他widget重复渲染
|
||||
List<OpusPicsModel> pics = item.modules.moduleDynamic.major.opus.pics;
|
||||
int len = pics.length;
|
||||
List<String> picList = [];
|
||||
|
||||
if (len == 1) {
|
||||
OpusPicsModel pictureItem = pics.first;
|
||||
picList.add(pictureItem.url!);
|
||||
spanChilds.add(const TextSpan(text: '\n'));
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: 0, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: NetworkImgLayer(
|
||||
src: pictureItem.url,
|
||||
width: box.maxWidth / 2,
|
||||
height: box.maxWidth *
|
||||
0.5 *
|
||||
pictureItem.height! /
|
||||
pictureItem.width!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (len > 1) {
|
||||
List<Widget> list = [];
|
||||
for (var i = 0; i < len; i++) {
|
||||
picList.add(pics[i].url!);
|
||||
list.add(
|
||||
LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return ImagePreview(initialPage: i, imgList: picList);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: NetworkImgLayer(
|
||||
src: pics[i].url,
|
||||
width: box.maxWidth,
|
||||
height: box.maxWidth,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
spanChilds.add(
|
||||
WidgetSpan(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, BoxConstraints box) {
|
||||
double maxWidth = box.maxWidth;
|
||||
double crossCount = len < 3 ? 2 : 3;
|
||||
double height = maxWidth /
|
||||
crossCount *
|
||||
(len % crossCount == 0
|
||||
? len ~/ crossCount
|
||||
: len ~/ crossCount + 1) +
|
||||
6;
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
height: height,
|
||||
child: GridView.count(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: crossCount.toInt(),
|
||||
mainAxisSpacing: 4.0,
|
||||
crossAxisSpacing: 4.0,
|
||||
childAspectRatio: 1,
|
||||
children: list,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// spanChilds.add(
|
||||
// WidgetSpan(
|
||||
// child: NetworkImgLayer(
|
||||
// src: pics.first.url,
|
||||
// type: 'emote',
|
||||
// width: 100,
|
||||
// height: 200,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
}
|
||||
return TextSpan(
|
||||
children: spanChilds,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
print('❌rich_node_panel err: $err');
|
||||
return spacer;
|
||||
}
|
||||
return TextSpan(
|
||||
children: spanChilds,
|
||||
);
|
||||
}
|
||||
|
||||
class _VerticalSpaceSpan extends WidgetSpan {
|
||||
_VerticalSpaceSpan(double height)
|
||||
: super(child: SizedBox(height: height, width: double.infinity));
|
||||
}
|
||||
|
@ -24,24 +24,28 @@ class _UpPanelState extends State<UpPanel> {
|
||||
List<UpItem> upList = [];
|
||||
List<LiveUserItem> liveList = [];
|
||||
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
var userInfo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
upList = widget.upData!.upList!;
|
||||
liveList = widget.upData!.liveUsers!.items!;
|
||||
if (widget.upData!.liveUsers != null) {
|
||||
liveList = widget.upData!.liveUsers!.items!;
|
||||
}
|
||||
upList.insert(
|
||||
0,
|
||||
UpItem(
|
||||
face: 'https://files.catbox.moe/8uc48f.png', uname: '全部动态', mid: -1),
|
||||
);
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
upList.insert(
|
||||
1,
|
||||
UpItem(
|
||||
face: user.get(UserBoxKey.userFace),
|
||||
face: userInfo.face,
|
||||
uname: '我',
|
||||
mid: user.get(UserBoxKey.userMid),
|
||||
mid: userInfo.mid,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -64,15 +68,20 @@ class _UpPanelState extends State<UpPanel> {
|
||||
controller: scrollController,
|
||||
children: [
|
||||
const SizedBox(width: 10),
|
||||
for (int i = 0; i < liveList.length; i++) ...[
|
||||
upItemBuild(liveList[i], i)
|
||||
if (liveList.isNotEmpty) ...[
|
||||
for (int i = 0; i < liveList.length; i++) ...[
|
||||
upItemBuild(liveList[i], i)
|
||||
],
|
||||
VerticalDivider(
|
||||
indent: 20,
|
||||
endIndent: 40,
|
||||
width: 26,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
],
|
||||
VerticalDivider(
|
||||
indent: 20,
|
||||
endIndent: 40,
|
||||
width: 26,
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.5),
|
||||
),
|
||||
for (int i = 0; i < upList.length; i++) ...[
|
||||
upItemBuild(upList[i], i)
|
||||
],
|
||||
@ -123,7 +132,8 @@ class _UpPanelState extends State<UpPanel> {
|
||||
double itemWidth = contentWidth + itemPadding.horizontal;
|
||||
double screenWidth = MediaQuery.of(context).size.width;
|
||||
double moveDistance = 0.0;
|
||||
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
|
||||
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
moveDistance =
|
||||
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
} else {
|
||||
|
@ -57,20 +57,21 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
// const SizedBox(height: 4),
|
||||
if (item.modules.moduleDynamic.topic != null) ...[
|
||||
Padding(
|
||||
padding: floor == 2
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: GestureDetector(
|
||||
child: Text(
|
||||
'#${item.modules.moduleDynamic.topic.name}',
|
||||
style: authorStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
/// fix #话题跟content重复
|
||||
// if (item.modules.moduleDynamic.topic != null) ...[
|
||||
// Padding(
|
||||
// padding: floor == 2
|
||||
// ? EdgeInsets.zero
|
||||
// : const EdgeInsets.only(left: 12, right: 12),
|
||||
// child: GestureDetector(
|
||||
// child: Text(
|
||||
// '#${item.modules.moduleDynamic.topic.name}',
|
||||
// style: authorStyle,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 6),
|
||||
// ],
|
||||
if (floor == 2 && item.modules.moduleDynamic.desc != null) ...[
|
||||
Text.rich(richNode(item, context)),
|
||||
const SizedBox(height: 6),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/fan.dart';
|
||||
@ -5,41 +6,54 @@ import 'package:pilipala/models/fans/result.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class FansController extends GetxController {
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
int pn = 1;
|
||||
int ps = 20;
|
||||
int total = 0;
|
||||
RxList<FansItemModel> fansList = [FansItemModel()].obs;
|
||||
late int mid;
|
||||
late String name;
|
||||
var userInfo;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(
|
||||
Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString());
|
||||
name = Get.parameters['name'] ?? user.get(UserBoxKey.userName);
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
mid = Get.parameters['mid'] != null
|
||||
? int.parse(Get.parameters['mid']!)
|
||||
: userInfo.mid;
|
||||
name = Get.parameters['name'] ?? userInfo.uname;
|
||||
}
|
||||
|
||||
Future queryFans(type) async {
|
||||
if (type == 'init') {
|
||||
pn = 1;
|
||||
loadingText.value == '加载中...';
|
||||
}
|
||||
if (loadingText.value == '没有更多了') {
|
||||
return;
|
||||
}
|
||||
var res = await FanHttp.fans(
|
||||
vmid: mid,
|
||||
pn: pn,
|
||||
ps: 20,
|
||||
ps: ps,
|
||||
orderType: 'attention',
|
||||
);
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
fansList.value = res['data'].list;
|
||||
total = res['data'].total;
|
||||
} else if (type == 'onRefresh') {
|
||||
fansList.insertAll(0, res['data'].list);
|
||||
} else if (type == 'onLoad') {
|
||||
fansList.addAll(res['data'].list);
|
||||
}
|
||||
print(total);
|
||||
if ((pn == 1 && total < ps) || res['data'].list.isEmpty) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
pn += 1;
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/fans/result.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
@ -17,7 +19,6 @@ class _FansPageState extends State<FansPage> {
|
||||
final FansController _fansController = Get.put(FansController());
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Future? _futureBuilderFuture;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -27,16 +28,20 @@ class _FansPageState extends State<FansPage> {
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (!_isLoadingMore) {
|
||||
_isLoadingMore = true;
|
||||
await _fansController.queryFans('onLoad');
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
_fansController.queryFans('onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -60,14 +65,38 @@ class _FansPageState extends State<FansPage> {
|
||||
if (data['status']) {
|
||||
List<FansItemModel> list = _fansController.fansList;
|
||||
return Obx(
|
||||
() => list.length == 1
|
||||
? const SizedBox()
|
||||
: ListView.builder(
|
||||
() => list.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: list.length,
|
||||
itemCount: list.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return fanItem(item: list[index]);
|
||||
if (index == list.length) {
|
||||
return Container(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom + 60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom:
|
||||
MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_fansController.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return fanItem(item: list[index]);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(
|
||||
slivers: [NoData()],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
@ -1,20 +1,52 @@
|
||||
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/user.dart';
|
||||
import 'package:pilipala/models/user/fav_folder.dart';
|
||||
import 'package:pilipala/models/user/info.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class FavController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Rx<FavFolderData> favFolderData = FavFolderData().obs;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
UserInfoData? userInfo;
|
||||
int currentPage = 1;
|
||||
int pageSize = 10;
|
||||
RxBool hasMore = true.obs;
|
||||
|
||||
Future<dynamic> queryFavFolder() async {
|
||||
Future<dynamic> queryFavFolder({type = 'init'}) async {
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
if (userInfo == null) {
|
||||
return {'status': false, 'msg': '账号未登录'};
|
||||
}
|
||||
if (!hasMore.value) {
|
||||
return;
|
||||
}
|
||||
var res = await await UserHttp.userfavFolder(
|
||||
pn: 1,
|
||||
ps: 10,
|
||||
mid: GStrorage.user.get(UserBoxKey.userMid) ?? 0,
|
||||
pn: currentPage,
|
||||
ps: pageSize,
|
||||
mid: userInfo!.mid!,
|
||||
);
|
||||
if (res['status']) {
|
||||
favFolderData.value = res['data'];
|
||||
if (type == 'init') {
|
||||
favFolderData.value = res['data'];
|
||||
} else {
|
||||
if (res['data'].list.isNotEmpty) {
|
||||
favFolderData.value.list!.addAll(res['data'].list);
|
||||
favFolderData.update((val) {});
|
||||
}
|
||||
}
|
||||
hasMore.value = res['data'].hasMore;
|
||||
currentPage++;
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
Future onLoad() async {
|
||||
queryFavFolder(type: 'onload');
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
@ -14,11 +15,23 @@ class FavPage extends StatefulWidget {
|
||||
class _FavPageState extends State<FavPage> {
|
||||
final FavController _favController = Get.put(FavController());
|
||||
late Future _futureBuilderFuture;
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = _favController.queryFavFolder();
|
||||
scrollController = _favController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
|
||||
_favController.onLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -40,6 +53,7 @@ class _FavPageState extends State<FavPage> {
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: _favController.favFolderData.value.list!.length,
|
||||
itemBuilder: (context, index) {
|
||||
return FavItem(
|
||||
|
@ -77,7 +77,6 @@ class VideoContent extends StatelessWidget {
|
||||
favFolderItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
|
@ -14,7 +14,9 @@ class FavDetailController extends GetxController {
|
||||
int currentPage = 1;
|
||||
bool isLoadingMore = false;
|
||||
RxMap favInfo = {}.obs;
|
||||
RxList<FavDetailItemData> favList = [FavDetailItemData()].obs;
|
||||
RxList favList = [].obs;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
int mediaCount = 0;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -27,6 +29,11 @@ class FavDetailController extends GetxController {
|
||||
}
|
||||
|
||||
Future<dynamic> queryUserFavFolderDetail({type = 'init'}) async {
|
||||
if (type == 'onLoad' && favList.length >= mediaCount) {
|
||||
loadingText.value = '没有更多了';
|
||||
return;
|
||||
}
|
||||
isLoadingMore = true;
|
||||
var res = await await UserHttp.userFavFolderDetail(
|
||||
pn: currentPage,
|
||||
ps: 20,
|
||||
@ -36,11 +43,16 @@ class FavDetailController extends GetxController {
|
||||
favInfo.value = res['data'].info;
|
||||
if (currentPage == 1 && type == 'init') {
|
||||
favList.value = res['data'].medias;
|
||||
} else if (type == 'onload') {
|
||||
mediaCount = res['data'].info['media_count'];
|
||||
} else if (type == 'onLoad') {
|
||||
favList.addAll(res['data'].medias);
|
||||
}
|
||||
if (favList.length >= mediaCount) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
}
|
||||
currentPage += 1;
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
@ -49,21 +61,19 @@ class FavDetailController extends GetxController {
|
||||
aid: id, addIds: '', delIds: mediaId.toString());
|
||||
if (result['status']) {
|
||||
if (result['data']['prompt']) {
|
||||
List<FavDetailItemData> dataList = favDetailData.value.medias!;
|
||||
List dataList = favList;
|
||||
for (var i in dataList) {
|
||||
if (i.id == id) {
|
||||
dataList.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
favDetailData.value.medias = dataList;
|
||||
favDetailData.refresh();
|
||||
SmartDialog.showToast('取消收藏');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
queryUserFavFolderDetail(type: 'onload');
|
||||
queryUserFavFolderDetail(type: 'onLoad');
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/pages/favDetail/index.dart';
|
||||
|
||||
import 'widget/fav_video_card.dart';
|
||||
@ -37,10 +40,9 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
|
||||
if (_controller.position.pixels >=
|
||||
_controller.position.maxScrollExtent - 200) {
|
||||
if (!_favDetailController.isLoadingMore) {
|
||||
_favDetailController.isLoadingMore = true;
|
||||
EasyThrottle.throttle('favDetail', const Duration(seconds: 1), () {
|
||||
_favDetailController.onLoad();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -61,6 +63,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
SliverAppBar(
|
||||
expandedHeight: 260 - MediaQuery.of(context).padding.top,
|
||||
pinned: true,
|
||||
titleSpacing: 0,
|
||||
title: StreamBuilder(
|
||||
stream: titleStreamC.stream,
|
||||
initialData: false,
|
||||
@ -126,32 +129,34 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_favDetailController.item!.title!,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.fontSize,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_favDetailController.item!.upper!.name!,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
)
|
||||
],
|
||||
)
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_favDetailController.item!.title!,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.fontSize,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_favDetailController.item!.upper!.name!,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context)
|
||||
.textTheme
|
||||
.labelSmall!
|
||||
.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -163,7 +168,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14),
|
||||
child: Obx(
|
||||
() => Text(
|
||||
'共${_favDetailController.favInfo['media_count'] ?? '-'}条视频',
|
||||
'共${_favDetailController.favList.length}条视频',
|
||||
style: TextStyle(
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
@ -180,21 +185,22 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
if (_favDetailController.item!.mediaCount == 0) {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: Center(child: Text('没有内容')),
|
||||
),
|
||||
);
|
||||
return const NoData();
|
||||
} else {
|
||||
List favList = _favDetailController.favList;
|
||||
return Obx(
|
||||
() => SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return FavVideoCardH(
|
||||
videoItem: _favDetailController.favList[index],
|
||||
);
|
||||
}, childCount: _favDetailController.favList.length),
|
||||
),
|
||||
() => favList.isEmpty
|
||||
? const SliverToBoxAdapter(child: SizedBox())
|
||||
: SliverList(
|
||||
delegate:
|
||||
SliverChildBuilderDelegate((context, index) {
|
||||
return FavVideoCardH(
|
||||
videoItem: favList[index],
|
||||
callFn: () => _favDetailController
|
||||
.onCancelFav(favList[index].id),
|
||||
);
|
||||
}, childCount: favList.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -204,18 +210,30 @@ class _FavDetailPageState extends State<FavDetailPage> {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: Center(child: Text('加载中')),
|
||||
),
|
||||
// 骨架屏
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
}, childCount: 10),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: MediaQuery.of(context).padding.bottom + 20,
|
||||
child: Container(
|
||||
height: MediaQuery.of(context).padding.bottom + 60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_favDetailController.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
|
@ -10,134 +10,109 @@ import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
|
||||
import '../controller.dart';
|
||||
|
||||
// 收藏视频卡片 - 水平布局
|
||||
class FavVideoCardH extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
final FavDetailController _favDetailController =
|
||||
Get.put(FavDetailController());
|
||||
final Function? callFn;
|
||||
|
||||
FavVideoCardH({Key? key, required this.videoItem}) : super(key: key);
|
||||
const FavVideoCardH({Key? key, required this.videoItem, this.callFn})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int id = videoItem.id;
|
||||
String bvid = videoItem.bvid ?? IdUtils.av2bv(id);
|
||||
String heroTag = Utils.makeHeroTag(id);
|
||||
return Dismissible(
|
||||
movementDuration: const Duration(milliseconds: 300),
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.clear_all_rounded),
|
||||
SizedBox(width: 6),
|
||||
Text('取消收藏')
|
||||
],
|
||||
)),
|
||||
direction: DismissDirection.endToStart,
|
||||
key: ValueKey<int>(videoItem.id),
|
||||
onDismissed: (DismissDirection direction) {
|
||||
_favDetailController.onCancelFav(videoItem.id);
|
||||
// widget.onDeleteNotice();
|
||||
},
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
// int? seasonId;
|
||||
String? epId;
|
||||
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
|
||||
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
// seasonId = videoItem.ogv['season_id'];
|
||||
epId = videoItem.epId;
|
||||
} else if (videoItem.page == 0 || videoItem.page > 1) {
|
||||
var result = await VideoHttp.videoIntro(bvid: bvid);
|
||||
if (result['status']) {
|
||||
epId = result['data'].epId;
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
// int? seasonId;
|
||||
String? epId;
|
||||
if (videoItem.ogv != null && videoItem.ogv['type_name'] == '番剧') {
|
||||
videoItem.cid = await SearchHttp.ab2c(bvid: bvid);
|
||||
// seasonId = videoItem.ogv['season_id'];
|
||||
epId = videoItem.epId;
|
||||
} else if (videoItem.page == 0 || videoItem.page > 1) {
|
||||
var result = await VideoHttp.videoIntro(bvid: bvid);
|
||||
if (result['status']) {
|
||||
epId = result['data'].epId;
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> parameters = {
|
||||
'bvid': bvid,
|
||||
'cid': videoItem.cid.toString(),
|
||||
'epId': epId ?? '',
|
||||
};
|
||||
// if (seasonId != null) {
|
||||
// parameters['seasonId'] = seasonId.toString();
|
||||
// }
|
||||
Get.toNamed('/video', parameters: parameters, arguments: {
|
||||
'videoItem': videoItem,
|
||||
'heroTag': heroTag,
|
||||
'videoType':
|
||||
epId != null ? SearchType.media_bangumi : SearchType.video,
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
Map<String, String> parameters = {
|
||||
'bvid': bvid,
|
||||
'cid': videoItem.cid.toString(),
|
||||
'epId': epId ?? '',
|
||||
};
|
||||
// if (seasonId != null) {
|
||||
// parameters['seasonId'] = seasonId.toString();
|
||||
// }
|
||||
Get.toNamed('/video', parameters: parameters, arguments: {
|
||||
'videoItem': videoItem,
|
||||
'heroTag': heroTag,
|
||||
// 'videoType':
|
||||
// epId != null ? SearchType.media_bangumi : SearchType.video,
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
||||
return SizedBox(
|
||||
height: width / StyleString.aspectRatio,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: videoItem.pic,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: Colors.black54.withOpacity(0.4)),
|
||||
child: Text(
|
||||
Utils.timeFormat(videoItem.duration!),
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 4,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1, horizontal: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
color:
|
||||
Colors.black54.withOpacity(0.4)),
|
||||
child: Text(
|
||||
Utils.timeFormat(videoItem.duration!),
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: Colors.white),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
VideoContent(videoItem: videoItem, callFn: callFn)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -145,7 +120,8 @@ class FavVideoCardH extends StatelessWidget {
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const VideoContent({super.key, required this.videoItem});
|
||||
final Function? callFn;
|
||||
const VideoContent({super.key, required this.videoItem, this.callFn});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -159,7 +135,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
@ -167,22 +142,59 @@ class VideoContent extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
videoItem.owner.name,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
children: [
|
||||
StatView(
|
||||
theme: 'gray',
|
||||
view: videoItem.cntInfo['play'],
|
||||
Text(
|
||||
videoItem.owner.name,
|
||||
style: TextStyle(
|
||||
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
SizedBox(
|
||||
width: 26,
|
||||
height: 26,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(EdgeInsets.zero),
|
||||
),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('要取消收藏吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline),
|
||||
)),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await callFn!();
|
||||
Get.back();
|
||||
},
|
||||
child: const Text('确定取消'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.clear_outlined,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatDanMu(theme: 'gray', danmu: videoItem.cntInfo['danmaku'])
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/follow.dart';
|
||||
@ -5,41 +6,53 @@ import 'package:pilipala/models/follow/result.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class FollowController extends GetxController {
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
int pn = 1;
|
||||
int ps = 20;
|
||||
int total = 0;
|
||||
RxList<FollowItemModel> followList = [FollowItemModel()].obs;
|
||||
late int mid;
|
||||
late String name;
|
||||
var userInfo;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
mid = int.parse(
|
||||
Get.parameters['mid'] ?? user.get(UserBoxKey.userMid).toString());
|
||||
name = Get.parameters['name'] ?? user.get(UserBoxKey.userName);
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
mid = Get.parameters['mid'] != null
|
||||
? int.parse(Get.parameters['mid']!)
|
||||
: userInfo.mid;
|
||||
name = Get.parameters['name'] ?? userInfo.uname;
|
||||
}
|
||||
|
||||
Future queryFollowings(type) async {
|
||||
if (type == 'init') {
|
||||
pn = 1;
|
||||
loadingText.value == '加载中...';
|
||||
}
|
||||
if (loadingText.value == '没有更多了') {
|
||||
return;
|
||||
}
|
||||
var res = await FollowHttp.followings(
|
||||
vmid: mid,
|
||||
pn: pn,
|
||||
ps: 20,
|
||||
ps: ps,
|
||||
orderType: 'attention',
|
||||
);
|
||||
if (res['status']) {
|
||||
if (type == 'init') {
|
||||
followList.value = res['data'].list;
|
||||
total = res['data'].total;
|
||||
} else if (type == 'onRefresh') {
|
||||
followList.insertAll(0, res['data'].list);
|
||||
} else if (type == 'onLoad') {
|
||||
followList.addAll(res['data'].list);
|
||||
}
|
||||
if ((pn == 1 && total < ps) || res['data'].list.isEmpty) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
pn += 1;
|
||||
} else {
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/follow/result.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
@ -17,7 +19,6 @@ class _FollowPageState extends State<FollowPage> {
|
||||
final FollowController _followController = Get.put(FollowController());
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Future? _futureBuilderFuture;
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -27,16 +28,20 @@ class _FollowPageState extends State<FollowPage> {
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
if (!_isLoadingMore) {
|
||||
_isLoadingMore = true;
|
||||
await _followController.queryFollowings('onLoad');
|
||||
_isLoadingMore = false;
|
||||
}
|
||||
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||
_followController.queryFollowings('onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -61,14 +66,40 @@ class _FollowPageState extends State<FollowPage> {
|
||||
if (data['status']) {
|
||||
List<FollowItemModel> list = _followController.followList;
|
||||
return Obx(
|
||||
() => list.length == 1
|
||||
? const SizedBox()
|
||||
: ListView.builder(
|
||||
() => list.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: list.length,
|
||||
itemCount: list.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return followItem(item: list[index]);
|
||||
if (index == list.length) {
|
||||
return Container(
|
||||
height:
|
||||
MediaQuery.of(context).padding.bottom +
|
||||
60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_followController.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return followItem(item: list[index]);
|
||||
}
|
||||
},
|
||||
)
|
||||
: const CustomScrollView(
|
||||
slivers: [NoData()],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
@ -27,12 +27,6 @@ Widget followItem({item}) {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
item.sign,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
dense: true,
|
||||
trailing: const SizedBox(width: 6),
|
||||
);
|
||||
}
|
||||
|
@ -8,10 +8,13 @@ import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HistoryController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
RxList<HisListItem> historyList = [HisListItem()].obs;
|
||||
bool isLoadingMore = false;
|
||||
RxList<HisListItem> historyList = <HisListItem>[].obs;
|
||||
RxBool isLoadingMore = false.obs;
|
||||
RxBool pauseStatus = false.obs;
|
||||
Box localCache = GStrorage.localCache;
|
||||
RxBool isLoading = false.obs;
|
||||
RxBool enableMultiple = false.obs;
|
||||
RxInt checkedCount = 0.obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
@ -26,9 +29,9 @@ class HistoryController extends GetxController {
|
||||
max = historyList.last.history!.oid!;
|
||||
viewAt = historyList.last.viewAt!;
|
||||
}
|
||||
isLoadingMore = true;
|
||||
isLoadingMore.value = true;
|
||||
var res = await UserHttp.historyList(max, viewAt);
|
||||
isLoadingMore = false;
|
||||
isLoadingMore.value = false;
|
||||
if (res['status']) {
|
||||
if (type == 'onload') {
|
||||
historyList.addAll(res['data'].list);
|
||||
@ -120,4 +123,80 @@ class HistoryController extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 删除某条历史记录
|
||||
Future delHistory(kid, business) async {
|
||||
String resKid = 'archive_$kid';
|
||||
if (business == 'live') {
|
||||
resKid = 'live_$kid';
|
||||
} else if (business.contains('article')) {
|
||||
resKid = 'article_$kid';
|
||||
}
|
||||
|
||||
var res = await UserHttp.delHistory(resKid);
|
||||
if (res['status']) {
|
||||
historyList.removeWhere((e) => e.kid == kid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除已看历史记录
|
||||
Future onDelHistory() async {
|
||||
/// TODO 优化
|
||||
List<HisListItem> result =
|
||||
historyList.where((e) => e.progress == -1).toList();
|
||||
for (HisListItem i in result) {
|
||||
String resKid = 'archive_${i.kid}';
|
||||
await UserHttp.delHistory(resKid);
|
||||
historyList.removeWhere((e) => e.kid == i.kid);
|
||||
}
|
||||
SmartDialog.showToast('操作完成');
|
||||
}
|
||||
|
||||
// 删除选中的记录
|
||||
Future onDelCheckedHistory() async {
|
||||
SmartDialog.show(
|
||||
useSystem: true,
|
||||
animationType: SmartAnimationType.centerFade_otherSlide,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('确认删除所选历史记录吗?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => SmartDialog.dismiss(),
|
||||
child: Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
/// TODO 优化
|
||||
await SmartDialog.dismiss();
|
||||
SmartDialog.showLoading(msg: '请求中');
|
||||
List<HisListItem> result =
|
||||
historyList.where((e) => e.checked!).toList();
|
||||
for (HisListItem i in result) {
|
||||
String str = 'archive';
|
||||
try {
|
||||
str = i.history!.business!;
|
||||
} catch (_) {}
|
||||
String resKid = '${str}_${i.kid}';
|
||||
await UserHttp.delHistory(resKid);
|
||||
historyList.removeWhere((e) => e.kid == i.kid);
|
||||
}
|
||||
checkedCount.value = 0;
|
||||
SmartDialog.dismiss();
|
||||
enableMultiple.value = false;
|
||||
},
|
||||
child: const Text('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/pages/history/index.dart';
|
||||
|
||||
import 'widgets/item.dart';
|
||||
@ -16,66 +18,166 @@ class HistoryPage extends StatefulWidget {
|
||||
class _HistoryPageState extends State<HistoryPage> {
|
||||
final HistoryController _historyController = Get.put(HistoryController());
|
||||
Future? _futureBuilderFuture;
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_futureBuilderFuture = _historyController.queryHistoryList();
|
||||
super.initState();
|
||||
|
||||
_historyController.scrollController.addListener(
|
||||
scrollController = _historyController.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (_historyController.scrollController.position.pixels >=
|
||||
_historyController.scrollController.position.maxScrollExtent -
|
||||
300) {
|
||||
if (!_historyController.isLoadingMore) {
|
||||
_historyController.onLoad();
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
if (!_historyController.isLoadingMore.value) {
|
||||
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
|
||||
_historyController.onLoad();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
_historyController.enableMultiple.listen((p0) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
// 选中
|
||||
onChoose(index) {
|
||||
_historyController.historyList[index].checked =
|
||||
!_historyController.historyList[index].checked!;
|
||||
_historyController.checkedCount.value =
|
||||
_historyController.historyList.where((item) => item.checked!).length;
|
||||
_historyController.historyList.refresh();
|
||||
}
|
||||
|
||||
// 更新多选状态
|
||||
onUpdateMultiple() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
title: Text(
|
||||
'观看记录',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (String type) {
|
||||
// 处理菜单项选择的逻辑
|
||||
switch (type) {
|
||||
case 'pause':
|
||||
_historyController.onPauseHistory();
|
||||
break;
|
||||
case 'clear':
|
||||
_historyController.onClearHistory();
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'pause',
|
||||
child: Obx(
|
||||
() => Text(!_historyController.pauseStatus.value
|
||||
? '暂停观看记录'
|
||||
: '恢复观看记录'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'clear',
|
||||
child: Text('清空观看记录'),
|
||||
),
|
||||
],
|
||||
appBar: AppBarWidget(
|
||||
visible: _historyController.enableMultiple.value,
|
||||
child1: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
onPressed: () => Get.back(),
|
||||
icon: const Icon(Icons.arrow_back_outlined),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
title: Text(
|
||||
'观看记录',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
actions: [
|
||||
// TextButton(
|
||||
// onPressed: () {
|
||||
// _historyController.enableMultiple.value = true;
|
||||
// setState(() {});
|
||||
// },
|
||||
// child: const Text('多选'),
|
||||
// ),
|
||||
IconButton(
|
||||
onPressed: () => Get.toNamed('/historySearch'),
|
||||
icon: const Icon(Icons.search_outlined),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (String type) {
|
||||
// 处理菜单项选择的逻辑
|
||||
switch (type) {
|
||||
case 'pause':
|
||||
_historyController.onPauseHistory();
|
||||
break;
|
||||
case 'clear':
|
||||
_historyController.onClearHistory();
|
||||
break;
|
||||
case 'del':
|
||||
_historyController.onDelHistory();
|
||||
break;
|
||||
case 'multiple':
|
||||
_historyController.enableMultiple.value = true;
|
||||
setState(() {});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: 'pause',
|
||||
child: Obx(
|
||||
() => Text(!_historyController.pauseStatus.value
|
||||
? '暂停观看记录'
|
||||
: '恢复观看记录'),
|
||||
),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'clear',
|
||||
child: Text('清空观看记录'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'del',
|
||||
child: Text('删除已看记录'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'multiple',
|
||||
child: Text('多选删除'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
child2: AppBar(
|
||||
titleSpacing: 0,
|
||||
centerTitle: false,
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
_historyController.enableMultiple.value = false;
|
||||
for (var item in _historyController.historyList) {
|
||||
item.checked = false;
|
||||
}
|
||||
_historyController.checkedCount.value = 0;
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.close_outlined),
|
||||
),
|
||||
title: Obx(
|
||||
() => Text(
|
||||
'已选择${_historyController.checkedCount.value}项',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
for (var item in _historyController.historyList) {
|
||||
item.checked = true;
|
||||
}
|
||||
_historyController.checkedCount.value =
|
||||
_historyController.historyList.length;
|
||||
_historyController.historyList.refresh();
|
||||
},
|
||||
child: const Text('全选'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _historyController.onDelCheckedHistory(),
|
||||
child: Text(
|
||||
'删除',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
@ -89,26 +191,32 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
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']) {
|
||||
return Obx(
|
||||
() => _historyController.historyList.isEmpty
|
||||
? const SliverToBoxAdapter(
|
||||
child: Center(
|
||||
child: Text('没数据'),
|
||||
),
|
||||
)
|
||||
: SliverList(
|
||||
() => _historyController.historyList.isNotEmpty
|
||||
? SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return HistoryItem(
|
||||
videoItem:
|
||||
_historyController.historyList[index],
|
||||
ctr: _historyController,
|
||||
onChoose: () => onChoose(index),
|
||||
onUpdateMultiple: () => onUpdateMultiple(),
|
||||
);
|
||||
},
|
||||
childCount:
|
||||
_historyController.historyList.length),
|
||||
),
|
||||
)
|
||||
: _historyController.isLoadingMore.value
|
||||
? const SliverToBoxAdapter(
|
||||
child: Center(child: Text('加载中')),
|
||||
)
|
||||
: const NoData(),
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
@ -134,6 +242,36 @@ class _HistoryPageState extends State<HistoryPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// bottomNavigationBar: BottomAppBar(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
|
||||
const AppBarWidget({
|
||||
required this.child1,
|
||||
required this.child2,
|
||||
required this.visible,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
final PreferredSizeWidget child1;
|
||||
final PreferredSizeWidget child2;
|
||||
final bool visible;
|
||||
@override
|
||||
Size get preferredSize => child1.preferredSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||
return ScaleTransition(
|
||||
scale: animation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: !visible ? child1 : child2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,17 +5,30 @@ import 'package:pilipala/common/constants.dart';
|
||||
import 'package:pilipala/common/widgets/badge.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/http/video.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/common/business_type.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/models/live/item.dart';
|
||||
import 'package:pilipala/pages/history/index.dart';
|
||||
import 'package:pilipala/pages/history_search/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class HistoryItem extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const HistoryItem({super.key, required this.videoItem});
|
||||
final dynamic ctr;
|
||||
final Function? onChoose;
|
||||
final Function? onUpdateMultiple;
|
||||
const HistoryItem({
|
||||
super.key,
|
||||
required this.videoItem,
|
||||
this.ctr,
|
||||
this.onChoose,
|
||||
this.onUpdateMultiple,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -24,6 +37,11 @@ class HistoryItem extends StatelessWidget {
|
||||
String heroTag = Utils.makeHeroTag(aid);
|
||||
return InkWell(
|
||||
onTap: () async {
|
||||
if (ctr!.enableMultiple.value) {
|
||||
feedBack();
|
||||
onChoose!();
|
||||
return;
|
||||
}
|
||||
if (videoItem.history.business.contains('article')) {
|
||||
int cid = videoItem.history.cid ??
|
||||
// videoItem.history.oid ??
|
||||
@ -36,20 +54,23 @@ class HistoryItem extends StatelessWidget {
|
||||
'pageTitle': videoItem.title
|
||||
},
|
||||
);
|
||||
} else if (videoItem.history.business == 'live' &&
|
||||
videoItem.liveStatus == 1) {
|
||||
LiveItemModel liveItem = LiveItemModel.fromJson({
|
||||
'face': videoItem.authorFace,
|
||||
'roomid': videoItem.history.oid,
|
||||
'pic': videoItem.cover,
|
||||
'title': videoItem.title,
|
||||
'uname': videoItem.authorName,
|
||||
'cover': videoItem.cover,
|
||||
});
|
||||
Get.toNamed(
|
||||
'/liveRoom?roomid=${videoItem.history.oid}',
|
||||
arguments: {'liveItem': liveItem},
|
||||
);
|
||||
} else if (videoItem.history.business == 'live') {
|
||||
if (videoItem.liveStatus == 1) {
|
||||
LiveItemModel liveItem = LiveItemModel.fromJson({
|
||||
'face': videoItem.authorFace,
|
||||
'roomid': videoItem.history.oid,
|
||||
'pic': videoItem.cover,
|
||||
'title': videoItem.title,
|
||||
'uname': videoItem.authorName,
|
||||
'cover': videoItem.cover,
|
||||
});
|
||||
Get.toNamed(
|
||||
'/liveRoom?roomid=${videoItem.history.oid}',
|
||||
arguments: {'liveItem': liveItem},
|
||||
);
|
||||
} else {
|
||||
SmartDialog.showToast('直播未开播');
|
||||
}
|
||||
} else if (videoItem.badge == '番剧' ||
|
||||
videoItem.tagName.contains('动画')) {
|
||||
/// hack
|
||||
@ -68,7 +89,7 @@ class HistoryItem extends StatelessWidget {
|
||||
arguments: {
|
||||
'pic': pic,
|
||||
'heroTag': heroTag,
|
||||
'videoType': SearchType.media_bangumi,
|
||||
// 'videoType': SearchType.media_bangumi,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@ -96,7 +117,7 @@ class HistoryItem extends StatelessWidget {
|
||||
arguments: {
|
||||
'pic': pic,
|
||||
'heroTag': heroTag,
|
||||
'videoType': SearchType.media_bangumi,
|
||||
// 'videoType': SearchType.media_bangumi,
|
||||
'bangumiItem': res['data'],
|
||||
},
|
||||
);
|
||||
@ -111,11 +132,22 @@ class HistoryItem extends StatelessWidget {
|
||||
arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (ctr is HistorySearchController) {
|
||||
return;
|
||||
}
|
||||
if (!ctr!.enableMultiple.value) {
|
||||
feedBack();
|
||||
ctr!.enableMultiple.value = true;
|
||||
onChoose!();
|
||||
onUpdateMultiple!();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
StyleString.cardSpace, 5, StyleString.cardSpace, 5),
|
||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double width =
|
||||
@ -126,53 +158,110 @@ class HistoryItem extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: (videoItem.cover != ''
|
||||
? videoItem.cover
|
||||
: videoItem.covers.first),
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: StyleString.aspectRatio,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, boxConstraints) {
|
||||
double maxWidth = boxConstraints.maxWidth;
|
||||
double maxHeight = boxConstraints.maxHeight;
|
||||
return Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: heroTag,
|
||||
child: NetworkImgLayer(
|
||||
src: (videoItem.cover != ''
|
||||
? videoItem.cover
|
||||
: videoItem.covers.first),
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
),
|
||||
),
|
||||
if (!BusinessType
|
||||
.hiddenDurationType.hiddenDurationType
|
||||
.contains(videoItem.history.business))
|
||||
PBadge(
|
||||
text: videoItem.progress == -1
|
||||
? '已看完'
|
||||
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
// 右上角
|
||||
if (BusinessType.showBadge.showBadge
|
||||
.contains(
|
||||
videoItem.history.business) ||
|
||||
videoItem.history.business ==
|
||||
BusinessType.live.type)
|
||||
PBadge(
|
||||
text: videoItem.badge,
|
||||
top: 6.0,
|
||||
right: 6.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => Positioned.fill(
|
||||
child: AnimatedOpacity(
|
||||
opacity: ctr!.enableMultiple.value ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.black.withOpacity(
|
||||
ctr!.enableMultiple.value &&
|
||||
videoItem.checked
|
||||
? 0.6
|
||||
: 0),
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: AnimatedScale(
|
||||
scale: videoItem.checked ? 1 : 0,
|
||||
duration:
|
||||
const Duration(milliseconds: 250),
|
||||
curve: Curves.easeInOut,
|
||||
child: IconButton(
|
||||
style: ButtonStyle(
|
||||
padding: MaterialStateProperty.all(
|
||||
EdgeInsets.zero),
|
||||
backgroundColor:
|
||||
MaterialStateProperty
|
||||
.resolveWith(
|
||||
(states) {
|
||||
return Colors.white
|
||||
.withOpacity(0.8);
|
||||
},
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
onChoose!();
|
||||
},
|
||||
icon: Icon(Icons.done_all_outlined,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!BusinessType
|
||||
.hiddenDurationType.hiddenDurationType
|
||||
.contains(videoItem.history.business))
|
||||
PBadge(
|
||||
text: videoItem.progress == -1
|
||||
? '已看完'
|
||||
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
|
||||
right: 6.0,
|
||||
bottom: 6.0,
|
||||
type: 'gray',
|
||||
),
|
||||
// 右上角
|
||||
if (BusinessType.showBadge.showBadge
|
||||
.contains(videoItem.history.business) ||
|
||||
videoItem.history.business ==
|
||||
BusinessType.live.type)
|
||||
PBadge(
|
||||
text: videoItem.badge,
|
||||
top: 6.0,
|
||||
right: 6.0,
|
||||
bottom: null,
|
||||
left: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
VideoContent(videoItem: videoItem)
|
||||
VideoContent(videoItem: videoItem, ctr: ctr)
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -187,7 +276,8 @@ class HistoryItem extends StatelessWidget {
|
||||
|
||||
class VideoContent extends StatelessWidget {
|
||||
final dynamic videoItem;
|
||||
const VideoContent({super.key, required this.videoItem});
|
||||
final dynamic? ctr;
|
||||
const VideoContent({super.key, required this.videoItem, this.ctr});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -201,7 +291,6 @@ class VideoContent extends StatelessWidget {
|
||||
videoItem.title,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
@ -232,6 +321,7 @@ class VideoContent extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
Utils.dateFormat(videoItem.viewAt!),
|
||||
@ -239,7 +329,59 @@ class VideoContent extends StatelessWidget {
|
||||
fontSize:
|
||||
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||
color: Theme.of(context).colorScheme.outline),
|
||||
)
|
||||
),
|
||||
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>>[
|
||||
if (videoItem.badge != '番剧' &&
|
||||
!videoItem.tagName.contains('动画') &&
|
||||
videoItem.history.business != 'live' &&
|
||||
!videoItem.history.business.contains('article'))
|
||||
PopupMenuItem<String>(
|
||||
onTap: () async {
|
||||
var res = await UserHttp.toViewLater(
|
||||
bvid: videoItem.history.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))
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<String>(
|
||||
onTap: () => ctr!.delHistory(
|
||||
videoItem.kid, videoItem.history.business),
|
||||
value: 'pause',
|
||||
height: 35,
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.close_outlined, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text('删除记录', style: TextStyle(fontSize: 13))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
91
lib/pages/history_search/controller.dart
Normal file
91
lib/pages/history_search/controller.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/user.dart';
|
||||
import 'package:pilipala/models/user/history.dart';
|
||||
|
||||
class HistorySearchController extends GetxController {
|
||||
final ScrollController scrollController = ScrollController();
|
||||
Rx<TextEditingController> controller = TextEditingController().obs;
|
||||
final FocusNode searchFocusNode = FocusNode();
|
||||
RxString searchKeyWord = ''.obs;
|
||||
String hintText = '搜索';
|
||||
RxString loadingStatus = 'init'.obs;
|
||||
RxString loadingText = '加载中...'.obs;
|
||||
bool hasRequest = false;
|
||||
late int mid;
|
||||
RxString uname = ''.obs;
|
||||
int pn = 1;
|
||||
int count = 0;
|
||||
RxList<HisListItem> historyList = <HisListItem>[].obs;
|
||||
RxBool enableMultiple = false.obs;
|
||||
|
||||
// 清空搜索
|
||||
void onClear() {
|
||||
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
|
||||
controller.value.clear();
|
||||
searchKeyWord.value = '';
|
||||
} else {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
void onChange(value) {
|
||||
searchKeyWord.value = value;
|
||||
}
|
||||
|
||||
// 提交搜索内容
|
||||
void submit() {
|
||||
loadingStatus.value = 'loading';
|
||||
if (hasRequest) {
|
||||
pn = 1;
|
||||
searchHistories();
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索视频
|
||||
Future searchHistories({type = 'init'}) async {
|
||||
if (type == 'onLoad' && loadingText.value == '没有更多了') {
|
||||
return;
|
||||
}
|
||||
var res = await UserHttp.searchHistory(
|
||||
pn: pn,
|
||||
keyword: controller.value.text,
|
||||
);
|
||||
if (res['status']) {
|
||||
if (type == 'init' && pn == 1) {
|
||||
historyList.value = res['data'].list;
|
||||
} else {
|
||||
historyList.addAll(res['data'].list);
|
||||
}
|
||||
count = res['data'].page['total'];
|
||||
if (historyList.length == count) {
|
||||
loadingText.value = '没有更多了';
|
||||
}
|
||||
pn += 1;
|
||||
hasRequest = true;
|
||||
}
|
||||
loadingStatus.value = 'finish';
|
||||
return res;
|
||||
}
|
||||
|
||||
onLoad() {
|
||||
searchHistories(type: 'onLoad');
|
||||
}
|
||||
|
||||
Future delHistory(kid, business) async {
|
||||
String resKid = 'archive_$kid';
|
||||
if (business == 'live') {
|
||||
resKid = 'live_$kid';
|
||||
} else if (business.contains('article')) {
|
||||
resKid = 'article_$kid';
|
||||
}
|
||||
|
||||
var res = await UserHttp.delHistory(resKid);
|
||||
if (res['status']) {
|
||||
historyList.removeWhere((e) => e.kid == kid);
|
||||
SmartDialog.showToast(res['msg']);
|
||||
}
|
||||
loadingStatus.value = 'finish';
|
||||
}
|
||||
}
|
4
lib/pages/history_search/index.dart
Normal file
4
lib/pages/history_search/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library history_search;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
174
lib/pages/history_search/view.dart
Normal file
174
lib/pages/history_search/view.dart
Normal file
@ -0,0 +1,174 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/video_card_h.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/pages/history/widgets/item.dart';
|
||||
|
||||
import 'controller.dart';
|
||||
|
||||
class HistorySearchPage extends StatefulWidget {
|
||||
const HistorySearchPage({super.key});
|
||||
|
||||
@override
|
||||
State<HistorySearchPage> createState() => _HistorySearchPageState();
|
||||
}
|
||||
|
||||
class _HistorySearchPageState extends State<HistorySearchPage> {
|
||||
final HistorySearchController _historySearchCtr =
|
||||
Get.put(HistorySearchController());
|
||||
late ScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = _historySearchCtr.scrollController;
|
||||
scrollController.addListener(
|
||||
() {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 300) {
|
||||
EasyThrottle.throttle('history', const Duration(seconds: 1), () {
|
||||
_historySearchCtr.onLoad();
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(() {});
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
titleSpacing: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => _historySearchCtr.submit(),
|
||||
icon: const Icon(Icons.search_outlined, size: 22)),
|
||||
const SizedBox(width: 10)
|
||||
],
|
||||
title: Obx(
|
||||
() => TextField(
|
||||
autofocus: true,
|
||||
focusNode: _historySearchCtr.searchFocusNode,
|
||||
controller: _historySearchCtr.controller.value,
|
||||
textInputAction: TextInputAction.search,
|
||||
onChanged: (value) => _historySearchCtr.onChange(value),
|
||||
decoration: InputDecoration(
|
||||
hintText: _historySearchCtr.hintText,
|
||||
border: InputBorder.none,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
Icons.clear,
|
||||
size: 22,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
onPressed: () => _historySearchCtr.onClear(),
|
||||
),
|
||||
),
|
||||
onSubmitted: (String value) => _historySearchCtr.submit(),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Obx(
|
||||
() => Column(
|
||||
children: _historySearchCtr.loadingStatus.value == 'init'
|
||||
? [const SizedBox()]
|
||||
: [
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: _historySearchCtr.searchHistories(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
Map data = snapshot.data as Map;
|
||||
if (data['status']) {
|
||||
return Obx(
|
||||
() => _historySearchCtr.historyList.isNotEmpty
|
||||
? ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount:
|
||||
_historySearchCtr.historyList.length +
|
||||
1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index ==
|
||||
_historySearchCtr
|
||||
.historyList.length) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom +
|
||||
60,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context)
|
||||
.padding
|
||||
.bottom),
|
||||
child: Center(
|
||||
child: Obx(
|
||||
() => Text(
|
||||
_historySearchCtr
|
||||
.loadingText.value,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.outline,
|
||||
fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return HistoryItem(
|
||||
videoItem: _historySearchCtr
|
||||
.historyList[index],
|
||||
ctr: _historySearchCtr,
|
||||
onChoose: null,
|
||||
onUpdateMultiple: () => null,
|
||||
);
|
||||
;
|
||||
}
|
||||
},
|
||||
)
|
||||
: _historySearchCtr.loadingStatus.value ==
|
||||
'loading'
|
||||
? const SizedBox(child: Text('加载中...'))
|
||||
: const CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
NoData(),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
HttpError(
|
||||
errMsg: data['msg'],
|
||||
fn: () => setState(() {}),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return ListView.builder(
|
||||
itemCount: 10,
|
||||
itemBuilder: (context, index) {
|
||||
return const VideoCardHSkeleton();
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/black.dart';
|
||||
import 'package:pilipala/models/common/tab_type.dart';
|
||||
import 'package:pilipala/models/user/black.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
|
||||
class HomeController extends GetxController with GetTickerProviderStateMixin {
|
||||
@ -11,16 +13,24 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
|
||||
late TabController tabController;
|
||||
late List tabsCtrList;
|
||||
late List<Widget> tabsPageList;
|
||||
Box user = GStrorage.user;
|
||||
Box userInfoCache = GStrorage.userInfo;
|
||||
RxBool userLogin = false.obs;
|
||||
RxString userFace = ''.obs;
|
||||
var userInfo;
|
||||
static Box setting = GStrorage.setting;
|
||||
late List<int> blackMidsList;
|
||||
|
||||
int currentPage = 1;
|
||||
int pageSize = 50;
|
||||
int total = 0;
|
||||
List<BlackListItem> blackList = [BlackListItem()];
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
|
||||
userFace.value = user.get(UserBoxKey.userFace) ?? '';
|
||||
userInfo = userInfoCache.get('userInfoCache');
|
||||
userLogin.value = userInfo != null;
|
||||
userFace.value = userInfo != null ? userInfo.face : '';
|
||||
|
||||
// 进行tabs配置
|
||||
tabs = tabsConfig;
|
||||
@ -47,8 +57,36 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
|
||||
}
|
||||
|
||||
// 更新登录状态
|
||||
void updateLoginStatus(val) {
|
||||
void updateLoginStatus(val) async {
|
||||
userInfo = await userInfoCache.get('userInfoCache');
|
||||
userLogin.value = val ?? false;
|
||||
userFace.value = user.get(UserBoxKey.userFace) ?? '';
|
||||
if (val) {
|
||||
// 获取黑名单
|
||||
await queryBlacklist();
|
||||
blackMidsList = blackList.map<int>((e) => e.mid!).toList();
|
||||
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
|
||||
return;
|
||||
}
|
||||
userFace.value = userInfo != null ? userInfo.face : '';
|
||||
}
|
||||
|
||||
Future queryBlacklist({type = 'init'}) async {
|
||||
if (type == 'init') {
|
||||
currentPage = 1;
|
||||
}
|
||||
var result = await BlackHttp.blackList(pn: currentPage, ps: pageSize);
|
||||
if (result['status']) {
|
||||
if (type == 'init') {
|
||||
blackList = result['data'].list;
|
||||
total = result['data'].total;
|
||||
} else {
|
||||
blackList.addAll(result['data'].list);
|
||||
}
|
||||
currentPage += 1;
|
||||
if (blackList.length < total) {
|
||||
await queryBlacklist(type: 'onLoad');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user