Compare commits
340 Commits
v1.0.3.082
...
feature-au
| Author | SHA1 | Date | |
|---|---|---|---|
| cb3fd24cf7 | |||
| 27e268b2a0 | |||
| 1a3da13a4d | |||
| 5edcc756a0 | |||
| a8e57d9b0e | |||
| 96523a99ce | |||
| d105718fbf | |||
| 63992c6ec1 | |||
| 13ce50f730 | |||
| eaff4def1c | |||
| 3613e27643 | |||
| 720e9f0040 | |||
| e844870c34 | |||
| fd4eb0fad1 | |||
| e01292a8f9 | |||
| 1804e6ab00 | |||
| 0adc3257d2 | |||
| 2348c14008 | |||
| e65e6229ad | |||
| 41617a6c44 | |||
| ed35970d01 | |||
| 6668b6e520 | |||
| 2a4ad969d3 | |||
| bf905fa46e | |||
| d8d7ab22c9 | |||
| 1c3f8beeb1 | |||
| 819619563e | |||
| cfd2038e36 | |||
| 79be397f91 | |||
| 0a5dea0535 | |||
| eda8a5c6a7 | |||
| 101ae2e991 | |||
| c41679d6f5 | |||
| 6b5b1a8e31 | |||
| 1c370fb224 | |||
| fd97bd7455 | |||
| 81bf8d915c | |||
| 2db5c45158 | |||
| 2cd8e86864 | |||
| 8b28417962 | |||
| 5d79c7ebbf | |||
| 943553a4db | |||
| 59f7c52611 | |||
| b5209de56f | |||
| dc2bd04143 | |||
| 7cae946f21 | |||
| 50b5f221e8 | |||
| 2f901afd2f | |||
| 48030d5ee7 | |||
| 7b09c112c0 | |||
| 8aa38a36c6 | |||
| b9e255044e | |||
| 445a37d305 | |||
| 9fd5193259 | |||
| ab7dd149d3 | |||
| 5c6b8624d7 | |||
| 88e6eb607c | |||
| f0851c9737 | |||
| 3eb461f0cc | |||
| aa95d9020d | |||
| f30fb7a71c | |||
| 5ac700bfef | |||
| fe64967a87 | |||
| eec052c47e | |||
| b4cc542a4d | |||
| 8782462603 | |||
| b71558173e | |||
| 9744ec88a0 | |||
| a97f57642e | |||
| d0590933e0 | |||
| 41e9cfcbbb | |||
| 45bd4fc6d5 | |||
| 789d95e728 | |||
| 86c87dc1d5 | |||
| 3d6c270070 | |||
| f214c45448 | |||
| 5f26e19c62 | |||
| 960104929f | |||
| f25dab2eb8 | |||
| 15947e45da | |||
| 6c983cf849 | |||
| 4d5f3eb14a | |||
| 94aef39f7b | |||
| 424bdd9fff | |||
| c77c8e683d | |||
| ad1ced51f2 | |||
| c0c1a3a59a | |||
| 690b168a45 | |||
| 7f7919d585 | |||
| 76974bd874 | |||
| f8173b0b5f | |||
| 7ecfbac786 | |||
| c794eb465c | |||
| 7adbf76362 | |||
| 353287e053 | |||
| 856d699fd7 | |||
| 15914e5961 | |||
| 0e5b1633be | |||
| 24f22f8afa | |||
| 7c38340fc6 | |||
| 79f661e5da | |||
| 3ba90d6c85 | |||
| 85e86f1d61 | |||
| ec58d060bf | |||
| 7576f39010 | |||
| 77f47b8242 | |||
| 4b3e791370 | |||
| f25f5c28d9 | |||
| 7ca367869b | |||
| 7222ca4425 | |||
| 692d596818 | |||
| 47e3cf46e4 | |||
| 6b2229dddc | |||
| 1e202979d3 | |||
| 82ad1662aa | |||
| 7feda8d187 | |||
| d83b4bc59e | |||
| 1d1d4f8c7d | |||
| 1061ffca3d | |||
| 52ee5b36be | |||
| 87807466ff | |||
| 227cfb637e | |||
| 2c4ee083ef | |||
| 2ef3a8cd25 | |||
| 3a19b089c5 | |||
| 21e6d1aa52 | |||
| 10965fae73 | |||
| 8f987e8352 | |||
| 4e147b6f18 | |||
| 6d982bdba2 | |||
| 3edce0c4ec | |||
| 2eb7b388e1 | |||
| 2fd23aa20d | |||
| 2ecd1d3dab | |||
| 26d8ab5b43 | |||
| 8fa59f8f58 | |||
| 6ea4626288 | |||
| 329f158155 | |||
| d6b6df3eed | |||
| 3f50aab12d | |||
| 227da31857 | |||
| 7ad6b25abe | |||
| f79e4765c2 | |||
| e8671dee6b | |||
| 97268c36dc | |||
| d1272efad4 | |||
| ba815bccda | |||
| 75fb81b959 | |||
| a48d15ee73 | |||
| 620d7214df | |||
| 7458c33173 | |||
| 97fa047c60 | |||
| 9f4b928257 | |||
| afcc5a9a02 | |||
| 7181db66bd | |||
| 820a1e9162 | |||
| 566f75f760 | |||
| 4c49f466db | |||
| 8f97431665 | |||
| f543be562a | |||
| 93383a5c65 | |||
| fd57ebc4cc | |||
| 9de9b885bc | |||
| c2db6a50f0 | |||
| 427bd2eb79 | |||
| 8c01de47e4 | |||
| 83b27e7231 | |||
| fc767fab97 | |||
| 7db9d290f5 | |||
| bb66de29d4 | |||
| dd97636494 | |||
| 95f5ac6a71 | |||
| 7fa7152245 | |||
| 41df90561b | |||
| 5c68772f7b | |||
| 262f244a98 | |||
| c91cfedfe2 | |||
| 1d9372b4f1 | |||
| e9095932ed | |||
| 3daa06a198 | |||
| 380ada9ae0 | |||
| 76bd5550c7 | |||
| 54c66d54da | |||
| 481d5e77d7 | |||
| 33413cdb51 | |||
| 277c7a25cb | |||
| 2c9b3e8854 | |||
| fff54a55a1 | |||
| 838467451b | |||
| 8803fbd777 | |||
| 7c9b5bb891 | |||
| 7867af0f85 | |||
| 6d2e0f2049 | |||
| 74ec4cccea | |||
| bd568c4945 | |||
| 097ab4310a | |||
| 09ff01905e | |||
| ef38844798 | |||
| 1922a91575 | |||
| 3c17d18acf | |||
| 4cf2fc3c23 | |||
| b9a47da92b | |||
| c16106d676 | |||
| 0e39453558 | |||
| 8ff4259972 | |||
| 5082dc6d59 | |||
| 627df8e6ad | |||
| 2467fd0dea | |||
| c6f6af4628 | |||
| 9e907f9151 | |||
| 22e17d437b | |||
| 8a06ce65a5 | |||
| 72ff3fdab0 | |||
| 517ca032d2 | |||
| 396f9fbbac | |||
| c0332c74d7 | |||
| 2669b41ede | |||
| 81dace96d7 | |||
| d693d7ad6c | |||
| 8c02a566f6 | |||
| a2420d0bef | |||
| 29d3f78da9 | |||
| a864bea3f4 | |||
| 070156da86 | |||
| 69f846760d | |||
| 2ca79003bf | |||
| 18af065a1e | |||
| 0ad54d8c0b | |||
| 0dfcd4ed40 | |||
| 5b953ae0be | |||
| 392980f0e8 | |||
| 4c938ed8aa | |||
| 7f961e998c | |||
| e6b307ddd7 | |||
| f5b4ad33c6 | |||
| 3e8216923f | |||
| 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 | |||
| c8810b458f | |||
| 0003c057cd |
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/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" />
|
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/850shots_so.png" width="32%" alt="home" />
|
||||||
<br/>
|
<br/>
|
||||||
|
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/main_screen.png" width="96%" alt="home" />
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### 开发环境
|
## 开发环境
|
||||||
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -30,7 +31,15 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
### 功能
|
|
||||||
|
## 技术交流
|
||||||
|
|
||||||
|
Telegram: https://t.me/+lm_oOVmF0RJiODk1
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
目前着重移动端(Android、iOS),暂时没有适配桌面端、Pad端、手表端等
|
目前着重移动端(Android、iOS),暂时没有适配桌面端、Pad端、手表端等
|
||||||
|
|
||||||
@ -74,12 +83,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
|||||||
- [ ] 弹幕
|
- [ ] 弹幕
|
||||||
- [ ] 字幕
|
- [ ] 字幕
|
||||||
- [x] 记忆播放
|
- [x] 记忆播放
|
||||||
|
- [x] 视频比例:高度/宽度适应、填充、包含等
|
||||||
|
|
||||||
- [x] 搜索相关
|
- [x] 搜索相关
|
||||||
- [x] 热搜
|
- [x] 热搜
|
||||||
- [x] 搜索历史
|
- [x] 搜索历史
|
||||||
- [x] 默认搜索词
|
- [x] 默认搜索词
|
||||||
- [x] 投稿、番剧、直播间、用户搜索
|
- [x] 投稿、番剧、直播间、用户搜索
|
||||||
|
- [x] 视频搜索排序、按时长筛选
|
||||||
|
|
||||||
- [x] 视频详情页相关
|
- [x] 视频详情页相关
|
||||||
- [x] 视频选集(分p)切换
|
- [x] 视频选集(分p)切换
|
||||||
@ -96,26 +107,29 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
|
|||||||
- [x] 图片质量设定
|
- [x] 图片质量设定
|
||||||
- [x] 主题模式:亮色/暗色/跟随系统
|
- [x] 主题模式:亮色/暗色/跟随系统
|
||||||
- [x] 震动反馈(可选)
|
- [x] 震动反馈(可选)
|
||||||
|
- [x] 高帧率
|
||||||
|
- [x] 自动全屏
|
||||||
- [ ] 等等
|
- [ ] 等等
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
### 下载
|
## 下载
|
||||||
|
|
||||||
可以通过右侧release进行下载或拉取代码到本地进行编译
|
可以通过右侧release进行下载或拉取代码到本地进行编译
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
### 声明
|
## 声明
|
||||||
|
|
||||||
此项目(PiliPala)是个人为了兴趣而开发, 仅用于学习和测试。
|
此项目(PiliPala)是个人为了兴趣而开发, 仅用于学习和测试。
|
||||||
所用API皆从官方网站收集, 不提供任何破解内容。
|
所用API皆从官方网站收集, 不提供任何破解内容。
|
||||||
|
|
||||||
感谢使用
|
感谢使用
|
||||||
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
### 致谢
|
## 致谢
|
||||||
|
|
||||||
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
|
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
|
||||||
- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)
|
- [flutter_meedu_videoplayer](https://github.com/zezo357/flutter_meedu_videoplayer)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.guozhigq.pilipala">
|
package="com.guozhigq.pilipala">
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
@ -20,13 +21,32 @@
|
|||||||
"android.support.customtabs.action.CustomTabsService" />
|
"android.support.customtabs.action.CustomTabsService" />
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</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:label="PiliPala"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:enableOnBackInvokedCallback="true">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:fullBackupContent="false"
|
||||||
|
tools:replace="android:allowBackup">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name="com.ryanheise.audioservice.AudioServiceActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
@ -44,11 +64,208 @@
|
|||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme"
|
android:resource="@style/NormalTheme"
|
||||||
/>
|
/>
|
||||||
|
<!-- ADD THIS "SERVICE" element -->
|
||||||
|
<service
|
||||||
|
android:name="com.ryanheise.audioservice.AudioService"
|
||||||
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
tools:ignore="Instantiatable">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<!-- ADD THIS "RECEIVER" element -->
|
||||||
|
<receiver
|
||||||
|
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="Instantiatable">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name="com.ryanheise.audioservice.AudioService"
|
||||||
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="Instantiatable">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||||
|
android:exported="true"
|
||||||
|
tools:ignore="Instantiatable">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
@ -56,12 +273,13 @@
|
|||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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.READ_MEDIA_IMAGES"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
<!--
|
<!--
|
||||||
Media access permissions.
|
Media access permissions.
|
||||||
Android 13 or higher.
|
Android 13 or higher.
|
||||||
@ -69,4 +287,8 @@
|
|||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
BIN
android/app/src/main/res/drawable-hdpi/ic_notification_icon.png
Normal file
|
After Width: | Height: | Size: 528 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_notification_icon.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_notification_icon.png
Normal file
|
After Width: | Height: | Size: 648 B |
|
After Width: | Height: | Size: 962 B |
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M10.86,15.94l0,-4.27l-0.09,0l-1.77,0.63l0,0.69l1.01,-0.31l0,3.26z"/>
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M12.25,13.44v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82v-0.74c0,-1.9 -1.31,-1.82 -1.44,-1.82C13.55,11.62 12.25,11.53 12.25,13.44zM14.29,13.32v0.97c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01C14.07,12.3 14.29,12.57 14.29,13.32z"/>
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M11.99,5V1l-5,5l5,5V7c3.31,0 6,2.69 6,6s-2.69,6 -6,6s-6,-2.69 -6,-6h-2c0,4.42 3.58,8 8,8s8,-3.58 8,-8S16.41,5 11.99,5z"/>
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M10.89,16h-0.85v-3.26l-1.01,0.31v-0.69l1.77,-0.63h0.09V16z"/>
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M15.17,14.24c0,0.32 -0.03,0.6 -0.1,0.82s-0.17,0.42 -0.29,0.57s-0.28,0.26 -0.45,0.33s-0.37,0.1 -0.59,0.1s-0.41,-0.03 -0.59,-0.1s-0.33,-0.18 -0.46,-0.33s-0.23,-0.34 -0.3,-0.57s-0.11,-0.5 -0.11,-0.82V13.5c0,-0.32 0.03,-0.6 0.1,-0.82s0.17,-0.42 0.29,-0.57s0.28,-0.26 0.45,-0.33s0.37,-0.1 0.59,-0.1s0.41,0.03 0.59,0.1c0.18,0.07 0.33,0.18 0.46,0.33s0.23,0.34 0.3,0.57s0.11,0.5 0.11,0.82V14.24zM14.32,13.38c0,-0.19 -0.01,-0.35 -0.04,-0.48s-0.07,-0.23 -0.12,-0.31s-0.11,-0.14 -0.19,-0.17s-0.16,-0.05 -0.25,-0.05s-0.18,0.02 -0.25,0.05s-0.14,0.09 -0.19,0.17s-0.09,0.18 -0.12,0.31s-0.04,0.29 -0.04,0.48v0.97c0,0.19 0.01,0.35 0.04,0.48s0.07,0.24 0.12,0.32s0.11,0.14 0.19,0.17s0.16,0.05 0.25,0.05s0.18,-0.02 0.25,-0.05s0.14,-0.09 0.19,-0.17s0.09,-0.19 0.11,-0.32s0.04,-0.29 0.04,-0.48V13.38z"/>
|
||||||
|
</vector>
|
||||||
@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|||||||
3
android/app/src/main/res/raw/keep.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:keep="@drawable/*" />
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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 -->
|
<!-- 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">
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<!-- 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. -->
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
BIN
assets/images/ai.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
assets/sreenshot/main_screen.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
4
change_log/1.0.10.1016.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
## 1.0.10
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
+ 长按倍速抬起后未恢复默认倍速
|
||||||
26
change_log/1.0.11.1112.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
## 1.0.11
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
+ 适配了原生媒体通知栏 @Daydreamer-riri
|
||||||
|
+ 视频主题图标 @Daydreamer-riri
|
||||||
|
+ 关闭软件后自动画中画播放
|
||||||
|
+ UP主分组管理
|
||||||
|
+ md2样式底栏
|
||||||
|
+
|
||||||
|
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
+ 历史记录记忆播放
|
||||||
|
+ 部分类型视频连播
|
||||||
|
+ 播放速度选择框不支持返回手势
|
||||||
|
+ 播放速度选择框不支持返回手势
|
||||||
|
+ 视频播放速度总是显示1.0X
|
||||||
|
+ 评论页面计数错误
|
||||||
|
+ 退出视频还有声音
|
||||||
|
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
+ 视频加载速度
|
||||||
|
|
||||||
|
更多更新日志可在Github上查看
|
||||||
|
问题反馈、功能建议请查看「关于」页面。
|
||||||
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
@ -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
@ -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
@ -0,0 +1,22 @@
|
|||||||
|
## 1.0.7
|
||||||
|
|
||||||
|
默认倍速、直播弹幕、专栏等功能开发中
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
+ 弹幕设置、屏蔽功能
|
||||||
|
+ 不是很完美的后台播放功能
|
||||||
|
+ 不是很完美的画中画(pip)功能(Android端)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
+ 动态页面加载异常
|
||||||
|
+ 网络异常时页面空白
|
||||||
|
+ 竖屏全屏状态栏问题
|
||||||
|
+ iOS端代理请求异常
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
+ 图片预览
|
||||||
|
+ 全屏播放时自动旋转
|
||||||
|
+ 转发内容增加视频标题
|
||||||
|
|
||||||
|
更多更新日志可在Github上查看
|
||||||
|
问题反馈、功能建议请查看「关于」页面。
|
||||||
24
change_log/1.0.8.0917.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
## 1.0.8
|
||||||
|
|
||||||
|
直播弹幕、循环播放等功能开发中
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
+ 用户拉黑功能
|
||||||
|
+ gif图片保存
|
||||||
|
+ 删除已看历史记录
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
+ 弹幕数量较少
|
||||||
|
+ 弹幕屏蔽设置自动记忆
|
||||||
|
+ 动态页面渲染
|
||||||
|
+ 用户主页数据错乱
|
||||||
|
+ 大家都在搜空白
|
||||||
|
+ 默认自动全屏,顶部操作栏丢失
|
||||||
|
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
+ 全屏状态栏区域显示优化
|
||||||
|
+ 图片保存至PiliPala文件夹
|
||||||
|
|
||||||
|
更多更新日志可在Github上查看
|
||||||
|
问题反馈、功能建议请查看「关于」页面。
|
||||||
28
change_log/1.0.9.1015.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
## 1.0.9
|
||||||
|
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
+ 自定义倍速、默认倍速
|
||||||
|
+ 历史记录搜索
|
||||||
|
+ 收藏夹搜索
|
||||||
|
+ 历史记录多选删除
|
||||||
|
+ 视频循环播放
|
||||||
|
+ 免登录看1080P
|
||||||
|
+ 评论区视频链接跳转
|
||||||
|
+ up主分组
|
||||||
|
+ up主投稿搜索
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
+ 搜索视频标题乱码
|
||||||
|
+ 屏幕帧率
|
||||||
|
+ 动态页面渲染
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
+ 快进手势
|
||||||
|
+ 视频简介链接匹配
|
||||||
|
+ 视频全屏时安全区域
|
||||||
|
|
||||||
|
更多更新日志可在Github上查看
|
||||||
|
问题反馈、功能建议请查看「关于」页面。
|
||||||
@ -37,5 +37,11 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||||
|
'$(inherited)',
|
||||||
|
'AUDIO_SESSION_MICROPHONE=0'
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
PODS:
|
PODS:
|
||||||
|
- appscheme (1.0.4):
|
||||||
|
- Flutter
|
||||||
|
- audio_service (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- audio_session (0.0.1):
|
||||||
|
- Flutter
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
@ -10,8 +16,15 @@ PODS:
|
|||||||
- FMDB (2.7.5):
|
- FMDB (2.7.5):
|
||||||
- FMDB/standard (= 2.7.5)
|
- FMDB/standard (= 2.7.5)
|
||||||
- FMDB/standard (2.7.5)
|
- FMDB/standard (2.7.5)
|
||||||
- image_gallery_saver (2.0.2):
|
- gt3_flutter_plugin (0.0.8):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
<<<<<<< HEAD
|
||||||
|
- just_audio (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
=======
|
||||||
|
- GT3Captcha-iOS
|
||||||
|
- GT3Captcha-iOS (0.15.8.3)
|
||||||
|
>>>>>>> main
|
||||||
- media_kit_libs_ios_video (1.0.4):
|
- media_kit_libs_ios_video (1.0.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- media_kit_native_event_loop (1.0.0):
|
- media_kit_native_event_loop (1.0.0):
|
||||||
@ -26,6 +39,8 @@ PODS:
|
|||||||
- permission_handler_apple (9.1.1):
|
- permission_handler_apple (9.1.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- ReachabilitySwift (5.0.0)
|
- ReachabilitySwift (5.0.0)
|
||||||
|
- saver_gallery (0.0.1):
|
||||||
|
- Flutter
|
||||||
- screen_brightness_ios (0.1.0):
|
- screen_brightness_ios (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
@ -33,6 +48,10 @@ PODS:
|
|||||||
- sqflite (0.0.3):
|
- sqflite (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FMDB (>= 2.7.5)
|
- FMDB (>= 2.7.5)
|
||||||
|
- status_bar_control (3.2.1):
|
||||||
|
- Flutter
|
||||||
|
- system_proxy (0.0.1):
|
||||||
|
- Flutter
|
||||||
- url_launcher_ios (0.0.1):
|
- url_launcher_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- volume_controller (0.0.1):
|
- volume_controller (0.0.1):
|
||||||
@ -45,20 +64,31 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
|
- appscheme (from `.symlinks/plugins/appscheme/ios`)
|
||||||
|
- audio_service (from `.symlinks/plugins/audio_service/ios`)
|
||||||
|
- audio_session (from `.symlinks/plugins/audio_session/ios`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
|
||||||
|
<<<<<<< HEAD
|
||||||
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
|
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
|
||||||
|
- just_audio (from `.symlinks/plugins/just_audio/ios`)
|
||||||
|
=======
|
||||||
|
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
|
||||||
|
>>>>>>> main
|
||||||
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
|
||||||
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
|
||||||
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
|
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
|
||||||
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/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`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
|
||||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
@ -68,9 +98,16 @@ DEPENDENCIES:
|
|||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
- FMDB
|
- FMDB
|
||||||
|
- GT3Captcha-iOS
|
||||||
- ReachabilitySwift
|
- ReachabilitySwift
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
|
appscheme:
|
||||||
|
:path: ".symlinks/plugins/appscheme/ios"
|
||||||
|
audio_service:
|
||||||
|
:path: ".symlinks/plugins/audio_service/ios"
|
||||||
|
audio_session:
|
||||||
|
:path: ".symlinks/plugins/audio_session/ios"
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
@ -79,8 +116,15 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_volume_controller:
|
flutter_volume_controller:
|
||||||
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
:path: ".symlinks/plugins/flutter_volume_controller/ios"
|
||||||
|
<<<<<<< HEAD
|
||||||
image_gallery_saver:
|
image_gallery_saver:
|
||||||
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
||||||
|
just_audio:
|
||||||
|
:path: ".symlinks/plugins/just_audio/ios"
|
||||||
|
=======
|
||||||
|
gt3_flutter_plugin:
|
||||||
|
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
|
||||||
|
>>>>>>> main
|
||||||
media_kit_libs_ios_video:
|
media_kit_libs_ios_video:
|
||||||
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
|
||||||
media_kit_native_event_loop:
|
media_kit_native_event_loop:
|
||||||
@ -93,12 +137,18 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
|
saver_gallery:
|
||||||
|
:path: ".symlinks/plugins/saver_gallery/ios"
|
||||||
screen_brightness_ios:
|
screen_brightness_ios:
|
||||||
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
:path: ".symlinks/plugins/screen_brightness_ios/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
sqflite:
|
sqflite:
|
||||||
:path: ".symlinks/plugins/sqflite/ios"
|
: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:
|
url_launcher_ios:
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
volume_controller:
|
volume_controller:
|
||||||
@ -111,12 +161,21 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
|
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
|
||||||
|
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
|
||||||
|
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
|
||||||
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
|
||||||
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
|
||||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||||
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
|
||||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||||
|
<<<<<<< HEAD
|
||||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||||
|
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
|
||||||
|
=======
|
||||||
|
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
|
||||||
|
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
|
||||||
|
>>>>>>> main
|
||||||
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
|
||||||
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
|
||||||
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
|
||||||
@ -124,15 +183,18 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
|
||||||
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
|
||||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||||
|
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
|
||||||
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
|
||||||
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
|
||||||
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
|
||||||
|
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
|
||||||
|
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
|
||||||
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
|
||||||
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
|
||||||
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
|
||||||
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
|
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
|
||||||
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
|
||||||
|
|
||||||
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
|
PODFILE CHECKSUM: fc8a34c4ba2e14d31df90bf03cf419a764f2778c
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
COCOAPODS: 1.12.1
|
||||||
|
|||||||
@ -140,6 +140,7 @@
|
|||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,
|
5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,
|
||||||
|
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -268,6 +269,23 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
};
|
};
|
||||||
|
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputFileListPaths = (
|
||||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|||||||
@ -1,62 +1,115 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>PiliPala</string>
|
<string>PiliPala</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>pilipala</string>
|
<string>PiliPala</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>请允许APP保存图片到相册</string>
|
<string>请允许APP保存图片到相册</string>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>App需要您的同意,才能访问相册</string>
|
<string>App需要您的同意,才能访问相册</string>
|
||||||
<key>NSAppleMusicUsageDescription</key>
|
<key>NSAppleMusicUsageDescription</key>
|
||||||
<string>App需要您的同意,才能访问媒体资料库</string>
|
<string>App需要您的同意,才能访问媒体资料库</string>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>https</string>
|
<string>https</string>
|
||||||
<string>http</string>
|
<string>http</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
<!-- 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>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<!-- audio service配置 -->
|
||||||
|
=======
|
||||||
|
>>>>>>> main
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>audio</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -45,11 +45,6 @@ class VideoCardVSkeleton extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
color: Theme.of(context).colorScheme.onInverseSurface,
|
||||||
),
|
),
|
||||||
Container(
|
|
||||||
width: 80,
|
|
||||||
height: 12,
|
|
||||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
351
lib/common/widgets/app_expansion_panel_list.dart
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const double _kPanelHeaderCollapsedHeight = kMinInteractiveDimension;
|
||||||
|
|
||||||
|
class _SaltedKey<S, V> extends LocalKey {
|
||||||
|
const _SaltedKey(this.salt, this.value);
|
||||||
|
|
||||||
|
final S salt;
|
||||||
|
final V value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is _SaltedKey<S, V> &&
|
||||||
|
other.salt == salt &&
|
||||||
|
other.value == value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType, salt, value);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
final String saltString = S == String ? "<'$salt'>" : '<$salt>';
|
||||||
|
final String valueString = V == String ? "<'$value'>" : '<$value>';
|
||||||
|
return '[$saltString $valueString]';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppExpansionPanelList extends StatefulWidget {
|
||||||
|
/// Creates an expansion panel list widget. The [expansionCallback] is
|
||||||
|
/// triggered when an expansion panel expand/collapse button is pushed.
|
||||||
|
///
|
||||||
|
/// The [children] and [animationDuration] arguments must not be null.
|
||||||
|
const AppExpansionPanelList({
|
||||||
|
super.key,
|
||||||
|
required this.children,
|
||||||
|
this.expansionCallback,
|
||||||
|
this.animationDuration = kThemeAnimationDuration,
|
||||||
|
this.expandedHeaderPadding = EdgeInsets.zero,
|
||||||
|
this.dividerColor,
|
||||||
|
this.elevation = 2,
|
||||||
|
}) : _allowOnlyOnePanelOpen = false,
|
||||||
|
initialOpenPanelValue = null;
|
||||||
|
|
||||||
|
/// The children of the expansion panel list. They are laid out in a similar
|
||||||
|
/// fashion to [ListBody].
|
||||||
|
final List<AppExpansionPanel> children;
|
||||||
|
|
||||||
|
/// The callback that gets called whenever one of the expand/collapse buttons
|
||||||
|
/// is pressed. The arguments passed to the callback are the index of the
|
||||||
|
/// pressed panel and whether the panel is currently expanded or not.
|
||||||
|
///
|
||||||
|
/// If AppExpansionPanelList.radio is used, the callback may be called a
|
||||||
|
/// second time if a different panel was previously open. The arguments
|
||||||
|
/// passed to the second callback are the index of the panel that will close
|
||||||
|
/// and false, marking that it will be closed.
|
||||||
|
///
|
||||||
|
/// For AppExpansionPanelList, the callback needs to setState when it's notified
|
||||||
|
/// about the closing/opening panel. On the other hand, the callback for
|
||||||
|
/// AppExpansionPanelList.radio is simply meant to inform the parent widget of
|
||||||
|
/// changes, as the radio panels' open/close states are managed internally.
|
||||||
|
///
|
||||||
|
/// This callback is useful in order to keep track of the expanded/collapsed
|
||||||
|
/// panels in a parent widget that may need to react to these changes.
|
||||||
|
final ExpansionPanelCallback? expansionCallback;
|
||||||
|
|
||||||
|
/// The duration of the expansion animation.
|
||||||
|
final Duration animationDuration;
|
||||||
|
|
||||||
|
// Whether multiple panels can be open simultaneously
|
||||||
|
final bool _allowOnlyOnePanelOpen;
|
||||||
|
|
||||||
|
/// The value of the panel that initially begins open. (This value is
|
||||||
|
/// only used when initializing with the [AppExpansionPanelList.radio]
|
||||||
|
/// constructor.)
|
||||||
|
final Object? initialOpenPanelValue;
|
||||||
|
|
||||||
|
/// The padding that surrounds the panel header when expanded.
|
||||||
|
///
|
||||||
|
/// By default, 16px of space is added to the header vertically (above and below)
|
||||||
|
/// during expansion.
|
||||||
|
final EdgeInsets expandedHeaderPadding;
|
||||||
|
|
||||||
|
/// Defines color for the divider when [AppExpansionPanel.isExpanded] is false.
|
||||||
|
///
|
||||||
|
/// If `dividerColor` is null, then [DividerThemeData.color] is used. If that
|
||||||
|
/// is null, then [ThemeData.dividerColor] is used.
|
||||||
|
final Color? dividerColor;
|
||||||
|
|
||||||
|
/// Defines elevation for the [AppExpansionPanel] while it's expanded.
|
||||||
|
///
|
||||||
|
/// By default, the value of elevation is 2.
|
||||||
|
final double elevation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppExpansionPanelList> createState() => _AppExpansionPanelListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppExpansionPanelListState extends State<AppExpansionPanelList> {
|
||||||
|
ExpansionPanelRadio? _currentOpenPanel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget._allowOnlyOnePanelOpen) {
|
||||||
|
assert(_allIdentifiersUnique(),
|
||||||
|
'All ExpansionPanelRadio identifier values must be unique.');
|
||||||
|
if (widget.initialOpenPanelValue != null) {
|
||||||
|
_currentOpenPanel = searchPanelByValue(
|
||||||
|
widget.children.cast<ExpansionPanelRadio>(),
|
||||||
|
widget.initialOpenPanelValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(AppExpansionPanelList oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
|
||||||
|
if (widget._allowOnlyOnePanelOpen) {
|
||||||
|
assert(_allIdentifiersUnique(),
|
||||||
|
'All ExpansionPanelRadio identifier values must be unique.');
|
||||||
|
// If the previous widget was non-radio AppExpansionPanelList, initialize the
|
||||||
|
// open panel to widget.initialOpenPanelValue
|
||||||
|
if (!oldWidget._allowOnlyOnePanelOpen) {
|
||||||
|
_currentOpenPanel = searchPanelByValue(
|
||||||
|
widget.children.cast<ExpansionPanelRadio>(),
|
||||||
|
widget.initialOpenPanelValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_currentOpenPanel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _allIdentifiersUnique() {
|
||||||
|
final Map<Object, bool> identifierMap = <Object, bool>{};
|
||||||
|
for (final ExpansionPanelRadio child
|
||||||
|
in widget.children.cast<ExpansionPanelRadio>()) {
|
||||||
|
identifierMap[child.value] = true;
|
||||||
|
}
|
||||||
|
return identifierMap.length == widget.children.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isChildExpanded(int index) {
|
||||||
|
if (widget._allowOnlyOnePanelOpen) {
|
||||||
|
final ExpansionPanelRadio radioWidget =
|
||||||
|
widget.children[index] as ExpansionPanelRadio;
|
||||||
|
return _currentOpenPanel?.value == radioWidget.value;
|
||||||
|
}
|
||||||
|
return widget.children[index].isExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePressed(bool isExpanded, int index) {
|
||||||
|
widget.expansionCallback?.call(index, isExpanded);
|
||||||
|
|
||||||
|
if (widget._allowOnlyOnePanelOpen) {
|
||||||
|
final ExpansionPanelRadio pressedChild =
|
||||||
|
widget.children[index] as ExpansionPanelRadio;
|
||||||
|
|
||||||
|
// If another ExpansionPanelRadio was already open, apply its
|
||||||
|
// expansionCallback (if any) to false, because it's closing.
|
||||||
|
for (int childIndex = 0;
|
||||||
|
childIndex < widget.children.length;
|
||||||
|
childIndex += 1) {
|
||||||
|
final ExpansionPanelRadio child =
|
||||||
|
widget.children[childIndex] as ExpansionPanelRadio;
|
||||||
|
if (widget.expansionCallback != null &&
|
||||||
|
childIndex != index &&
|
||||||
|
child.value == _currentOpenPanel?.value) {
|
||||||
|
widget.expansionCallback!(childIndex, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_currentOpenPanel = isExpanded ? null : pressedChild;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpansionPanelRadio? searchPanelByValue(
|
||||||
|
List<ExpansionPanelRadio> panels, Object? value) {
|
||||||
|
for (final ExpansionPanelRadio panel in panels) {
|
||||||
|
if (panel.value == value) return panel;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
assert(
|
||||||
|
kElevationToShadow.containsKey(widget.elevation),
|
||||||
|
'Invalid value for elevation. See the kElevationToShadow constant for'
|
||||||
|
' possible elevation values.',
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<MergeableMaterialItem> items = <MergeableMaterialItem>[];
|
||||||
|
|
||||||
|
for (int index = 0; index < widget.children.length; index += 1) {
|
||||||
|
//todo: Uncomment to add gap between selected panels
|
||||||
|
/*if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
|
||||||
|
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));*/
|
||||||
|
|
||||||
|
final AppExpansionPanel child = widget.children[index];
|
||||||
|
final Widget headerWidget = child.headerBuilder(
|
||||||
|
context,
|
||||||
|
_isChildExpanded(index),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget? expandIconContainer = ExpandIcon(
|
||||||
|
isExpanded: _isChildExpanded(index),
|
||||||
|
onPressed: !child.canTapOnHeader
|
||||||
|
? (bool isExpanded) => _handlePressed(isExpanded, index)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
if (!child.canTapOnHeader) {
|
||||||
|
final MaterialLocalizations localizations =
|
||||||
|
MaterialLocalizations.of(context);
|
||||||
|
expandIconContainer = Semantics(
|
||||||
|
label: _isChildExpanded(index)
|
||||||
|
? localizations.expandedIconTapHint
|
||||||
|
: localizations.collapsedIconTapHint,
|
||||||
|
container: true,
|
||||||
|
child: expandIconContainer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final iconContainer = child.iconBuilder;
|
||||||
|
if (iconContainer != null) {
|
||||||
|
expandIconContainer = iconContainer(
|
||||||
|
expandIconContainer,
|
||||||
|
_isChildExpanded(index),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget header = Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: widget.animationDuration,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
margin: _isChildExpanded(index)
|
||||||
|
? widget.expandedHeaderPadding
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minHeight: _kPanelHeaderCollapsedHeight),
|
||||||
|
child: headerWidget,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (expandIconContainer != null) expandIconContainer,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (child.canTapOnHeader) {
|
||||||
|
header = MergeSemantics(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => _handlePressed(_isChildExpanded(index), index),
|
||||||
|
child: header,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
items.add(
|
||||||
|
MaterialSlice(
|
||||||
|
key: _SaltedKey<BuildContext, int>(context, index * 2),
|
||||||
|
color: child.backgroundColor,
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
header,
|
||||||
|
AnimatedCrossFade(
|
||||||
|
firstChild: Container(height: 0.0),
|
||||||
|
secondChild: child.body,
|
||||||
|
firstCurve:
|
||||||
|
const Interval(0.0, 0.6, curve: Curves.fastOutSlowIn),
|
||||||
|
secondCurve:
|
||||||
|
const Interval(0.4, 1.0, curve: Curves.fastOutSlowIn),
|
||||||
|
sizeCurve: Curves.fastOutSlowIn,
|
||||||
|
crossFadeState: _isChildExpanded(index)
|
||||||
|
? CrossFadeState.showSecond
|
||||||
|
: CrossFadeState.showFirst,
|
||||||
|
duration: widget.animationDuration,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_isChildExpanded(index) && index != widget.children.length - 1) {
|
||||||
|
items.add(MaterialGap(
|
||||||
|
key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MergeableMaterial(
|
||||||
|
hasDividers: true,
|
||||||
|
dividerColor: widget.dividerColor,
|
||||||
|
elevation: widget.elevation,
|
||||||
|
children: items,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef ExpansionPanelIconBuilder = Widget? Function(
|
||||||
|
Widget child,
|
||||||
|
bool isExpanded,
|
||||||
|
);
|
||||||
|
|
||||||
|
class AppExpansionPanel {
|
||||||
|
/// Creates an expansion panel to be used as a child for [ExpansionPanelList].
|
||||||
|
/// See [ExpansionPanelList] for an example on how to use this widget.
|
||||||
|
///
|
||||||
|
/// The [headerBuilder], [body], and [isExpanded] arguments must not be null.
|
||||||
|
AppExpansionPanel({
|
||||||
|
required this.headerBuilder,
|
||||||
|
required this.body,
|
||||||
|
this.iconBuilder,
|
||||||
|
this.isExpanded = false,
|
||||||
|
this.canTapOnHeader = false,
|
||||||
|
this.backgroundColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The widget builder that builds the expansion panels' header.
|
||||||
|
final ExpansionPanelHeaderBuilder headerBuilder;
|
||||||
|
|
||||||
|
/// The widget builder that builds the expansion panels' icon.
|
||||||
|
///
|
||||||
|
/// If not pass any function, then default icon will be displayed.
|
||||||
|
///
|
||||||
|
/// If builder function return null, then icon will not displayed.
|
||||||
|
final ExpansionPanelIconBuilder? iconBuilder;
|
||||||
|
|
||||||
|
/// The body of the expansion panel that's displayed below the header.
|
||||||
|
///
|
||||||
|
/// This widget is visible only when the panel is expanded.
|
||||||
|
final Widget body;
|
||||||
|
|
||||||
|
/// Whether the panel is expanded.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool isExpanded;
|
||||||
|
|
||||||
|
/// Whether tapping on the panel's header will expand/collapse it.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool canTapOnHeader;
|
||||||
|
|
||||||
|
/// Defines the background color of the panel.
|
||||||
|
///
|
||||||
|
/// Defaults to [ThemeData.cardColor].
|
||||||
|
final Color? backgroundColor;
|
||||||
|
}
|
||||||
96
lib/common/widgets/html_render.dart
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_html/flutter_html.dart';
|
||||||
|
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||||
|
|
||||||
|
// ignore: must_be_immutable
|
||||||
|
class HtmlRender extends StatelessWidget {
|
||||||
|
String? htmlContent;
|
||||||
|
final int? imgCount;
|
||||||
|
final List? imgList;
|
||||||
|
|
||||||
|
HtmlRender({
|
||||||
|
this.htmlContent,
|
||||||
|
this.imgCount,
|
||||||
|
this.imgList,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Html(
|
||||||
|
data: htmlContent,
|
||||||
|
onLinkTap: (url, buildContext, attributes) => {},
|
||||||
|
extensions: [
|
||||||
|
TagExtension(
|
||||||
|
tagsToExtend: {"img"},
|
||||||
|
builder: (extensionContext) {
|
||||||
|
try {
|
||||||
|
Map attributes = extensionContext.attributes;
|
||||||
|
List key = attributes.keys.toList();
|
||||||
|
String? imgUrl = key.contains('src')
|
||||||
|
? attributes['src']
|
||||||
|
: attributes['data-src'];
|
||||||
|
if (imgUrl!.startsWith('//')) {
|
||||||
|
imgUrl = 'https:$imgUrl';
|
||||||
|
}
|
||||||
|
if (imgUrl.startsWith('http://')) {
|
||||||
|
imgUrl = imgUrl.replaceAll('http://', 'https://');
|
||||||
|
}
|
||||||
|
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
|
||||||
|
bool isEmote = imgUrl.contains('/emote/');
|
||||||
|
bool isMall = imgUrl.contains('/mall/');
|
||||||
|
if (isMall) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
// bool inTable =
|
||||||
|
// extensionContext.element!.previousElementSibling == null ||
|
||||||
|
// extensionContext.element!.nextElementSibling == null;
|
||||||
|
// imgUrl = Utils().imageUrl(imgUrl!);
|
||||||
|
// return Image.network(
|
||||||
|
// imgUrl,
|
||||||
|
// width: isEmote ? 22 : null,
|
||||||
|
// height: isEmote ? 22 : null,
|
||||||
|
// );
|
||||||
|
return NetworkImgLayer(
|
||||||
|
width: isEmote ? 22 : Get.size.width - 24,
|
||||||
|
height: isEmote ? 22 : 200,
|
||||||
|
src: imgUrl,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
print(err);
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
style: {
|
||||||
|
"html": Style(
|
||||||
|
fontSize: FontSize.medium,
|
||||||
|
lineHeight: LineHeight.percent(140),
|
||||||
|
),
|
||||||
|
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
|
||||||
|
"a": Style(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
textDecoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
"p": Style(
|
||||||
|
margin: Margins.only(bottom: 10),
|
||||||
|
),
|
||||||
|
"span": Style(
|
||||||
|
fontSize: FontSize.medium,
|
||||||
|
height: Height(1.65),
|
||||||
|
),
|
||||||
|
"div": Style(height: Height.auto()),
|
||||||
|
"li > p": Style(
|
||||||
|
display: Display.inline,
|
||||||
|
),
|
||||||
|
"li": Style(
|
||||||
|
padding: HtmlPaddings.only(bottom: 4),
|
||||||
|
textAlign: TextAlign.justify,
|
||||||
|
),
|
||||||
|
"img": Style(margin: Margins.only(top: 4, bottom: 4)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -70,9 +70,17 @@ class NetworkImgLayer extends StatelessWidget {
|
|||||||
|
|
||||||
Widget placeholder(context) {
|
Widget placeholder(context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
|
|
||||||
width: width ?? double.infinity,
|
width: width ?? double.infinity,
|
||||||
height: height ?? 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: Center(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
type == 'avatar'
|
type == 'avatar'
|
||||||
|
|||||||
@ -58,8 +58,11 @@ class VideoCardH extends StatelessWidget {
|
|||||||
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
StyleString.safeSpace, 5, StyleString.safeSpace, 5),
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, boxConstraints) {
|
builder: (context, boxConstraints) {
|
||||||
double width =
|
double width = (boxConstraints.maxWidth -
|
||||||
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
|
StyleString.cardSpace *
|
||||||
|
6 /
|
||||||
|
MediaQuery.of(context).textScaleFactor) /
|
||||||
|
2;
|
||||||
return Container(
|
return Container(
|
||||||
constraints: const BoxConstraints(minHeight: 88),
|
constraints: const BoxConstraints(minHeight: 88),
|
||||||
height: width / StyleString.aspectRatio,
|
height: width / StyleString.aspectRatio,
|
||||||
@ -123,7 +126,7 @@ class VideoContent extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(10, 2, 6, 0),
|
padding: const EdgeInsets.fromLTRB(10, 0, 6, 0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -132,7 +135,6 @@ class VideoContent extends StatelessWidget {
|
|||||||
videoItem.title,
|
videoItem.title,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
@ -147,7 +149,6 @@ class VideoContent extends StatelessWidget {
|
|||||||
TextSpan(
|
TextSpan(
|
||||||
text: i['text'],
|
text: i['text'],
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
color: i['type'] == 'em'
|
color: i['type'] == 'em'
|
||||||
@ -177,7 +178,7 @@ class VideoContent extends StatelessWidget {
|
|||||||
// color: Theme.of(context).colorScheme.surfaceTint),
|
// color: Theme.of(context).colorScheme.surfaceTint),
|
||||||
// ),
|
// ),
|
||||||
// ),
|
// ),
|
||||||
const SizedBox(height: 4),
|
// const SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
|
|||||||
import 'package:pilipala/common/widgets/badge.dart';
|
import 'package:pilipala/common/widgets/badge.dart';
|
||||||
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
import 'package:pilipala/common/widgets/stat/danmu.dart';
|
||||||
import 'package:pilipala/common/widgets/stat/view.dart';
|
import 'package:pilipala/common/widgets/stat/view.dart';
|
||||||
|
import 'package:pilipala/http/dynamics.dart';
|
||||||
import 'package:pilipala/http/search.dart';
|
import 'package:pilipala/http/search.dart';
|
||||||
import 'package:pilipala/http/user.dart';
|
import 'package:pilipala/http/user.dart';
|
||||||
import 'package:pilipala/models/common/search_type.dart';
|
import 'package:pilipala/models/common/search_type.dart';
|
||||||
@ -15,16 +16,23 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
|
|||||||
// 视频卡片 - 垂直布局
|
// 视频卡片 - 垂直布局
|
||||||
class VideoCardV extends StatelessWidget {
|
class VideoCardV extends StatelessWidget {
|
||||||
final dynamic videoItem;
|
final dynamic videoItem;
|
||||||
|
final int crossAxisCount;
|
||||||
final Function()? longPress;
|
final Function()? longPress;
|
||||||
final Function()? longPressEnd;
|
final Function()? longPressEnd;
|
||||||
|
|
||||||
const VideoCardV({
|
const VideoCardV({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.videoItem,
|
required this.videoItem,
|
||||||
|
required this.crossAxisCount,
|
||||||
this.longPress,
|
this.longPress,
|
||||||
this.longPressEnd,
|
this.longPressEnd,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
bool isStringNumeric(String str) {
|
||||||
|
RegExp numericRegex = RegExp(r'^\d+$');
|
||||||
|
return numericRegex.hasMatch(str);
|
||||||
|
}
|
||||||
|
|
||||||
void onPushDetail(heroTag) async {
|
void onPushDetail(heroTag) async {
|
||||||
String goto = videoItem.goto;
|
String goto = videoItem.goto;
|
||||||
switch (goto) {
|
switch (goto) {
|
||||||
@ -60,6 +68,47 @@ class VideoCardV extends StatelessWidget {
|
|||||||
'heroTag': heroTag,
|
'heroTag': heroTag,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
// 动态
|
||||||
|
case 'picture':
|
||||||
|
try {
|
||||||
|
String dynamicType = 'picture';
|
||||||
|
String uri = videoItem.uri;
|
||||||
|
String id = '';
|
||||||
|
if (videoItem.uri.startsWith('bilibili://article/')) {
|
||||||
|
// https://www.bilibili.com/read/cv27063554
|
||||||
|
dynamicType = 'read';
|
||||||
|
RegExp regex = RegExp(r'\d+');
|
||||||
|
Match match = regex.firstMatch(videoItem.uri)!;
|
||||||
|
String matchedNumber = match.group(0)!;
|
||||||
|
videoItem.param = int.parse(matchedNumber);
|
||||||
|
id = 'cv${videoItem.param}';
|
||||||
|
}
|
||||||
|
if (uri.startsWith('http')) {
|
||||||
|
String path = Uri.parse(uri).path;
|
||||||
|
if (isStringNumeric(path.split('/')[1])) {
|
||||||
|
// 请求接口
|
||||||
|
var res =
|
||||||
|
await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
|
||||||
|
if (res['status']) {
|
||||||
|
Get.toNamed('/dynamicDetail', arguments: {
|
||||||
|
'item': res['data'],
|
||||||
|
'floor': 1,
|
||||||
|
'action': 'detail'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Get.toNamed('/htmlRender', parameters: {
|
||||||
|
'url': uri,
|
||||||
|
'title': videoItem.title,
|
||||||
|
'id': id,
|
||||||
|
'dynamicType': dynamicType
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
SmartDialog.showToast(err.toString());
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
SmartDialog.showToast(videoItem.goto);
|
SmartDialog.showToast(videoItem.goto);
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
@ -79,9 +128,6 @@ class VideoCardV extends StatelessWidget {
|
|||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: StyleString.mdRadius,
|
|
||||||
),
|
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
@ -103,17 +149,37 @@ class VideoCardV extends StatelessWidget {
|
|||||||
child: LayoutBuilder(builder: (context, boxConstraints) {
|
child: LayoutBuilder(builder: (context, boxConstraints) {
|
||||||
double maxWidth = boxConstraints.maxWidth;
|
double maxWidth = boxConstraints.maxWidth;
|
||||||
double maxHeight = boxConstraints.maxHeight;
|
double maxHeight = boxConstraints.maxHeight;
|
||||||
return Hero(
|
return Stack(
|
||||||
tag: heroTag,
|
children: [
|
||||||
child: NetworkImgLayer(
|
Hero(
|
||||||
src: videoItem.pic,
|
tag: heroTag,
|
||||||
width: maxWidth,
|
child: NetworkImgLayer(
|
||||||
height: maxHeight,
|
src: videoItem.pic,
|
||||||
),
|
width: maxWidth,
|
||||||
|
height: maxHeight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -124,23 +190,53 @@ class VideoCardV extends StatelessWidget {
|
|||||||
|
|
||||||
class VideoContent extends StatelessWidget {
|
class VideoContent extends StatelessWidget {
|
||||||
final dynamic videoItem;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Expanded(
|
return Expanded(
|
||||||
|
flex: crossAxisCount == 1 ? 0 : 1,
|
||||||
child: Padding(
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
videoItem.title,
|
children: [
|
||||||
style: const TextStyle(fontSize: 13),
|
Expanded(
|
||||||
maxLines: 2,
|
child: Text(
|
||||||
overflow: TextOverflow.ellipsis,
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
if (videoItem.goto == 'bangumi') ...[
|
if (videoItem.goto == 'bangumi') ...[
|
||||||
@ -171,6 +267,7 @@ class VideoContent extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
|
flex: crossAxisCount == 1 ? 0 : 1,
|
||||||
child: Text(
|
child: Text(
|
||||||
videoItem.owner.name,
|
videoItem.owner.name,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
@ -181,95 +278,36 @@ class VideoContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (videoItem.goto == 'av')
|
if (crossAxisCount == 1) ...[
|
||||||
SizedBox(
|
Text(
|
||||||
width: 24,
|
' • ',
|
||||||
height: 24,
|
style: TextStyle(
|
||||||
child: PopupMenuButton<String>(
|
fontSize:
|
||||||
padding: EdgeInsets.zero,
|
Theme.of(context).textTheme.labelMedium!.fontSize,
|
||||||
tooltip: '稍后再看',
|
color: Theme.of(context).colorScheme.outline,
|
||||||
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))
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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))
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -278,53 +316,72 @@ class VideoContent extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class VideoStat extends StatelessWidget {
|
class VideoStat extends StatelessWidget {
|
||||||
final int? view;
|
final dynamic videoItem;
|
||||||
final int? danmaku;
|
|
||||||
final int? duration;
|
|
||||||
|
|
||||||
const VideoStat(
|
const VideoStat({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.view,
|
required this.videoItem,
|
||||||
required this.danmaku,
|
}) : super(key: key);
|
||||||
required this.duration})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return RichText(
|
||||||
height: 48,
|
maxLines: 1,
|
||||||
padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
|
text: TextSpan(
|
||||||
decoration: const BoxDecoration(
|
style: TextStyle(
|
||||||
gradient: LinearGradient(
|
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
|
||||||
begin: Alignment.topCenter,
|
color: Theme.of(context).colorScheme.outline,
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: <Color>[
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.black54,
|
|
||||||
],
|
|
||||||
tileMode: TileMode.mirror,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
children: [
|
||||||
Row(
|
TextSpan(text: '${videoItem.stat.view}观看'),
|
||||||
children: [
|
TextSpan(text: ' • ${videoItem.stat.danmu}弹幕'),
|
||||||
StatView(
|
],
|
||||||
theme: 'white',
|
),
|
||||||
view: view,
|
);
|
||||||
),
|
}
|
||||||
const SizedBox(width: 6),
|
}
|
||||||
StatDanMu(
|
|
||||||
theme: 'white',
|
class WatchLater extends StatelessWidget {
|
||||||
danmu: danmaku,
|
final double? size;
|
||||||
),
|
final double? iconSize;
|
||||||
],
|
final Function? callFn;
|
||||||
),
|
|
||||||
Text(
|
const WatchLater({
|
||||||
Utils.timeFormat(duration!),
|
Key? key,
|
||||||
style: const TextStyle(fontSize: 11, color: Colors.white),
|
required this.size,
|
||||||
)
|
required this.iconSize,
|
||||||
|
this.callFn,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: PopupMenuButton<String>(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
tooltip: '稍后再看',
|
||||||
|
icon: Icon(
|
||||||
|
Icons.more_vert_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
position: PopupMenuPosition.under,
|
||||||
|
// constraints: const BoxConstraints(maxHeight: 35),
|
||||||
|
onSelected: (String type) {},
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
|
||||||
|
PopupMenuItem<String>(
|
||||||
|
onTap: () => callFn!(),
|
||||||
|
value: 'pause',
|
||||||
|
height: 35,
|
||||||
|
child: const Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.watch_later_outlined, size: 16),
|
||||||
|
SizedBox(width: 6),
|
||||||
|
Text('稍后再看', style: TextStyle(fontSize: 13))
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -97,6 +97,9 @@ class Api {
|
|||||||
// 操作用户关系
|
// 操作用户关系
|
||||||
static const String relationMod = '/x/relation/modify';
|
static const String relationMod = '/x/relation/modify';
|
||||||
|
|
||||||
|
// 相互关系查询
|
||||||
|
static const String relationSearch = '/x/space/wbi/acc/relation';
|
||||||
|
|
||||||
// 评论列表
|
// 评论列表
|
||||||
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
|
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
|
||||||
static const String replyList = '/x/v2/reply';
|
static const String replyList = '/x/v2/reply';
|
||||||
@ -126,12 +129,14 @@ class Api {
|
|||||||
static const String userFavFolder = '/x/v3/fav/folder/created/list';
|
static const String userFavFolder = '/x/v3/fav/folder/created/list';
|
||||||
|
|
||||||
/// 收藏夹 详情
|
/// 收藏夹 详情
|
||||||
/// media_id int 收藏夹id
|
/// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
|
||||||
/// pn int 当前页
|
/// pn int 当前页
|
||||||
/// ps int pageSize
|
/// ps int pageSize
|
||||||
/// keyword String 搜索词
|
/// keyword String 搜索词
|
||||||
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
|
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
|
||||||
/// tid int 分区id
|
/// tid int 分区id
|
||||||
|
/// platform web
|
||||||
|
/// type 0 当前收藏夹 1 全部收藏夹
|
||||||
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
|
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
|
||||||
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
|
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
|
||||||
|
|
||||||
@ -164,6 +169,12 @@ class Api {
|
|||||||
// 清空历史记录
|
// 清空历史记录
|
||||||
static const String clearHistory = '/x/v2/history/clear';
|
static const String clearHistory = '/x/v2/history/clear';
|
||||||
|
|
||||||
|
// 删除某条历史记录
|
||||||
|
static const String delHistory = '/x/v2/history/delete';
|
||||||
|
|
||||||
|
// 搜索历史记录
|
||||||
|
static const String searchHistory = '/x/web-goblin/history/search';
|
||||||
|
|
||||||
// 热搜
|
// 热搜
|
||||||
static const String hotSearchList =
|
static const String hotSearchList =
|
||||||
'https://s.search.bilibili.com/main/hotword';
|
'https://s.search.bilibili.com/main/hotword';
|
||||||
@ -239,6 +250,9 @@ class Api {
|
|||||||
// wts=1689767832
|
// wts=1689767832
|
||||||
static const String memberArchive = '/x/space/wbi/arc/search';
|
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';
|
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
|
||||||
|
|
||||||
@ -285,7 +299,77 @@ class Api {
|
|||||||
// 黑名单
|
// 黑名单
|
||||||
static const String blackLst = '/x/relation/blacks';
|
static const String blackLst = '/x/relation/blacks';
|
||||||
|
|
||||||
|
// 移除黑名单
|
||||||
|
static const String removeBlack = '/x/relation/modify';
|
||||||
|
|
||||||
// github 获取最新版
|
// github 获取最新版
|
||||||
static const String latestApp =
|
static const String latestApp =
|
||||||
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
|
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
|
||||||
|
|
||||||
|
// 多少人在看
|
||||||
|
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
|
||||||
|
static const String onlineTotal = '/x/player/online/total';
|
||||||
|
|
||||||
|
static const String webDanmaku = '/x/v2/dm/web/seg.so';
|
||||||
|
|
||||||
|
// up主分组
|
||||||
|
static const String followUpTag = '/x/relation/tags';
|
||||||
|
|
||||||
|
// 设置Up主分组
|
||||||
|
// 0 添加至默认分组 否则使用,分割tagid
|
||||||
|
static const String addUsers = '/x/relation/tags/addUsers';
|
||||||
|
|
||||||
|
// 获取指定分组下的up
|
||||||
|
static const String followUpGroup = '/x/relation/tag';
|
||||||
|
|
||||||
|
// 获取某个动态详情
|
||||||
|
// timezone_offset=-480
|
||||||
|
// id=849312409672744983
|
||||||
|
// features=itemOpusStyle
|
||||||
|
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
|
||||||
|
|
||||||
|
// AI总结
|
||||||
|
/// https://api.bilibili.com/x/web-interface/view/conclusion/get?
|
||||||
|
/// bvid=BV1ju4y1s7kn&
|
||||||
|
/// cid=1296086601&
|
||||||
|
/// up_mid=4641697&
|
||||||
|
/// w_rid=1607c6c5a4a35a1297e31992220900ae&
|
||||||
|
/// wts=1697033079
|
||||||
|
static const String aiConclusion = '/x/web-interface/view/conclusion/get';
|
||||||
|
|
||||||
|
// captcha验证码
|
||||||
|
static const String getCaptcha =
|
||||||
|
'https://passport.bilibili.com/x/passport-login/captcha?source=main_web';
|
||||||
|
|
||||||
|
// web端短信验证码
|
||||||
|
static const String smsCode =
|
||||||
|
'https://passport.bilibili.com/x/passport-login/web/sms/send';
|
||||||
|
|
||||||
|
// web端验证码登录
|
||||||
|
|
||||||
|
// web端密码登录
|
||||||
|
|
||||||
|
// app端短信验证码
|
||||||
|
static const String appSmsCode =
|
||||||
|
'https://passport.bilibili.com/x/passport-login/sms/send';
|
||||||
|
|
||||||
|
// app端验证码登录
|
||||||
|
|
||||||
|
// 获取短信验证码
|
||||||
|
// static const String appSafeSmsCode =
|
||||||
|
// 'https://passport.bilibili.com/x/safecenter/common/sms/send';
|
||||||
|
|
||||||
|
/// app端密码登录
|
||||||
|
/// username
|
||||||
|
/// password
|
||||||
|
/// key
|
||||||
|
/// rhash
|
||||||
|
static const String loginInByPwdApi =
|
||||||
|
'https://passport.bilibili.com/x/passport-login/oauth2/login';
|
||||||
|
|
||||||
|
/// 密码加密密钥
|
||||||
|
/// disable_rcmd
|
||||||
|
/// local_id
|
||||||
|
static const getWebKey =
|
||||||
|
'https://passport.bilibili.com/x/passport-login/web/key';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,4 +23,31 @@ class BlackHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移除黑名单
|
||||||
|
static Future removeBlack({required int fid}) async {
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.removeBlack,
|
||||||
|
queryParameters: {
|
||||||
|
'act': 6,
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
'fid': fid,
|
||||||
|
'jsonp': 'jsonp',
|
||||||
|
're_src': 116,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': [],
|
||||||
|
'msg': '操作成功',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,4 +2,37 @@ class HttpString {
|
|||||||
static const String baseUrl = 'https://www.bilibili.com';
|
static const String baseUrl = 'https://www.bilibili.com';
|
||||||
static const String baseApiUrl = 'https://api.bilibili.com';
|
static const String baseApiUrl = 'https://api.bilibili.com';
|
||||||
static const String tUrl = 'https://api.vc.bilibili.com';
|
static const String tUrl = 'https://api.vc.bilibili.com';
|
||||||
|
static const List<int> validateStatusCodes = [
|
||||||
|
302,
|
||||||
|
304,
|
||||||
|
307,
|
||||||
|
400,
|
||||||
|
401,
|
||||||
|
403,
|
||||||
|
404,
|
||||||
|
405,
|
||||||
|
409,
|
||||||
|
412,
|
||||||
|
500,
|
||||||
|
503,
|
||||||
|
504,
|
||||||
|
509,
|
||||||
|
616,
|
||||||
|
617,
|
||||||
|
625,
|
||||||
|
626,
|
||||||
|
628,
|
||||||
|
629,
|
||||||
|
632,
|
||||||
|
643,
|
||||||
|
650,
|
||||||
|
652,
|
||||||
|
658,
|
||||||
|
662,
|
||||||
|
688,
|
||||||
|
689,
|
||||||
|
701,
|
||||||
|
799,
|
||||||
|
8888
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
27
lib/http/danmaku.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:pilipala/http/index.dart';
|
||||||
|
import 'package:pilipala/models/danmaku/dm.pb.dart';
|
||||||
|
|
||||||
|
import 'constants.dart';
|
||||||
|
|
||||||
|
class DanmakaHttp {
|
||||||
|
// 获取视频弹幕
|
||||||
|
static Future queryDanmaku({
|
||||||
|
required int cid,
|
||||||
|
required int segmentIndex,
|
||||||
|
}) async {
|
||||||
|
// 构建参数对象
|
||||||
|
Map<String, int> params = {
|
||||||
|
'type': 1,
|
||||||
|
'oid': cid,
|
||||||
|
'segment_index': segmentIndex,
|
||||||
|
};
|
||||||
|
var response = await Request().get(
|
||||||
|
Api.webDanmaku,
|
||||||
|
data: params,
|
||||||
|
extra: {'resType': ResponseType.bytes},
|
||||||
|
);
|
||||||
|
return DmSegMobileReply.fromBuffer(response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,10 +22,19 @@ class DynamicsHttp {
|
|||||||
}
|
}
|
||||||
var res = await Request().get(Api.followDynamic, data: data);
|
var res = await Request().get(Api.followDynamic, data: data);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return {
|
try {
|
||||||
'status': true,
|
return {
|
||||||
'data': DynamicsDataModel.fromJson(res.data['data']),
|
'status': true,
|
||||||
};
|
'data': DynamicsDataModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
print(err);
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': err.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'status': false,
|
'status': false,
|
||||||
@ -77,4 +86,35 @@ class DynamicsHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
static Future dynamicDetail({
|
||||||
|
String? id,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().get(Api.dynamicDetail, data: {
|
||||||
|
'timezone_offset': -480,
|
||||||
|
'id': id,
|
||||||
|
'features': 'itemOpusStyle',
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': DynamicItemModel.fromJson(res.data['data']['item']),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': err.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
lib/http/html.dart
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import 'package:html/dom.dart';
|
||||||
|
import 'package:html/parser.dart';
|
||||||
|
import 'package:pilipala/http/index.dart';
|
||||||
|
|
||||||
|
class HtmlHttp {
|
||||||
|
// article
|
||||||
|
static Future reqHtml(id, dynamicType) async {
|
||||||
|
var response = await Request().get(
|
||||||
|
"https://www.bilibili.com/opus/$id",
|
||||||
|
extra: {'ua': 'pc'},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.contains('Redirecting to')) {
|
||||||
|
RegExp regex = RegExp(r'//([\w\.]+)/(\w+)/(\w+)');
|
||||||
|
Match match = regex.firstMatch(response.data)!;
|
||||||
|
String matchedString = match.group(0)!;
|
||||||
|
response = await Request().get(
|
||||||
|
'https:$matchedString' + '/',
|
||||||
|
extra: {'ua': 'pc'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Document rootTree = parse(response.data);
|
||||||
|
// log(response.data.body.toString());
|
||||||
|
Element body = rootTree.body!;
|
||||||
|
Element appDom = body.querySelector('#app')!;
|
||||||
|
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
|
||||||
|
// 头像
|
||||||
|
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
|
||||||
|
avatar = 'https:${avatar.split('@')[0]}';
|
||||||
|
String uname = authorHeader
|
||||||
|
.querySelector('.fixed-author-header__author__name')!
|
||||||
|
.text;
|
||||||
|
|
||||||
|
// 动态详情
|
||||||
|
Element opusDetail = appDom.querySelector('.opus-detail')!;
|
||||||
|
// 发布时间
|
||||||
|
String updateTime =
|
||||||
|
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
|
||||||
|
//
|
||||||
|
String opusContent =
|
||||||
|
opusDetail.querySelector('.opus-module-content')!.innerHtml;
|
||||||
|
String test = opusDetail
|
||||||
|
.querySelector('.horizontal-scroll-album__pic__img')!
|
||||||
|
.innerHtml;
|
||||||
|
String commentId = opusDetail
|
||||||
|
.querySelector('.bili-comment-container')!
|
||||||
|
.className
|
||||||
|
.split(' ')[1]
|
||||||
|
.split('-')[2];
|
||||||
|
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'avatar': avatar,
|
||||||
|
'uname': uname,
|
||||||
|
'updateTime': updateTime,
|
||||||
|
'content': test + opusContent,
|
||||||
|
'commentId': int.parse(commentId)
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
print('err: $err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read
|
||||||
|
static Future reqReadHtml(id, dynamicType) async {
|
||||||
|
var response = await Request().get(
|
||||||
|
"https://www.bilibili.com/$dynamicType/$id/",
|
||||||
|
extra: {'ua': 'pc'},
|
||||||
|
);
|
||||||
|
Document rootTree = parse(response.data);
|
||||||
|
Element body = rootTree.body!;
|
||||||
|
Element appDom = body.querySelector('#app')!;
|
||||||
|
Element authorHeader = appDom.querySelector('.up-left')!;
|
||||||
|
// 头像
|
||||||
|
// String avatar =
|
||||||
|
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
|
||||||
|
// print(avatar);
|
||||||
|
// avatar = 'https:${avatar.split('@')[0]}';
|
||||||
|
String uname = authorHeader.querySelector('.up-name')!.text.trim();
|
||||||
|
// 动态详情
|
||||||
|
Element opusDetail = appDom.querySelector('.article-content')!;
|
||||||
|
// 发布时间
|
||||||
|
// String updateTime =
|
||||||
|
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
|
||||||
|
// print(updateTime);
|
||||||
|
|
||||||
|
//
|
||||||
|
String opusContent =
|
||||||
|
opusDetail.querySelector('#read-article-holder')!.innerHtml;
|
||||||
|
RegExp digitRegExp = RegExp(r'\d+');
|
||||||
|
Iterable<Match> matches = digitRegExp.allMatches(id);
|
||||||
|
String number = matches.first.group(0)!;
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'avatar': '',
|
||||||
|
'uname': uname,
|
||||||
|
'updateTime': '',
|
||||||
|
'content': opusContent,
|
||||||
|
'commentId': int.parse(number)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,12 +4,13 @@ import 'dart:io';
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:cookie_jar/cookie_jar.dart';
|
import 'package:cookie_jar/cookie_jar.dart';
|
||||||
|
import 'package:dio/io.dart';
|
||||||
|
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
import 'package:pilipala/http/constants.dart';
|
import 'package:pilipala/http/constants.dart';
|
||||||
import 'package:pilipala/http/interceptor.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';
|
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||||
|
|
||||||
class Request {
|
class Request {
|
||||||
@ -17,6 +18,11 @@ class Request {
|
|||||||
static late CookieManager cookieManager;
|
static late CookieManager cookieManager;
|
||||||
static late final Dio dio;
|
static late final Dio dio;
|
||||||
factory Request() => _instance;
|
factory Request() => _instance;
|
||||||
|
Box setting = GStrorage.setting;
|
||||||
|
static Box localCache = GStrorage.localCache;
|
||||||
|
late dynamic enableSystemProxy;
|
||||||
|
late String systemProxyHost;
|
||||||
|
late String systemProxyPort;
|
||||||
|
|
||||||
/// 设置cookie
|
/// 设置cookie
|
||||||
static setCookie() async {
|
static setCookie() async {
|
||||||
@ -42,6 +48,7 @@ class Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
|
||||||
|
|
||||||
if (cookie.isEmpty) {
|
if (cookie.isEmpty) {
|
||||||
try {
|
try {
|
||||||
@ -59,9 +66,6 @@ class Request {
|
|||||||
static Future<String> getCsrf() async {
|
static Future<String> getCsrf() async {
|
||||||
var cookies = await cookieManager.cookieJar
|
var cookies = await cookieManager.cookieJar
|
||||||
.loadForRequest(Uri.parse(HttpString.baseApiUrl));
|
.loadForRequest(Uri.parse(HttpString.baseApiUrl));
|
||||||
// for (var i in cookies) {
|
|
||||||
// print(i);
|
|
||||||
// }
|
|
||||||
String token = '';
|
String token = '';
|
||||||
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
|
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
|
||||||
token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
|
token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
|
||||||
@ -69,6 +73,17 @@ class Request {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static setOptionsHeaders(userInfo, status) {
|
||||||
|
if (status) {
|
||||||
|
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
|
* config it and create
|
||||||
*/
|
*/
|
||||||
@ -82,29 +97,47 @@ class Request {
|
|||||||
//响应流上前后两次接受到数据的间隔,单位为毫秒。
|
//响应流上前后两次接受到数据的间隔,单位为毫秒。
|
||||||
receiveTimeout: const Duration(milliseconds: 12000),
|
receiveTimeout: const Duration(milliseconds: 12000),
|
||||||
//Http请求头.
|
//Http请求头.
|
||||||
headers: {
|
headers: {},
|
||||||
// 'cookie': '',
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Box userInfoCache = GStrorage.userInfo;
|
enableSystemProxy =
|
||||||
var userInfo = userInfoCache.get('userInfoCache');
|
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
|
||||||
if (userInfo != null && userInfo.mid != null) {
|
systemProxyHost =
|
||||||
options.headers['x-bili-mid'] = userInfo.mid.toString();
|
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
|
||||||
options.headers['env'] = 'prod';
|
systemProxyPort =
|
||||||
options.headers['app-key'] = 'android64';
|
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
|
||||||
options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
|
|
||||||
options.headers['x-bili-aurora-zone'] = 'sh001';
|
|
||||||
options.headers['referer'] = 'https://www.bilibili.com/';
|
|
||||||
}
|
|
||||||
|
|
||||||
dio = Dio(options)
|
dio = Dio(options)
|
||||||
|
|
||||||
|
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
|
||||||
..httpClientAdapter = Http2Adapter(
|
..httpClientAdapter = Http2Adapter(
|
||||||
ConnectionManager(
|
ConnectionManager(
|
||||||
idleTimeout: const Duration(milliseconds: 10000),
|
idleTimeout: const Duration(milliseconds: 10000),
|
||||||
// Ignore bad certificate
|
|
||||||
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
|
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
/// 设置代理
|
||||||
|
..httpClientAdapter = IOHttpClientAdapter(
|
||||||
|
createHttpClient: () {
|
||||||
|
final client = HttpClient();
|
||||||
|
// Config the client.
|
||||||
|
client.findProxy = (uri) {
|
||||||
|
if (enableSystemProxy) {
|
||||||
|
print('🌹:$systemProxyHost');
|
||||||
|
print('🌹:$systemProxyPort');
|
||||||
|
|
||||||
|
// return 'PROXY host:port';
|
||||||
|
return 'PROXY $systemProxyHost:$systemProxyPort';
|
||||||
|
} else {
|
||||||
|
// 不设置代理
|
||||||
|
return 'DIRECT';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
client.badCertificateCallback =
|
||||||
|
(X509Certificate cert, String host, int port) => true;
|
||||||
|
return client;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
//添加拦截器
|
//添加拦截器
|
||||||
@ -119,30 +152,26 @@ class Request {
|
|||||||
|
|
||||||
dio.transformer = BackgroundTransformer();
|
dio.transformer = BackgroundTransformer();
|
||||||
dio.options.validateStatus = (status) {
|
dio.options.validateStatus = (status) {
|
||||||
return status! >= 200 && status < 300 || status == 304 || status == 302;
|
return status! >= 200 && status < 300 ||
|
||||||
|
HttpString.validateStatusCodes.contains(status);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* get请求
|
* get请求
|
||||||
*/
|
*/
|
||||||
get(url, {data, cacheOptions, options, cancelToken, extra}) async {
|
get(url, {data, options, cancelToken, extra}) async {
|
||||||
Response response;
|
Response response;
|
||||||
Options options;
|
Options options = Options();
|
||||||
String ua = 'pc';
|
|
||||||
ResponseType resType = ResponseType.json;
|
ResponseType resType = ResponseType.json;
|
||||||
if (extra != null) {
|
if (extra != null) {
|
||||||
ua = extra!['ua'] ?? 'pc';
|
|
||||||
resType = extra!['resType'] ?? ResponseType.json;
|
resType = extra!['resType'] ?? ResponseType.json;
|
||||||
|
if (extra['ua'] != null) {
|
||||||
|
options.headers = {'user-agent': headerUa(type: extra['ua'])};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (cacheOptions != null) {
|
options.responseType = resType;
|
||||||
cacheOptions.headers = {'user-agent': headerUa(ua)};
|
|
||||||
options = cacheOptions;
|
|
||||||
} else {
|
|
||||||
options = Options();
|
|
||||||
options.headers = {'user-agent': headerUa(ua)};
|
|
||||||
options.responseType = resType;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
response = await dio.get(
|
response = await dio.get(
|
||||||
url,
|
url,
|
||||||
@ -209,15 +238,19 @@ class Request {
|
|||||||
token.cancel("cancelled");
|
token.cancel("cancelled");
|
||||||
}
|
}
|
||||||
|
|
||||||
String headerUa(ua) {
|
String headerUa({type = 'mob'}) {
|
||||||
String headerUa = '';
|
String headerUa = '';
|
||||||
if (ua == 'mob') {
|
if (type == 'mob') {
|
||||||
headerUa = Platform.isIOS
|
if (Platform.isIOS) {
|
||||||
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
|
headerUa =
|
||||||
: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1';
|
||||||
|
} else {
|
||||||
|
headerUa =
|
||||||
|
'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
headerUa =
|
headerUa =
|
||||||
'Mozilla/5.0 (MaciMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
|
||||||
}
|
}
|
||||||
return headerUa;
|
return headerUa;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,8 +17,6 @@ class ApiInterceptor extends Interceptor {
|
|||||||
handler.next(options);
|
handler.next(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
Box localCache = GStrorage.localCache;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||||
try {
|
try {
|
||||||
@ -29,8 +27,11 @@ class ApiInterceptor extends Interceptor {
|
|||||||
final uri = Uri.parse(locations.first);
|
final uri = Uri.parse(locations.first);
|
||||||
final accessKey = uri.queryParameters['access_key'];
|
final accessKey = uri.queryParameters['access_key'];
|
||||||
final mid = uri.queryParameters['mid'];
|
final mid = uri.queryParameters['mid'];
|
||||||
localCache
|
try {
|
||||||
.put(LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
|
Box localCache = GStrorage.localCache;
|
||||||
|
localCache.put(
|
||||||
|
LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,7 +46,10 @@ class ApiInterceptor extends Interceptor {
|
|||||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||||
// 处理网络请求错误
|
// 处理网络请求错误
|
||||||
// handler.next(err);
|
// handler.next(err);
|
||||||
SmartDialog.showToast(await dioError(err));
|
SmartDialog.showToast(
|
||||||
|
await dioError(err),
|
||||||
|
displayType: SmartToastType.onlyRefresh,
|
||||||
|
);
|
||||||
super.onError(err, handler);
|
super.onError(err, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
177
lib/http/login.dart
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:encrypt/encrypt.dart';
|
||||||
|
import 'package:pilipala/http/index.dart';
|
||||||
|
import 'package:pilipala/models/login/index.dart';
|
||||||
|
import 'package:pilipala/utils/login.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class LoginHttp {
|
||||||
|
static Future queryCaptcha() async {
|
||||||
|
var res = await Request().get(Api.getCaptcha);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': CaptchaDataModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'data': res.message};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future sendSmsCode({
|
||||||
|
int? cid,
|
||||||
|
required int tel,
|
||||||
|
required String token,
|
||||||
|
required String challenge,
|
||||||
|
required String validate,
|
||||||
|
required String seccode,
|
||||||
|
}) async {
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.appSmsCode,
|
||||||
|
data: {
|
||||||
|
'cid': cid,
|
||||||
|
'tel': tel,
|
||||||
|
"source": "main_web",
|
||||||
|
'token': token,
|
||||||
|
'challenge': challenge,
|
||||||
|
'validate': validate,
|
||||||
|
'seccode': seccode,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
// headers: {'user-agent': ApiConstants.userAgent}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// web端验证码
|
||||||
|
static Future sendWebSmsCode({
|
||||||
|
int? cid,
|
||||||
|
required int tel,
|
||||||
|
required String token,
|
||||||
|
required String challenge,
|
||||||
|
required String validate,
|
||||||
|
required String seccode,
|
||||||
|
}) async {
|
||||||
|
Map data = {
|
||||||
|
'cid': cid,
|
||||||
|
'tel': tel,
|
||||||
|
'token': token,
|
||||||
|
'challenge': challenge,
|
||||||
|
'validate': validate,
|
||||||
|
'seccode': seccode,
|
||||||
|
};
|
||||||
|
FormData formData = FormData.fromMap({...data});
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.smsCode,
|
||||||
|
data: formData,
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// web端验证码登录
|
||||||
|
static Future loginInByWebSmsCode() async {}
|
||||||
|
|
||||||
|
// web端密码登录
|
||||||
|
static Future liginInByWebPwd() async {}
|
||||||
|
|
||||||
|
// app端验证码
|
||||||
|
static Future sendAppSmsCode({
|
||||||
|
int? cid,
|
||||||
|
required int tel,
|
||||||
|
required String token,
|
||||||
|
required String challenge,
|
||||||
|
required String validate,
|
||||||
|
required String seccode,
|
||||||
|
}) async {
|
||||||
|
Map<String, dynamic> data = {
|
||||||
|
'cid': cid,
|
||||||
|
'tel': tel,
|
||||||
|
'login_session_id': const Uuid().v4().replaceAll('-', ''),
|
||||||
|
'recaptcha_token': token,
|
||||||
|
'gee_challenge': challenge,
|
||||||
|
'gee_validate': validate,
|
||||||
|
'gee_seccode': seccode,
|
||||||
|
'channel': 'bili',
|
||||||
|
'buvid': buvid(),
|
||||||
|
'local_id': buvid(),
|
||||||
|
// 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
'statistics': {
|
||||||
|
"appId": 1,
|
||||||
|
"platform": 3,
|
||||||
|
"version": "7.52.0",
|
||||||
|
"abtest": ""
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// FormData formData = FormData.fromMap({...data});
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.appSmsCode,
|
||||||
|
data: data,
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String buvid() {
|
||||||
|
var mac = <String>[];
|
||||||
|
var random = Random();
|
||||||
|
|
||||||
|
for (var i = 0; i < 6; i++) {
|
||||||
|
var min = 0;
|
||||||
|
var max = 0xff;
|
||||||
|
var num = (random.nextInt(max - min + 1) + min).toRadixString(16);
|
||||||
|
mac.add(num);
|
||||||
|
}
|
||||||
|
|
||||||
|
var md5Str = md5.convert(utf8.encode(mac.join(':'))).toString();
|
||||||
|
var md5Arr = md5Str.split('');
|
||||||
|
return 'XY${md5Arr[2]}${md5Arr[12]}${md5Arr[22]}$md5Str';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取盐hash跟PubKey
|
||||||
|
static Future getWebKey() async {
|
||||||
|
var res = await Request().get(Api.getWebKey,
|
||||||
|
data: {'disable_rcmd': 0, 'local_id': LoginUtils.generateBuvid()});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {'status': true, 'data': res.data['data']};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'data': {}, 'msg': res.data['message']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// app端密码登录
|
||||||
|
static Future loginInByMobPwd({
|
||||||
|
required String tel,
|
||||||
|
required String password,
|
||||||
|
required String key,
|
||||||
|
required String rhash,
|
||||||
|
}) async {
|
||||||
|
dynamic publicKey = RSAKeyParser().parse(key);
|
||||||
|
String passwordEncryptyed =
|
||||||
|
Encrypter(RSA(publicKey: publicKey)).encrypt(rhash + password).base64;
|
||||||
|
Map<String, dynamic> data = {
|
||||||
|
'username': tel,
|
||||||
|
'password': passwordEncryptyed,
|
||||||
|
'local_id': LoginUtils.generateBuvid(),
|
||||||
|
'disable_rcmd': "0",
|
||||||
|
};
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.loginInByPwdApi,
|
||||||
|
data: data,
|
||||||
|
options: Options(
|
||||||
|
contentType: Headers.formUrlEncodedContentType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
print(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import 'package:pilipala/http/index.dart';
|
import 'package:pilipala/http/index.dart';
|
||||||
import 'package:pilipala/models/dynamics/result.dart';
|
import 'package:pilipala/models/dynamics/result.dart';
|
||||||
|
import 'package:pilipala/models/follow/result.dart';
|
||||||
import 'package:pilipala/models/member/archive.dart';
|
import 'package:pilipala/models/member/archive.dart';
|
||||||
import 'package:pilipala/models/member/info.dart';
|
import 'package:pilipala/models/member/info.dart';
|
||||||
|
import 'package:pilipala/models/member/tags.dart';
|
||||||
import 'package:pilipala/utils/wbi_sign.dart';
|
import 'package:pilipala/utils/wbi_sign.dart';
|
||||||
|
|
||||||
class MemberHttp {
|
class MemberHttp {
|
||||||
@ -18,6 +20,7 @@ class MemberHttp {
|
|||||||
var res = await Request().get(
|
var res = await Request().get(
|
||||||
Api.memberInfo,
|
Api.memberInfo,
|
||||||
data: params,
|
data: params,
|
||||||
|
extra: {'ua': 'pc'},
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return {
|
return {
|
||||||
@ -65,7 +68,7 @@ class MemberHttp {
|
|||||||
int ps = 30,
|
int ps = 30,
|
||||||
int tid = 0,
|
int tid = 0,
|
||||||
int? pn,
|
int? pn,
|
||||||
String keyword = '',
|
String? keyword,
|
||||||
String order = 'pubdate',
|
String order = 'pubdate',
|
||||||
bool orderAvoided = true,
|
bool orderAvoided = true,
|
||||||
}) async {
|
}) async {
|
||||||
@ -74,7 +77,7 @@ class MemberHttp {
|
|||||||
'ps': ps,
|
'ps': ps,
|
||||||
'tid': tid,
|
'tid': tid,
|
||||||
'pn': pn,
|
'pn': pn,
|
||||||
'keyword': keyword,
|
'keyword': keyword ?? '',
|
||||||
'order': order,
|
'order': order,
|
||||||
'platform': 'web',
|
'platform': 'web',
|
||||||
'web_location': 1550101,
|
'web_location': 1550101,
|
||||||
@ -83,6 +86,7 @@ class MemberHttp {
|
|||||||
var res = await Request().get(
|
var res = await Request().get(
|
||||||
Api.memberArchive,
|
Api.memberArchive,
|
||||||
data: params,
|
data: params,
|
||||||
|
extra: {'ua': 'pc'},
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return {
|
return {
|
||||||
@ -119,4 +123,96 @@ class MemberHttp {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索用户动态
|
||||||
|
static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
|
||||||
|
var res = await Request().get(Api.memberDynamic, data: {
|
||||||
|
'keyword': '海拔',
|
||||||
|
'mid': mid,
|
||||||
|
'pn': pn,
|
||||||
|
'ps': ps,
|
||||||
|
'platform': 'web'
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': DynamicsDataModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询分组
|
||||||
|
static Future followUpTags() async {
|
||||||
|
var res = await Request().get(Api.followUpTag);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data']
|
||||||
|
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置分组
|
||||||
|
static Future addUsers(int? fids, String? tagids) async {
|
||||||
|
var res = await Request().post(Api.addUsers, queryParameters: {
|
||||||
|
'fids': fids,
|
||||||
|
'tagids': tagids ?? '0',
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
}, data: {
|
||||||
|
'cross_domain': true
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {'status': true, 'data': [], 'msg': '操作成功'};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取某分组下的up
|
||||||
|
static Future followUpGroup(
|
||||||
|
int? mid,
|
||||||
|
int? tagid,
|
||||||
|
int? pn,
|
||||||
|
int? ps,
|
||||||
|
) async {
|
||||||
|
var res = await Request().get(Api.followUpGroup, data: {
|
||||||
|
'mid': mid,
|
||||||
|
'tagid': tagid,
|
||||||
|
'pn': pn,
|
||||||
|
'ps': ps,
|
||||||
|
});
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
// FollowItemModel
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': res.data['data']
|
||||||
|
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class ReplyHttp {
|
|||||||
Map errMap = {
|
Map errMap = {
|
||||||
-400: '请求错误',
|
-400: '请求错误',
|
||||||
-404: '无此项',
|
-404: '无此项',
|
||||||
12002: '当前页面评论功能已关闭"',
|
12002: '当前页面评论功能已关闭',
|
||||||
12009: '评论主体的type不合法',
|
12009: '评论主体的type不合法',
|
||||||
12061: 'UP主已关闭评论区',
|
12061: 'UP主已关闭评论区',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,37 +1,63 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/http/index.dart';
|
import 'package:pilipala/http/index.dart';
|
||||||
import 'package:pilipala/models/bangumi/info.dart';
|
import 'package:pilipala/models/bangumi/info.dart';
|
||||||
import 'package:pilipala/models/common/search_type.dart';
|
import 'package:pilipala/models/common/search_type.dart';
|
||||||
import 'package:pilipala/models/search/hot.dart';
|
import 'package:pilipala/models/search/hot.dart';
|
||||||
import 'package:pilipala/models/search/result.dart';
|
import 'package:pilipala/models/search/result.dart';
|
||||||
import 'package:pilipala/models/search/suggest.dart';
|
import 'package:pilipala/models/search/suggest.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
class SearchHttp {
|
class SearchHttp {
|
||||||
|
static Box setting = GStrorage.setting;
|
||||||
static Future hotSearchList() async {
|
static Future hotSearchList() async {
|
||||||
var res = await Request().get(Api.hotSearchList);
|
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 {
|
return {
|
||||||
'status': true,
|
'status': true,
|
||||||
'data': HotSearchModel.fromJson(res.data),
|
'data': HotSearchModel.fromJson(res.data),
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
'status': false,
|
|
||||||
'data': [],
|
|
||||||
'msg': '请求错误 🙅',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': '请求错误 🙅',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取搜索建议
|
// 获取搜索建议
|
||||||
static Future searchSuggest({required term}) async {
|
static Future searchSuggest({required term}) async {
|
||||||
var res = await Request().get(Api.serachSuggest,
|
var res = await Request().get(Api.serachSuggest,
|
||||||
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
|
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
|
||||||
if (res.data['code'] == 0) {
|
if (res.data is String) {
|
||||||
res.data['result']['term'] = term;
|
Map<String, dynamic> resultMap = json.decode(res.data);
|
||||||
return {
|
if (resultMap['code'] == 0) {
|
||||||
'status': true,
|
if (resultMap['result'] is Map) {
|
||||||
'data': SearchSuggestModel.fromJson(res.data['result']),
|
resultMap['result']['term'] = term;
|
||||||
};
|
}
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': resultMap['result'] is Map
|
||||||
|
? SearchSuggestModel.fromJson(resultMap['result'])
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': '请求错误 🙅',
|
||||||
|
};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
'status': false,
|
'status': false,
|
||||||
@ -46,39 +72,59 @@ class SearchHttp {
|
|||||||
required SearchType searchType,
|
required SearchType searchType,
|
||||||
required String keyword,
|
required String keyword,
|
||||||
required page,
|
required page,
|
||||||
|
String? order,
|
||||||
|
int? duration,
|
||||||
}) async {
|
}) async {
|
||||||
var res = await Request().get(Api.searchByType, data: {
|
var reqData = {
|
||||||
'search_type': searchType.type,
|
'search_type': searchType.type,
|
||||||
'keyword': keyword,
|
'keyword': keyword,
|
||||||
// 'order_sort': 0,
|
// 'order_sort': 0,
|
||||||
// 'user_type': 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) {
|
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
|
||||||
Object data;
|
Object data;
|
||||||
switch (searchType) {
|
try {
|
||||||
case SearchType.video:
|
switch (searchType) {
|
||||||
data = SearchVideoModel.fromJson(res.data['data']);
|
case SearchType.video:
|
||||||
break;
|
List<int> blackMidsList =
|
||||||
case SearchType.live_room:
|
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
|
||||||
data = SearchLiveModel.fromJson(res.data['data']);
|
for (var i in res.data['data']['result']) {
|
||||||
break;
|
// 屏蔽推广和拉黑用户
|
||||||
case SearchType.bili_user:
|
i['available'] = !blackMidsList.contains(i['mid']);
|
||||||
data = SearchUserModel.fromJson(res.data['data']);
|
}
|
||||||
break;
|
data = SearchVideoModel.fromJson(res.data['data']);
|
||||||
case SearchType.media_bangumi:
|
break;
|
||||||
data = SearchMBangumiModel.fromJson(res.data['data']);
|
case SearchType.live_room:
|
||||||
break;
|
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 {
|
} else {
|
||||||
return {
|
return {
|
||||||
'status': false,
|
'status': false,
|
||||||
'data': [],
|
'data': [],
|
||||||
'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅',
|
'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
|
||||||
|
? '没有相关数据'
|
||||||
|
: res.data['message'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
|
|||||||
import 'package:pilipala/models/user/history.dart';
|
import 'package:pilipala/models/user/history.dart';
|
||||||
import 'package:pilipala/models/user/info.dart';
|
import 'package:pilipala/models/user/info.dart';
|
||||||
import 'package:pilipala/models/user/stat.dart';
|
import 'package:pilipala/models/user/stat.dart';
|
||||||
|
import 'package:pilipala/utils/wbi_sign.dart';
|
||||||
|
|
||||||
class UserHttp {
|
class UserHttp {
|
||||||
static Future<dynamic> userStat({required int mid}) async {
|
static Future<dynamic> userStat({required int mid}) async {
|
||||||
@ -51,10 +52,17 @@ class UserHttp {
|
|||||||
'up_mid': mid,
|
'up_mid': mid,
|
||||||
});
|
});
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
FavFolderData data = FavFolderData.fromJson(res.data['data']);
|
late FavFolderData data;
|
||||||
return {'status': true, 'data': data};
|
if (res.data['data'] != null) {
|
||||||
|
data = FavFolderData.fromJson(res.data['data']);
|
||||||
|
return {'status': true, 'data': data};
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return {'status': false, 'data': [], 'msg': '账号未登录'};
|
return {
|
||||||
|
'status': false,
|
||||||
|
'data': [],
|
||||||
|
'msg': res.data['message'] ?? '账号未登录'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,14 +71,15 @@ class UserHttp {
|
|||||||
required int pn,
|
required int pn,
|
||||||
required int ps,
|
required int ps,
|
||||||
String keyword = '',
|
String keyword = '',
|
||||||
String order = 'mtime'}) async {
|
String order = 'mtime',
|
||||||
|
int type = 0}) async {
|
||||||
var res = await Request().get(Api.userFavFolderDetail, data: {
|
var res = await Request().get(Api.userFavFolderDetail, data: {
|
||||||
'media_id': mediaId,
|
'media_id': mediaId,
|
||||||
'pn': pn,
|
'pn': pn,
|
||||||
'ps': ps,
|
'ps': ps,
|
||||||
'keyword': keyword,
|
'keyword': keyword,
|
||||||
'order': order,
|
'order': order,
|
||||||
'type': 0,
|
'type': type,
|
||||||
'tid': 0,
|
'tid': 0,
|
||||||
'platform': 'web'
|
'platform': 'web'
|
||||||
});
|
});
|
||||||
@ -172,14 +181,16 @@ class UserHttp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 移除已观看
|
// 移除已观看
|
||||||
static Future toViewDel() async {
|
static Future toViewDel({int? aid}) async {
|
||||||
|
final Map<String, dynamic> params = {
|
||||||
|
'jsonp': 'jsonp',
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
};
|
||||||
|
|
||||||
|
params[aid != null ? 'aid' : 'viewed'] = aid ?? true;
|
||||||
var res = await Request().post(
|
var res = await Request().post(
|
||||||
Api.toViewDel,
|
Api.toViewDel,
|
||||||
queryParameters: {
|
queryParameters: params,
|
||||||
'jsonp': 'jsonp',
|
|
||||||
'viewed': true,
|
|
||||||
'csrf': await Request.getCsrf(),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
return {'status': true, 'msg': 'yeah!成功移除'};
|
return {'status': true, 'msg': 'yeah!成功移除'};
|
||||||
@ -222,4 +233,64 @@ class UserHttp {
|
|||||||
return {'status': false, 'msg': res.data['message']};
|
return {'status': false, 'msg': res.data['message']};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除历史记录
|
||||||
|
static Future delHistory(kid) async {
|
||||||
|
var res = await Request().post(
|
||||||
|
Api.delHistory,
|
||||||
|
queryParameters: {
|
||||||
|
'kid': kid,
|
||||||
|
'jsonp': 'jsonp',
|
||||||
|
'csrf': await Request.getCsrf(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {'status': true, 'msg': '已删除'};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'msg': res.data['message']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相互关系查询
|
||||||
|
static Future relationSearch(int mid) async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'mid': mid,
|
||||||
|
'token': '',
|
||||||
|
'platform': 'web',
|
||||||
|
'web_location': 1550101,
|
||||||
|
});
|
||||||
|
var res = await Request().get(
|
||||||
|
Api.relationSearch,
|
||||||
|
data: {
|
||||||
|
'mid': mid,
|
||||||
|
'w_rid': params['w_rid'],
|
||||||
|
'wts': params['wts'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
// relation 主动状态
|
||||||
|
// 被动状态
|
||||||
|
return {'status': true, 'data': res.data['data']};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'msg': res.data['message']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索历史记录
|
||||||
|
static Future searchHistory(
|
||||||
|
{required int pn, required String keyword}) async {
|
||||||
|
var res = await Request().get(
|
||||||
|
Api.searchHistory,
|
||||||
|
data: {
|
||||||
|
'pn': pn,
|
||||||
|
'keyword': keyword,
|
||||||
|
'business': 'all',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
|
||||||
|
} else {
|
||||||
|
return {'status': false, 'msg': res.data['message']};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import 'package:pilipala/models/home/rcmd/result.dart';
|
|||||||
import 'package:pilipala/models/model_hot_video_item.dart';
|
import 'package:pilipala/models/model_hot_video_item.dart';
|
||||||
import 'package:pilipala/models/model_rec_video_item.dart';
|
import 'package:pilipala/models/model_rec_video_item.dart';
|
||||||
import 'package:pilipala/models/user/fav_folder.dart';
|
import 'package:pilipala/models/user/fav_folder.dart';
|
||||||
|
import 'package:pilipala/models/video/ai.dart';
|
||||||
import 'package:pilipala/models/video/play/url.dart';
|
import 'package:pilipala/models/video/play/url.dart';
|
||||||
import 'package:pilipala/models/video_detail_res.dart';
|
import 'package:pilipala/models/video_detail_res.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
import 'package:pilipala/utils/wbi_sign.dart';
|
||||||
|
|
||||||
/// res.data['code'] == 0 请求正常返回结果
|
/// res.data['code'] == 0 请求正常返回结果
|
||||||
/// res.data['data'] 为结果
|
/// res.data['data'] 为结果
|
||||||
@ -20,6 +22,9 @@ import 'package:pilipala/utils/storage.dart';
|
|||||||
class VideoHttp {
|
class VideoHttp {
|
||||||
static Box localCache = GStrorage.localCache;
|
static Box localCache = GStrorage.localCache;
|
||||||
static Box setting = GStrorage.setting;
|
static Box setting = GStrorage.setting;
|
||||||
|
static bool enableRcmdDynamic =
|
||||||
|
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
|
||||||
|
static Box userInfoCache = GStrorage.userInfo;
|
||||||
|
|
||||||
// 首页推荐视频
|
// 首页推荐视频
|
||||||
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
|
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
|
||||||
@ -73,6 +78,7 @@ class VideoHttp {
|
|||||||
for (var i in res.data['data']['items']) {
|
for (var i in res.data['data']['items']) {
|
||||||
// 屏蔽推广和拉黑用户
|
// 屏蔽推广和拉黑用户
|
||||||
if (i['card_goto'] != 'ad_av' &&
|
if (i['card_goto'] != 'ad_av' &&
|
||||||
|
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
|
||||||
(i['args'] != null &&
|
(i['args'] != null &&
|
||||||
!blackMidsList.contains(i['args']['up_mid']))) {
|
!blackMidsList.contains(i['args']['up_mid']))) {
|
||||||
list.add(RecVideoItemAppModel.fromJson(i));
|
list.add(RecVideoItemAppModel.fromJson(i));
|
||||||
@ -130,6 +136,11 @@ class VideoHttp {
|
|||||||
// 'platform': '',
|
// 'platform': '',
|
||||||
// 'high_quality': ''
|
// 'high_quality': ''
|
||||||
};
|
};
|
||||||
|
// 免登录查看1080p
|
||||||
|
if (userInfoCache.get('userInfoCache') == null &&
|
||||||
|
setting.get(SettingBoxKey.p1080, defaultValue: true)) {
|
||||||
|
data['try_look'] = 1;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
var res = await Request().get(Api.videoUrl, data: data);
|
var res = await Request().get(Api.videoUrl, data: data);
|
||||||
if (res.data['code'] == 0) {
|
if (res.data['code'] == 0) {
|
||||||
@ -399,4 +410,35 @@ class VideoHttp {
|
|||||||
return {'status': false, 'msg': res.data['result']['toast']};
|
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']};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future aiConclusion({
|
||||||
|
String? bvid,
|
||||||
|
int? cid,
|
||||||
|
int? upMid,
|
||||||
|
}) async {
|
||||||
|
Map params = await WbiSign().makSign({
|
||||||
|
'bvid': bvid,
|
||||||
|
'cid': cid,
|
||||||
|
'up_mid': upMid,
|
||||||
|
});
|
||||||
|
var res = await Request().get(Api.aiConclusion, data: params);
|
||||||
|
if (res.data['code'] == 0) {
|
||||||
|
return {
|
||||||
|
'status': true,
|
||||||
|
'data': AiConclusionModel.fromJson(res.data['data']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
116
lib/main.dart
@ -1,4 +1,11 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
=======
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
>>>>>>> main
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@ -7,11 +14,14 @@ import 'package:dynamic_color/dynamic_color.dart';
|
|||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/common/widgets/custom_toast.dart';
|
import 'package:pilipala/common/widgets/custom_toast.dart';
|
||||||
import 'package:pilipala/http/init.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/models/common/theme_type.dart';
|
||||||
import 'package:pilipala/pages/search/index.dart';
|
import 'package:pilipala/pages/search/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/index.dart';
|
import 'package:pilipala/pages/video/detail/index.dart';
|
||||||
import 'package:pilipala/router/app_pages.dart';
|
import 'package:pilipala/router/app_pages.dart';
|
||||||
import 'package:pilipala/pages/main/view.dart';
|
import 'package:pilipala/pages/main/view.dart';
|
||||||
|
import 'package:pilipala/services/service_locator.dart';
|
||||||
|
import 'package:pilipala/utils/app_scheme.dart';
|
||||||
import 'package:pilipala/utils/data.dart';
|
import 'package:pilipala/utils/data.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
|
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
|
||||||
@ -23,10 +33,34 @@ void main() async {
|
|||||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
|
||||||
.then((_) async {
|
.then((_) async {
|
||||||
await GStrorage.init();
|
await GStrorage.init();
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
|
await AudioService.init<AudioHandler>(
|
||||||
|
builder: () => MyAudioHandler(),
|
||||||
|
config: const AudioServiceConfig(
|
||||||
|
androidNotificationChannelId: 'com.guozhigq.pilipala.channel.audio',
|
||||||
|
androidNotificationChannelName: 'Music playback',
|
||||||
|
androidNotificationOngoing: true,
|
||||||
|
androidStopForegroundOnPause: true,
|
||||||
|
androidNotificationIcon: 'drawable/audio_service_icon',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
=======
|
||||||
|
await setupServiceLocator();
|
||||||
|
>>>>>>> main
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
// 小白条、导航栏沉浸
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
|
||||||
|
systemNavigationBarColor: Colors.transparent,
|
||||||
|
systemNavigationBarDividerColor: Colors.transparent,
|
||||||
|
statusBarColor: Colors.transparent,
|
||||||
|
));
|
||||||
await Request.setCookie();
|
await Request.setCookie();
|
||||||
await Data.init();
|
Data.init();
|
||||||
await GStrorage.lazyInit();
|
GStrorage.lazyInit();
|
||||||
|
PiliSchame.init();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,15 +69,44 @@ class MyApp extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color brandColor = const Color.fromARGB(255, 92, 182, 123);
|
|
||||||
Box setting = GStrorage.setting;
|
Box setting = GStrorage.setting;
|
||||||
|
// 主题色
|
||||||
|
Color defaultColor =
|
||||||
|
colorThemeTypes[setting.get(SettingBoxKey.customColor, defaultValue: 0)]
|
||||||
|
['color'];
|
||||||
|
Color brandColor = defaultColor;
|
||||||
|
// 主题模式
|
||||||
ThemeType currentThemeValue = ThemeType.values[setting
|
ThemeType currentThemeValue = ThemeType.values[setting
|
||||||
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
|
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
|
||||||
|
// 是否动态取色
|
||||||
|
bool isDynamicColor =
|
||||||
|
setting.get(SettingBoxKey.dynamicColor, defaultValue: true);
|
||||||
|
// 字体缩放大小
|
||||||
|
double textScale =
|
||||||
|
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
|
||||||
|
|
||||||
|
// 强制设置高帧率
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
try {
|
||||||
|
late List modes;
|
||||||
|
FlutterDisplayMode.supported.then((value) {
|
||||||
|
modes = value;
|
||||||
|
var storageDisplay = setting.get(SettingBoxKey.displayMode);
|
||||||
|
DisplayMode f = DisplayMode.auto;
|
||||||
|
if (storageDisplay != null) {
|
||||||
|
f = modes.firstWhere((e) => e.toString() == storageDisplay);
|
||||||
|
}
|
||||||
|
DisplayMode preferred = modes.toList().firstWhere((el) => el == f);
|
||||||
|
FlutterDisplayMode.setPreferredMode(preferred);
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
|
||||||
ColorScheme? lightColorScheme;
|
ColorScheme? lightColorScheme;
|
||||||
ColorScheme? darkColorScheme;
|
ColorScheme? darkColorScheme;
|
||||||
if (lightDynamic != null && darkDynamic != null) {
|
if (lightDynamic != null && darkDynamic != null && isDynamicColor) {
|
||||||
// dynamic取色成功
|
// dynamic取色成功
|
||||||
lightColorScheme = lightDynamic.harmonized();
|
lightColorScheme = lightDynamic.harmonized();
|
||||||
darkColorScheme = darkDynamic.harmonized();
|
darkColorScheme = darkDynamic.harmonized();
|
||||||
@ -93,9 +156,17 @@ class MyApp extends StatelessWidget {
|
|||||||
fallbackLocale: const Locale("zh", "CN"),
|
fallbackLocale: const Locale("zh", "CN"),
|
||||||
getPages: Routes.getPages,
|
getPages: Routes.getPages,
|
||||||
home: const MainApp(),
|
home: const MainApp(),
|
||||||
builder: FlutterSmartDialog.init(
|
builder: (BuildContext context, Widget? child) {
|
||||||
toastBuilder: (String msg) => CustomToast(msg: msg),
|
return FlutterSmartDialog(
|
||||||
),
|
toastBuilder: (String msg) => CustomToast(msg: msg),
|
||||||
|
child: MediaQuery(
|
||||||
|
data: MediaQuery.of(context).copyWith(
|
||||||
|
textScaleFactor:
|
||||||
|
MediaQuery.of(context).textScaleFactor * textScale),
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
navigatorObservers: [
|
navigatorObservers: [
|
||||||
VideoDetailPage.routeObserver,
|
VideoDetailPage.routeObserver,
|
||||||
SearchPage.routeObserver,
|
SearchPage.routeObserver,
|
||||||
@ -105,3 +176,34 @@ class MyApp extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MyAudioHandler extends BaseAudioHandler
|
||||||
|
with
|
||||||
|
QueueHandler, // mix in default queue callback implementations
|
||||||
|
SeekHandler {
|
||||||
|
// mix in default seek callback implementations
|
||||||
|
|
||||||
|
// The most common callbacks:
|
||||||
|
@override
|
||||||
|
Future<void> play() async {
|
||||||
|
print('play');
|
||||||
|
// All 'play' requests from all origins route to here. Implement this
|
||||||
|
// callback to start playing audio appropriate to your app. e.g. music.
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
@override
|
||||||
|
Future<void> pause() async {}
|
||||||
|
|
||||||
|
///
|
||||||
|
@override
|
||||||
|
Future<void> stop() async {}
|
||||||
|
|
||||||
|
///
|
||||||
|
@override
|
||||||
|
Future<void> seek(Duration position) async {}
|
||||||
|
|
||||||
|
///
|
||||||
|
@override
|
||||||
|
Future<void> skipToQueueItem(int i) async {}
|
||||||
|
}
|
||||||
|
|||||||
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_room,
|
||||||
// 主播:live_user
|
// 主播:live_user
|
||||||
// live_user,
|
// live_user,
|
||||||
// 专栏:article
|
|
||||||
// article,
|
|
||||||
// 话题:topic
|
// 话题:topic
|
||||||
// topic,
|
// topic,
|
||||||
// 用户:bili_user
|
// 用户:bili_user
|
||||||
bili_user,
|
bili_user,
|
||||||
|
// 专栏:article
|
||||||
|
article,
|
||||||
// 相簿:photo
|
// 相簿:photo
|
||||||
// photo
|
// photo
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SearchTypeExtension on SearchType {
|
extension SearchTypeExtension on SearchType {
|
||||||
String get type =>
|
String get type =>
|
||||||
['video', 'media_bangumi', 'live_room', 'bili_user'][index];
|
['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
|
||||||
String get label => ['视频', '番剧', '直播间', '用户'][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
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
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
@ -0,0 +1,893 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package bilibili.community.service.dm.v1;
|
||||||
|
|
||||||
|
// 说明文档
|
||||||
|
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/danmaku_proto.md
|
||||||
|
|
||||||
|
//弹幕
|
||||||
|
service DM {
|
||||||
|
// 获取分段弹幕
|
||||||
|
rpc DmSegMobile (DmSegMobileReq) returns (DmSegMobileReply);
|
||||||
|
// 客户端弹幕元数据 字幕、分段、防挡蒙版等
|
||||||
|
rpc DmView(DmViewReq) returns (DmViewReply);
|
||||||
|
// 修改弹幕配置
|
||||||
|
rpc DmPlayerConfig (DmPlayerConfigReq) returns (Response);
|
||||||
|
// ott弹幕列表
|
||||||
|
rpc DmSegOtt(DmSegOttReq) returns(DmSegOttReply);
|
||||||
|
// SDK弹幕列表
|
||||||
|
rpc DmSegSDK(DmSegSDKReq) returns(DmSegSDKReply);
|
||||||
|
//
|
||||||
|
rpc DmExpoReport(DmExpoReportReq) returns (DmExpoReportRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Avatar {
|
||||||
|
//
|
||||||
|
string id = 1;
|
||||||
|
//
|
||||||
|
string url = 2;
|
||||||
|
//
|
||||||
|
AvatarType avatar_type = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum AvatarType {
|
||||||
|
AvatarTypeNone = 0; //
|
||||||
|
AvatarTypeNFT = 1; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Bubble {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
string url = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum BubbleType {
|
||||||
|
BubbleTypeNone = 0; //
|
||||||
|
BubbleTypeClickButton = 1; //
|
||||||
|
BubbleTypeDmSettingPanel = 2; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message BubbleV2 {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
string url = 2;
|
||||||
|
//
|
||||||
|
BubbleType bubble_type = 3;
|
||||||
|
//
|
||||||
|
bool exposure_once = 4;
|
||||||
|
//
|
||||||
|
ExposureType exposure_type = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Button {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
int32 action = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message BuzzwordConfig {
|
||||||
|
//
|
||||||
|
repeated BuzzwordShowConfig keywords = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message BuzzwordShowConfig {
|
||||||
|
//
|
||||||
|
string name = 1;
|
||||||
|
//
|
||||||
|
string schema = 2;
|
||||||
|
//
|
||||||
|
int32 source = 3;
|
||||||
|
//
|
||||||
|
int64 id = 4;
|
||||||
|
//
|
||||||
|
int64 buzzword_id = 5;
|
||||||
|
//
|
||||||
|
int32 schema_type = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message CheckBox {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
CheckboxType type = 2;
|
||||||
|
//
|
||||||
|
bool default_value = 3;
|
||||||
|
//
|
||||||
|
bool show = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum CheckboxType {
|
||||||
|
CheckboxTypeNone = 0; //
|
||||||
|
CheckboxTypeEncourage = 1; //
|
||||||
|
CheckboxTypeColorDM = 2; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message CheckBoxV2 {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
int32 type = 2;
|
||||||
|
//
|
||||||
|
bool default_value = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message ClickButton {
|
||||||
|
//
|
||||||
|
repeated string portrait_text = 1;
|
||||||
|
//
|
||||||
|
repeated string landscape_text = 2;
|
||||||
|
//
|
||||||
|
repeated string portrait_text_focus = 3;
|
||||||
|
//
|
||||||
|
repeated string landscape_text_focus = 4;
|
||||||
|
//
|
||||||
|
RenderType render_type = 5;
|
||||||
|
//
|
||||||
|
bool show = 6;
|
||||||
|
//
|
||||||
|
Bubble bubble = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message ClickButtonV2 {
|
||||||
|
//
|
||||||
|
repeated string portrait_text = 1;
|
||||||
|
//
|
||||||
|
repeated string landscape_text = 2;
|
||||||
|
//
|
||||||
|
repeated string portrait_text_focus = 3;
|
||||||
|
//
|
||||||
|
repeated string landscape_text_focus = 4;
|
||||||
|
//
|
||||||
|
int32 render_type = 5;
|
||||||
|
//
|
||||||
|
bool text_input_post = 6;
|
||||||
|
//
|
||||||
|
bool exposure_once = 7;
|
||||||
|
//
|
||||||
|
int32 exposure_type = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 互动弹幕条目信息
|
||||||
|
message CommandDm {
|
||||||
|
// 弹幕id
|
||||||
|
int64 id = 1;
|
||||||
|
// 对象视频cid
|
||||||
|
int64 oid = 2;
|
||||||
|
// 发送者mid
|
||||||
|
string mid = 3;
|
||||||
|
// 互动弹幕指令
|
||||||
|
string command = 4;
|
||||||
|
// 互动弹幕正文
|
||||||
|
string content = 5;
|
||||||
|
// 出现时间
|
||||||
|
int32 progress = 6;
|
||||||
|
// 创建时间
|
||||||
|
string ctime = 7;
|
||||||
|
// 发布时间
|
||||||
|
string mtime = 8;
|
||||||
|
// 扩展json数据
|
||||||
|
string extra = 9;
|
||||||
|
// 弹幕id str类型
|
||||||
|
string idStr = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕ai云屏蔽列表
|
||||||
|
message DanmakuAIFlag {
|
||||||
|
// 弹幕ai云屏蔽条目
|
||||||
|
repeated DanmakuFlag dm_flags = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕条目
|
||||||
|
message DanmakuElem {
|
||||||
|
// 弹幕dmid
|
||||||
|
int64 id = 1;
|
||||||
|
// 弹幕出现位置(单位ms)
|
||||||
|
int32 progress = 2;
|
||||||
|
// 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
|
||||||
|
int32 mode = 3;
|
||||||
|
// 弹幕字号
|
||||||
|
int32 fontsize = 4;
|
||||||
|
// 弹幕颜色
|
||||||
|
uint32 color = 5;
|
||||||
|
// 发送者mid hash
|
||||||
|
string midHash = 6;
|
||||||
|
// 弹幕正文
|
||||||
|
string content = 7;
|
||||||
|
// 发送时间
|
||||||
|
int64 ctime = 8;
|
||||||
|
// 权重 用于屏蔽等级 区间:[1,10]
|
||||||
|
int32 weight = 9;
|
||||||
|
// 动作
|
||||||
|
string action = 10;
|
||||||
|
// 弹幕池 0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕)
|
||||||
|
int32 pool = 11;
|
||||||
|
// 弹幕dmid str
|
||||||
|
string idStr = 12;
|
||||||
|
// 弹幕属性位(bin求AND)
|
||||||
|
// bit0:保护 bit1:直播 bit2:高赞
|
||||||
|
int32 attr = 13;
|
||||||
|
//
|
||||||
|
string animation = 22;
|
||||||
|
// 大会员专属颜色
|
||||||
|
DmColorfulType colorful = 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕ai云屏蔽条目
|
||||||
|
message DanmakuFlag {
|
||||||
|
// 弹幕dmid
|
||||||
|
int64 dmid = 1;
|
||||||
|
// 评分
|
||||||
|
uint32 flag = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 云屏蔽配置信息
|
||||||
|
message DanmakuFlagConfig {
|
||||||
|
// 云屏蔽等级
|
||||||
|
int32 rec_flag = 1;
|
||||||
|
// 云屏蔽文案
|
||||||
|
string rec_text = 2;
|
||||||
|
// 云屏蔽开关
|
||||||
|
int32 rec_switch = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕默认配置
|
||||||
|
message DanmuDefaultPlayerConfig {
|
||||||
|
bool player_danmaku_use_default_config = 1; // 是否使用推荐弹幕设置
|
||||||
|
bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽
|
||||||
|
int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级
|
||||||
|
bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕
|
||||||
|
bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕
|
||||||
|
bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕
|
||||||
|
bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕
|
||||||
|
bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕
|
||||||
|
bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕
|
||||||
|
float player_danmaku_opacity = 12; // 弹幕不透明度
|
||||||
|
float player_danmaku_scalingfactor = 13; // 弹幕缩放比例
|
||||||
|
float player_danmaku_domain = 14; // 弹幕显示区域
|
||||||
|
int32 player_danmaku_speed = 15; // 弹幕速度
|
||||||
|
bool inline_player_danmaku_switch = 16; // 是否开启弹幕
|
||||||
|
int32 player_danmaku_senior_mode_switch = 17; //
|
||||||
|
int32 player_danmaku_ai_recommended_level_v2 = 18; //
|
||||||
|
map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 19; //
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕配置
|
||||||
|
message DanmuPlayerConfig {
|
||||||
|
bool player_danmaku_switch = 1; // 是否开启弹幕
|
||||||
|
bool player_danmaku_switch_save = 2; // 是否记录弹幕开关设置
|
||||||
|
bool player_danmaku_use_default_config = 3; // 是否使用推荐弹幕设置
|
||||||
|
bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽
|
||||||
|
int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级
|
||||||
|
bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕
|
||||||
|
bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕
|
||||||
|
bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕
|
||||||
|
bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕
|
||||||
|
bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕
|
||||||
|
bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕
|
||||||
|
float player_danmaku_opacity = 12; // 弹幕不透明度
|
||||||
|
float player_danmaku_scalingfactor = 13; // 弹幕缩放比例
|
||||||
|
float player_danmaku_domain = 14; // 弹幕显示区域
|
||||||
|
int32 player_danmaku_speed = 15; // 弹幕速度
|
||||||
|
bool player_danmaku_enableblocklist = 16; // 是否开启屏蔽列表
|
||||||
|
bool inline_player_danmaku_switch = 17; // 是否开启弹幕
|
||||||
|
int32 inline_player_danmaku_config = 18; //
|
||||||
|
int32 player_danmaku_ios_switch_save = 19; //
|
||||||
|
int32 player_danmaku_senior_mode_switch = 20; //
|
||||||
|
int32 player_danmaku_ai_recommended_level_v2 = 21; //
|
||||||
|
map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 22; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message DanmuPlayerConfigPanel {
|
||||||
|
//
|
||||||
|
string selection_text = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕显示区域自动配置
|
||||||
|
message DanmuPlayerDynamicConfig {
|
||||||
|
// 时间
|
||||||
|
int32 progress = 1;
|
||||||
|
// 弹幕显示区域
|
||||||
|
float player_danmaku_domain = 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕配置信息
|
||||||
|
message DanmuPlayerViewConfig {
|
||||||
|
// 弹幕默认配置
|
||||||
|
DanmuDefaultPlayerConfig danmuku_default_player_config = 1;
|
||||||
|
// 弹幕用户配置
|
||||||
|
DanmuPlayerConfig danmuku_player_config = 2;
|
||||||
|
// 弹幕显示区域自动配置列表
|
||||||
|
repeated DanmuPlayerDynamicConfig danmuku_player_dynamic_config = 3;
|
||||||
|
//
|
||||||
|
DanmuPlayerConfigPanel danmuku_player_config_panel = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web端用户弹幕配置
|
||||||
|
message DanmuWebPlayerConfig {
|
||||||
|
bool dm_switch = 1; // 是否开启弹幕
|
||||||
|
bool ai_switch = 2; // 是否开启智能云屏蔽
|
||||||
|
int32 ai_level = 3; // 智能云屏蔽等级
|
||||||
|
bool blocktop = 4; // 是否屏蔽顶端弹幕
|
||||||
|
bool blockscroll = 5; // 是否屏蔽滚动弹幕
|
||||||
|
bool blockbottom = 6; // 是否屏蔽底端弹幕
|
||||||
|
bool blockcolor = 7; // 是否屏蔽彩色弹幕
|
||||||
|
bool blockspecial = 8; // 是否屏蔽重复弹幕
|
||||||
|
bool preventshade = 9; //
|
||||||
|
bool dmask = 10; //
|
||||||
|
float opacity = 11; //
|
||||||
|
int32 dmarea = 12; //
|
||||||
|
float speedplus = 13; //
|
||||||
|
float fontsize = 14; // 弹幕字号
|
||||||
|
bool screensync = 15; //
|
||||||
|
bool speedsync = 16; //
|
||||||
|
string fontfamily = 17; //
|
||||||
|
bool bold = 18; // 是否使用加粗
|
||||||
|
int32 fontborder = 19; //
|
||||||
|
string draw_type = 20; // 弹幕渲染类型
|
||||||
|
int32 senior_mode_switch = 21; //
|
||||||
|
int32 ai_level_v2 = 22; //
|
||||||
|
map<int32, int32> ai_level_v2_map = 23; //
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕属性位值
|
||||||
|
enum DMAttrBit {
|
||||||
|
DMAttrBitProtect = 0; // 保护弹幕
|
||||||
|
DMAttrBitFromLive = 1; // 直播弹幕
|
||||||
|
DMAttrHighLike = 2; // 高赞弹幕
|
||||||
|
}
|
||||||
|
|
||||||
|
message DmColorful {
|
||||||
|
DmColorfulType type = 1; // 颜色类型
|
||||||
|
string src = 2; //
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DmColorfulType {
|
||||||
|
NoneType = 0; // 无
|
||||||
|
VipGradualColor = 60001; // 渐变色
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message DmExpoReportReq {
|
||||||
|
//
|
||||||
|
string session_id = 1;
|
||||||
|
//
|
||||||
|
int64 oid = 2;
|
||||||
|
//
|
||||||
|
string spmid = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message DmExpoReportRes {}
|
||||||
|
|
||||||
|
// 修改弹幕配置-请求
|
||||||
|
message DmPlayerConfigReq {
|
||||||
|
int64 ts = 1; //
|
||||||
|
PlayerDanmakuSwitch switch = 2; // 是否开启弹幕
|
||||||
|
PlayerDanmakuSwitchSave switch_save = 3; // 是否记录弹幕开关设置
|
||||||
|
PlayerDanmakuUseDefaultConfig use_default_config = 4; // 是否使用推荐弹幕设置
|
||||||
|
PlayerDanmakuAiRecommendedSwitch ai_recommended_switch = 5; // 是否开启智能云屏蔽
|
||||||
|
PlayerDanmakuAiRecommendedLevel ai_recommended_level = 6; // 智能云屏蔽等级
|
||||||
|
PlayerDanmakuBlocktop blocktop = 7; // 是否屏蔽顶端弹幕
|
||||||
|
PlayerDanmakuBlockscroll blockscroll = 8; // 是否屏蔽滚动弹幕
|
||||||
|
PlayerDanmakuBlockbottom blockbottom = 9; // 是否屏蔽底端弹幕
|
||||||
|
PlayerDanmakuBlockcolorful blockcolorful = 10; // 是否屏蔽彩色弹幕
|
||||||
|
PlayerDanmakuBlockrepeat blockrepeat = 11; // 是否屏蔽重复弹幕
|
||||||
|
PlayerDanmakuBlockspecial blockspecial = 12; // 是否屏蔽高级弹幕
|
||||||
|
PlayerDanmakuOpacity opacity = 13; // 弹幕不透明度
|
||||||
|
PlayerDanmakuScalingfactor scalingfactor = 14; // 弹幕缩放比例
|
||||||
|
PlayerDanmakuDomain domain = 15; // 弹幕显示区域
|
||||||
|
PlayerDanmakuSpeed speed = 16; // 弹幕速度
|
||||||
|
PlayerDanmakuEnableblocklist enableblocklist = 17; // 是否开启屏蔽列表
|
||||||
|
InlinePlayerDanmakuSwitch inlinePlayerDanmakuSwitch = 18; // 是否开启弹幕
|
||||||
|
PlayerDanmakuSeniorModeSwitch senior_mode_switch = 19; //
|
||||||
|
PlayerDanmakuAiRecommendedLevelV2 ai_recommended_level_v2 = 20; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message DmSegConfig {
|
||||||
|
//
|
||||||
|
int64 page_size = 1;
|
||||||
|
//
|
||||||
|
int64 total = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取弹幕-响应
|
||||||
|
message DmSegMobileReply {
|
||||||
|
// 弹幕列表
|
||||||
|
repeated DanmakuElem elems = 1;
|
||||||
|
// 是否已关闭弹幕
|
||||||
|
// 0:未关闭 1:已关闭
|
||||||
|
int32 state = 2;
|
||||||
|
// 弹幕云屏蔽ai评分值
|
||||||
|
DanmakuAIFlag ai_flag = 3;
|
||||||
|
repeated DmColorful colorfulSrc = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取弹幕-请求
|
||||||
|
message DmSegMobileReq {
|
||||||
|
// 稿件avid/漫画epid
|
||||||
|
int64 pid = 1;
|
||||||
|
// 视频cid/漫画cid
|
||||||
|
int64 oid = 2;
|
||||||
|
// 弹幕类型
|
||||||
|
// 1:视频 2:漫画
|
||||||
|
int32 type = 3;
|
||||||
|
// 分段(6min)
|
||||||
|
int64 segment_index = 4;
|
||||||
|
// 是否青少年模式
|
||||||
|
int32 teenagers_mode = 5;
|
||||||
|
//
|
||||||
|
int64 ps = 6;
|
||||||
|
//
|
||||||
|
int64 pe = 7;
|
||||||
|
//
|
||||||
|
int32 pull_mode = 8;
|
||||||
|
//
|
||||||
|
int32 from_scene = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ott弹幕列表-响应
|
||||||
|
message DmSegOttReply {
|
||||||
|
// 是否已关闭弹幕
|
||||||
|
// 0:未关闭 1:已关闭
|
||||||
|
bool closed = 1;
|
||||||
|
// 弹幕列表
|
||||||
|
repeated DanmakuElem elems = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ott弹幕列表-请求
|
||||||
|
message DmSegOttReq {
|
||||||
|
// 稿件avid/漫画epid
|
||||||
|
int64 pid = 1;
|
||||||
|
// 视频cid/漫画cid
|
||||||
|
int64 oid = 2;
|
||||||
|
// 弹幕类型
|
||||||
|
// 1:视频 2:漫画
|
||||||
|
int32 type = 3;
|
||||||
|
// 分段(6min)
|
||||||
|
int64 segment_index = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕SDK-响应
|
||||||
|
message DmSegSDKReply {
|
||||||
|
// 是否已关闭弹幕
|
||||||
|
// 0:未关闭 1:已关闭
|
||||||
|
bool closed = 1;
|
||||||
|
// 弹幕列表
|
||||||
|
repeated DanmakuElem elems = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕SDK-请求
|
||||||
|
message DmSegSDKReq {
|
||||||
|
// 稿件avid/漫画epid
|
||||||
|
int64 pid = 1;
|
||||||
|
// 视频cid/漫画cid
|
||||||
|
int64 oid = 2;
|
||||||
|
// 弹幕类型
|
||||||
|
// 1:视频 2:漫画
|
||||||
|
int32 type = 3;
|
||||||
|
// 分段(6min)
|
||||||
|
int64 segment_index = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端弹幕元数据-响应
|
||||||
|
message DmViewReply {
|
||||||
|
// 是否已关闭弹幕
|
||||||
|
// 0:未关闭 1:已关闭
|
||||||
|
bool closed = 1;
|
||||||
|
// 智能防挡弹幕蒙版信息
|
||||||
|
VideoMask mask = 2;
|
||||||
|
// 视频字幕
|
||||||
|
VideoSubtitle subtitle = 3;
|
||||||
|
// 高级弹幕专包url(bfs)
|
||||||
|
repeated string special_dms = 4;
|
||||||
|
// 云屏蔽配置信息
|
||||||
|
DanmakuFlagConfig ai_flag = 5;
|
||||||
|
// 弹幕配置信息
|
||||||
|
DanmuPlayerViewConfig player_config = 6;
|
||||||
|
// 弹幕发送框样式
|
||||||
|
int32 send_box_style = 7;
|
||||||
|
// 是否允许
|
||||||
|
bool allow = 8;
|
||||||
|
// check box 是否展示
|
||||||
|
string check_box = 9;
|
||||||
|
// check box 展示文本
|
||||||
|
string check_box_show_msg = 10;
|
||||||
|
// 展示文案
|
||||||
|
string text_placeholder = 11;
|
||||||
|
// 弹幕输入框文案
|
||||||
|
string input_placeholder = 12;
|
||||||
|
// 用户举报弹幕 cid维度屏蔽的正则规则
|
||||||
|
repeated string report_filter_content = 13;
|
||||||
|
//
|
||||||
|
ExpoReport expo_report = 14;
|
||||||
|
//
|
||||||
|
BuzzwordConfig buzzword_config = 15;
|
||||||
|
//
|
||||||
|
repeated Expressions expressions = 16;
|
||||||
|
//
|
||||||
|
repeated PostPanel post_panel = 17;
|
||||||
|
//
|
||||||
|
repeated string activity_meta = 18;
|
||||||
|
//
|
||||||
|
repeated PostPanelV2 post_panel2 = 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端弹幕元数据-请求
|
||||||
|
message DmViewReq {
|
||||||
|
// 稿件avid/漫画epid
|
||||||
|
int64 pid = 1;
|
||||||
|
// 视频cid/漫画cid
|
||||||
|
int64 oid = 2;
|
||||||
|
// 弹幕类型
|
||||||
|
// 1:视频 2:漫画
|
||||||
|
int32 type = 3;
|
||||||
|
// 页面spm
|
||||||
|
string spmid = 4;
|
||||||
|
// 是否冷启
|
||||||
|
int32 is_hard_boot = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// web端弹幕元数据-响应
|
||||||
|
// https://api.bilibili.com/x/v2/dm/web/view
|
||||||
|
message DmWebViewReply {
|
||||||
|
// 是否已关闭弹幕
|
||||||
|
// 0:未关闭 1:已关闭
|
||||||
|
int32 state = 1;
|
||||||
|
//
|
||||||
|
string text = 2;
|
||||||
|
//
|
||||||
|
string text_side = 3;
|
||||||
|
// 分段弹幕配置
|
||||||
|
DmSegConfig dm_sge = 4;
|
||||||
|
// 云屏蔽配置信息
|
||||||
|
DanmakuFlagConfig flag = 5;
|
||||||
|
// 高级弹幕专包url(bfs)
|
||||||
|
repeated string special_dms = 6;
|
||||||
|
// check box 是否展示
|
||||||
|
bool check_box = 7;
|
||||||
|
// 弹幕数
|
||||||
|
int64 count = 8;
|
||||||
|
// 互动弹幕
|
||||||
|
repeated CommandDm commandDms = 9;
|
||||||
|
// 用户弹幕配置
|
||||||
|
DanmuWebPlayerConfig player_config = 10;
|
||||||
|
// 用户举报弹幕 cid维度屏蔽
|
||||||
|
repeated string report_filter_content = 11;
|
||||||
|
//
|
||||||
|
repeated Expressions expressions = 12;
|
||||||
|
//
|
||||||
|
repeated PostPanel post_panel = 13;
|
||||||
|
//
|
||||||
|
repeated string activity_meta = 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message ExpoReport {
|
||||||
|
//
|
||||||
|
bool should_report_at_end = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum ExposureType {
|
||||||
|
ExposureTypeNone = 0; //
|
||||||
|
ExposureTypeDMSend = 1; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Expression {
|
||||||
|
//
|
||||||
|
repeated string keyword = 1;
|
||||||
|
//
|
||||||
|
string url = 2;
|
||||||
|
//
|
||||||
|
repeated Period period = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Expressions {
|
||||||
|
//
|
||||||
|
repeated Expression data = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否开启弹幕
|
||||||
|
message InlinePlayerDanmakuSwitch {
|
||||||
|
//
|
||||||
|
bool value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Label {
|
||||||
|
//
|
||||||
|
string title = 1;
|
||||||
|
//
|
||||||
|
repeated string content = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message LabelV2 {
|
||||||
|
//
|
||||||
|
string title = 1;
|
||||||
|
//
|
||||||
|
repeated string content = 2;
|
||||||
|
//
|
||||||
|
bool exposure_once = 3;
|
||||||
|
//
|
||||||
|
int32 exposure_type = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Period {
|
||||||
|
//
|
||||||
|
int64 start = 1;
|
||||||
|
//
|
||||||
|
int64 end = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PlayerDanmakuAiRecommendedLevel {bool value = 1;} // 智能云屏蔽等级
|
||||||
|
message PlayerDanmakuAiRecommendedLevelV2 {int32 value = 1;} //
|
||||||
|
message PlayerDanmakuAiRecommendedSwitch {bool value = 1;} // 是否开启智能云屏蔽
|
||||||
|
message PlayerDanmakuBlockbottom {bool value = 1;} // 是否屏蔽底端弹幕
|
||||||
|
message PlayerDanmakuBlockcolorful {bool value = 1;} // 是否屏蔽彩色弹幕
|
||||||
|
message PlayerDanmakuBlockrepeat {bool value = 1;} // 是否屏蔽重复弹幕
|
||||||
|
message PlayerDanmakuBlockscroll {bool value = 1;} // 是否屏蔽滚动弹幕
|
||||||
|
message PlayerDanmakuBlockspecial {bool value = 1;} // 是否屏蔽高级弹幕
|
||||||
|
message PlayerDanmakuBlocktop {bool value = 1;} // 是否屏蔽顶端弹幕
|
||||||
|
message PlayerDanmakuDomain {float value = 1;} // 弹幕显示区域
|
||||||
|
message PlayerDanmakuEnableblocklist {bool value = 1;} // 是否开启屏蔽列表
|
||||||
|
message PlayerDanmakuOpacity {float value = 1;} // 弹幕不透明度
|
||||||
|
message PlayerDanmakuScalingfactor {float value = 1;} // 弹幕缩放比例
|
||||||
|
message PlayerDanmakuSeniorModeSwitch {int32 value = 1;} //
|
||||||
|
message PlayerDanmakuSpeed {int32 value = 1;} // 弹幕速度
|
||||||
|
message PlayerDanmakuSwitch {bool value = 1; bool can_ignore = 2;} // 是否开启弹幕
|
||||||
|
message PlayerDanmakuSwitchSave {bool value = 1;} // 是否记录弹幕开关设置
|
||||||
|
message PlayerDanmakuUseDefaultConfig {bool value = 1;} // 是否使用推荐弹幕设置
|
||||||
|
|
||||||
|
//
|
||||||
|
message PostPanel {
|
||||||
|
//
|
||||||
|
int64 start = 1;
|
||||||
|
//
|
||||||
|
int64 end = 2;
|
||||||
|
//
|
||||||
|
int64 priority = 3;
|
||||||
|
//
|
||||||
|
int64 biz_id = 4;
|
||||||
|
//
|
||||||
|
PostPanelBizType biz_type = 5;
|
||||||
|
//
|
||||||
|
ClickButton click_button = 6;
|
||||||
|
//
|
||||||
|
TextInput text_input = 7;
|
||||||
|
//
|
||||||
|
CheckBox check_box = 8;
|
||||||
|
//
|
||||||
|
Toast toast = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum PostPanelBizType {
|
||||||
|
PostPanelBizTypeNone = 0; //
|
||||||
|
PostPanelBizTypeEncourage = 1; //
|
||||||
|
PostPanelBizTypeColorDM = 2; //
|
||||||
|
PostPanelBizTypeNFTDM = 3; //
|
||||||
|
PostPanelBizTypeFragClose = 4; //
|
||||||
|
PostPanelBizTypeRecommend = 5; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message PostPanelV2 {
|
||||||
|
//
|
||||||
|
int64 start = 1;
|
||||||
|
//
|
||||||
|
int64 end = 2;
|
||||||
|
//
|
||||||
|
int32 biz_type = 3;
|
||||||
|
//
|
||||||
|
ClickButtonV2 click_button = 4;
|
||||||
|
//
|
||||||
|
TextInputV2 text_input = 5;
|
||||||
|
//
|
||||||
|
CheckBoxV2 check_box = 6;
|
||||||
|
//
|
||||||
|
ToastV2 toast = 7;
|
||||||
|
//
|
||||||
|
BubbleV2 bubble = 8;
|
||||||
|
//
|
||||||
|
LabelV2 label = 9;
|
||||||
|
//
|
||||||
|
int32 post_status = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum PostStatus {
|
||||||
|
PostStatusNormal = 0; //
|
||||||
|
PostStatusClosed = 1; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum RenderType {
|
||||||
|
RenderTypeNone = 0; //
|
||||||
|
RenderTypeSingle = 1; //
|
||||||
|
RenderTypeRotation = 2; //
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改弹幕配置-响应
|
||||||
|
message Response {
|
||||||
|
//
|
||||||
|
int32 code = 1;
|
||||||
|
//
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum SubtitleAiStatus {
|
||||||
|
None = 0; //
|
||||||
|
Exposure = 1; //
|
||||||
|
Assist = 2; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum SubtitleAiType {
|
||||||
|
Normal = 0; //
|
||||||
|
Translate = 1; //
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个字幕信息
|
||||||
|
message SubtitleItem {
|
||||||
|
// 字幕id
|
||||||
|
int64 id = 1;
|
||||||
|
// 字幕id str
|
||||||
|
string id_str = 2;
|
||||||
|
// 字幕语言代码
|
||||||
|
string lan = 3;
|
||||||
|
// 字幕语言
|
||||||
|
string lan_doc = 4;
|
||||||
|
// 字幕文件url
|
||||||
|
string subtitle_url = 5;
|
||||||
|
// 字幕作者信息
|
||||||
|
UserInfo author = 6;
|
||||||
|
// 字幕类型
|
||||||
|
SubtitleType type = 7;
|
||||||
|
//
|
||||||
|
string lan_doc_brief = 8;
|
||||||
|
//
|
||||||
|
SubtitleAiType ai_type = 9;
|
||||||
|
//
|
||||||
|
SubtitleAiStatus ai_status = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SubtitleType {
|
||||||
|
CC = 0; // CC字幕
|
||||||
|
AI = 1; // AI生成字幕
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message TextInput {
|
||||||
|
//
|
||||||
|
repeated string portrait_placeholder = 1;
|
||||||
|
//
|
||||||
|
repeated string landscape_placeholder = 2;
|
||||||
|
//
|
||||||
|
RenderType render_type = 3;
|
||||||
|
//
|
||||||
|
bool placeholder_post = 4;
|
||||||
|
//
|
||||||
|
bool show = 5;
|
||||||
|
//
|
||||||
|
repeated Avatar avatar = 6;
|
||||||
|
//
|
||||||
|
PostStatus post_status = 7;
|
||||||
|
//
|
||||||
|
Label label = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message TextInputV2 {
|
||||||
|
//
|
||||||
|
repeated string portrait_placeholder = 1;
|
||||||
|
//
|
||||||
|
repeated string landscape_placeholder = 2;
|
||||||
|
//
|
||||||
|
RenderType render_type = 3;
|
||||||
|
//
|
||||||
|
bool placeholder_post = 4;
|
||||||
|
//
|
||||||
|
repeated Avatar avatar = 5;
|
||||||
|
//
|
||||||
|
int32 text_input_limit = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message Toast {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
int32 duration = 2;
|
||||||
|
//
|
||||||
|
bool show = 3;
|
||||||
|
//
|
||||||
|
Button button = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message ToastButtonV2 {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
int32 action = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
enum ToastFunctionType {
|
||||||
|
ToastFunctionTypeNone = 0; //
|
||||||
|
ToastFunctionTypePostPanel = 1; //
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
message ToastV2 {
|
||||||
|
//
|
||||||
|
string text = 1;
|
||||||
|
//
|
||||||
|
int32 duration = 2;
|
||||||
|
//
|
||||||
|
ToastButtonV2 toast_button_v2 = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字幕作者信息
|
||||||
|
message UserInfo {
|
||||||
|
// 用户mid
|
||||||
|
int64 mid = 1;
|
||||||
|
// 用户昵称
|
||||||
|
string name = 2;
|
||||||
|
// 用户性别
|
||||||
|
string sex = 3;
|
||||||
|
// 用户头像url
|
||||||
|
string face = 4;
|
||||||
|
// 用户签名
|
||||||
|
string sign = 5;
|
||||||
|
// 用户等级
|
||||||
|
int32 rank = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能防挡弹幕蒙版信息
|
||||||
|
message VideoMask {
|
||||||
|
// 视频cid
|
||||||
|
int64 cid = 1;
|
||||||
|
// 平台
|
||||||
|
// 0:web端 1:客户端
|
||||||
|
int32 plat = 2;
|
||||||
|
// 帧率
|
||||||
|
int32 fps = 3;
|
||||||
|
// 间隔时间
|
||||||
|
int64 time = 4;
|
||||||
|
// 蒙版url
|
||||||
|
string mask_url = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视频字幕信息
|
||||||
|
message VideoSubtitle {
|
||||||
|
// 视频原语言代码
|
||||||
|
string lan = 1;
|
||||||
|
// 视频原语言
|
||||||
|
string lanDoc = 2;
|
||||||
|
// 视频字幕列表
|
||||||
|
repeated SubtitleItem subtitles = 3;
|
||||||
|
}
|
||||||
@ -244,7 +244,9 @@ class Vote {
|
|||||||
choiceCnt = json['choice_cnt'];
|
choiceCnt = json['choice_cnt'];
|
||||||
share = json['share'];
|
share = json['share'];
|
||||||
defaultShare = json['default_share'];
|
defaultShare = json['default_share'];
|
||||||
endTime = json['end_time'];
|
endTime = json['end_time'] is int
|
||||||
|
? json['end_time']
|
||||||
|
: int.parse(json['end_time']);
|
||||||
joinNum = json['join_num'];
|
joinNum = json['join_num'];
|
||||||
status = json['status'];
|
status = json['status'];
|
||||||
type = json['type'];
|
type = json['type'];
|
||||||
@ -360,7 +362,7 @@ class GoodItem {
|
|||||||
|
|
||||||
String? brief;
|
String? brief;
|
||||||
String? cover;
|
String? cover;
|
||||||
String? id;
|
dynamic id;
|
||||||
String? jumpDesc;
|
String? jumpDesc;
|
||||||
String? jumpUrl;
|
String? jumpUrl;
|
||||||
String? name;
|
String? name;
|
||||||
@ -408,6 +410,7 @@ class DynamicMajorModel {
|
|||||||
this.live,
|
this.live,
|
||||||
this.none,
|
this.none,
|
||||||
this.type,
|
this.type,
|
||||||
|
this.courses,
|
||||||
});
|
});
|
||||||
|
|
||||||
DynamicArchiveModel? archive;
|
DynamicArchiveModel? archive;
|
||||||
@ -422,6 +425,7 @@ class DynamicMajorModel {
|
|||||||
// MAJOR_TYPE_ARCHIVE 视频
|
// MAJOR_TYPE_ARCHIVE 视频
|
||||||
// MAJOR_TYPE_OPUS 图文/文章
|
// MAJOR_TYPE_OPUS 图文/文章
|
||||||
String? type;
|
String? type;
|
||||||
|
Map? courses;
|
||||||
|
|
||||||
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
|
DynamicMajorModel.fromJson(Map<String, dynamic> json) {
|
||||||
archive = json['archive'] != null
|
archive = json['archive'] != null
|
||||||
@ -444,6 +448,7 @@ class DynamicMajorModel {
|
|||||||
none =
|
none =
|
||||||
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
|
json['none'] != null ? DynamicNoneModel.fromJson(json['none']) : null;
|
||||||
type = json['type'];
|
type = json['type'];
|
||||||
|
courses = json['courses'] ?? {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ class FollowDataModel {
|
|||||||
List<FollowItemModel>? list;
|
List<FollowItemModel>? list;
|
||||||
|
|
||||||
FollowDataModel.fromJson(Map<String, dynamic> json) {
|
FollowDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
total = json['total'];
|
total = json['total'] ?? 0;
|
||||||
list = json['list']
|
list = json['list']
|
||||||
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
|
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
|
||||||
.toList();
|
.toList();
|
||||||
@ -19,7 +19,7 @@ class FollowItemModel {
|
|||||||
FollowItemModel({
|
FollowItemModel({
|
||||||
this.mid,
|
this.mid,
|
||||||
this.attribute,
|
this.attribute,
|
||||||
this.mtime,
|
// this.mtime,
|
||||||
this.tag,
|
this.tag,
|
||||||
this.special,
|
this.special,
|
||||||
this.uname,
|
this.uname,
|
||||||
@ -30,7 +30,7 @@ class FollowItemModel {
|
|||||||
|
|
||||||
int? mid;
|
int? mid;
|
||||||
int? attribute;
|
int? attribute;
|
||||||
int? mtime;
|
// int? mtime;
|
||||||
List? tag;
|
List? tag;
|
||||||
int? special;
|
int? special;
|
||||||
String? uname;
|
String? uname;
|
||||||
@ -41,7 +41,7 @@ class FollowItemModel {
|
|||||||
FollowItemModel.fromJson(Map<String, dynamic> json) {
|
FollowItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
mid = json['mid'];
|
mid = json['mid'];
|
||||||
attribute = json['attribute'];
|
attribute = json['attribute'];
|
||||||
mtime = json['mtime'];
|
// mtime = json['mtime'];
|
||||||
tag = json['tag'];
|
tag = json['tag'];
|
||||||
special = json['special'];
|
special = json['special'];
|
||||||
uname = json['uname'];
|
uname = json['uname'];
|
||||||
|
|||||||
49
lib/models/login/index.dart
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
class CaptchaDataModel {
|
||||||
|
CaptchaDataModel({
|
||||||
|
this.type,
|
||||||
|
this.token,
|
||||||
|
this.geetest,
|
||||||
|
this.tencent,
|
||||||
|
this.validate,
|
||||||
|
this.seccode,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? type;
|
||||||
|
String? token;
|
||||||
|
GeetestData? geetest;
|
||||||
|
Tencent? tencent;
|
||||||
|
String? validate;
|
||||||
|
String? seccode;
|
||||||
|
|
||||||
|
CaptchaDataModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
type = json["type"];
|
||||||
|
token = json["token"];
|
||||||
|
geetest =
|
||||||
|
json["geetest"] != null ? GeetestData.fromJson(json["geetest"]) : null;
|
||||||
|
tencent =
|
||||||
|
json["tencent"] != null ? Tencent.fromJson(json["tencent"]) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeetestData {
|
||||||
|
GeetestData({
|
||||||
|
this.challenge,
|
||||||
|
this.gt,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? challenge;
|
||||||
|
String? gt;
|
||||||
|
|
||||||
|
GeetestData.fromJson(Map<String, dynamic> json) {
|
||||||
|
challenge = json["challenge"];
|
||||||
|
gt = json["gt"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Tencent {
|
||||||
|
Tencent({this.appid});
|
||||||
|
String? appid;
|
||||||
|
Tencent.fromJson(Map<String, dynamic> json) {
|
||||||
|
appid = json["appid"];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
lib/models/member/tags.dart
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
class MemberTagItemModel {
|
||||||
|
MemberTagItemModel({
|
||||||
|
this.count,
|
||||||
|
this.name,
|
||||||
|
this.tagid,
|
||||||
|
this.tip,
|
||||||
|
this.checked,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? count;
|
||||||
|
String? name;
|
||||||
|
int? tagid;
|
||||||
|
String? tip;
|
||||||
|
bool? checked;
|
||||||
|
|
||||||
|
MemberTagItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
count = json['count'];
|
||||||
|
name = json['name'];
|
||||||
|
tagid = json['tagid'];
|
||||||
|
tip = json['tip'];
|
||||||
|
checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ class SearchVideoModel {
|
|||||||
List<SearchVideoItemModel>? list;
|
List<SearchVideoItemModel>? list;
|
||||||
SearchVideoModel.fromJson(Map<String, dynamic> json) {
|
SearchVideoModel.fromJson(Map<String, dynamic> json) {
|
||||||
list = json['result']
|
list = json['result']
|
||||||
|
.where((e) => e['available'] == true)
|
||||||
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
|
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
@ -17,7 +18,7 @@ class SearchVideoItemModel {
|
|||||||
this.id,
|
this.id,
|
||||||
this.cid,
|
this.cid,
|
||||||
// this.author,
|
// this.author,
|
||||||
// this.mid,
|
this.mid,
|
||||||
// this.typeid,
|
// this.typeid,
|
||||||
// this.typename,
|
// this.typename,
|
||||||
this.arcurl,
|
this.arcurl,
|
||||||
@ -47,7 +48,7 @@ class SearchVideoItemModel {
|
|||||||
int? id;
|
int? id;
|
||||||
int? cid;
|
int? cid;
|
||||||
// String? author;
|
// String? author;
|
||||||
// String? mid;
|
int? mid;
|
||||||
// String? typeid;
|
// String? typeid;
|
||||||
// String? typename;
|
// String? typename;
|
||||||
String? arcurl;
|
String? arcurl;
|
||||||
@ -80,6 +81,7 @@ class SearchVideoItemModel {
|
|||||||
arcurl = json['arcurl'];
|
arcurl = json['arcurl'];
|
||||||
aid = json['aid'];
|
aid = json['aid'];
|
||||||
bvid = json['bvid'];
|
bvid = json['bvid'];
|
||||||
|
mid = json['mid'];
|
||||||
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
|
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
|
||||||
title = Em.regTitle(json['title']);
|
title = Em.regTitle(json['title']);
|
||||||
description = json['description'];
|
description = json['description'];
|
||||||
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
|
|||||||
indexShow = json['index_show'];
|
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 {
|
class SearchSuggestModel {
|
||||||
SearchSuggestModel({
|
SearchSuggestModel({
|
||||||
this.tag,
|
this.tag,
|
||||||
@ -19,32 +22,74 @@ class SearchSuggestItem {
|
|||||||
SearchSuggestItem({
|
SearchSuggestItem({
|
||||||
this.value,
|
this.value,
|
||||||
this.term,
|
this.term,
|
||||||
this.name,
|
|
||||||
this.spid,
|
this.spid,
|
||||||
|
this.textRich,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? value;
|
String? value;
|
||||||
String? term;
|
String? term;
|
||||||
List? name;
|
|
||||||
int? spid;
|
int? spid;
|
||||||
|
Widget? textRich;
|
||||||
|
|
||||||
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
|
SearchSuggestItem.fromJson(Map<String, dynamic> json, String inputTerm) {
|
||||||
value = json['value'];
|
value = json['value'];
|
||||||
term = json['term'];
|
term = json['term'];
|
||||||
String reg = '<em class="suggest_high_light">$inputTerm</em>';
|
textRich = highlightText(json['name']);
|
||||||
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'];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.cursor,
|
||||||
this.tab,
|
this.tab,
|
||||||
this.list,
|
this.list,
|
||||||
|
this.page,
|
||||||
});
|
});
|
||||||
|
|
||||||
Cursor? cursor;
|
Cursor? cursor;
|
||||||
List<HisTabItem>? tab;
|
List<HisTabItem>? tab;
|
||||||
List<HisListItem>? list;
|
List<HisListItem>? list;
|
||||||
|
Map? page;
|
||||||
|
|
||||||
HistoryData.fromJson(Map<String, dynamic> json) {
|
HistoryData.fromJson(Map<String, dynamic> json) {
|
||||||
cursor = Cursor.fromJson(json['cursor']);
|
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
|
||||||
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList();
|
tab = json['tab'] != null
|
||||||
list =
|
? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()
|
||||||
json['list'].map<HisListItem>((e) => HisListItem.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.kid,
|
||||||
this.tagName,
|
this.tagName,
|
||||||
this.liveStatus,
|
this.liveStatus,
|
||||||
|
this.checked,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? title;
|
String? title;
|
||||||
@ -105,6 +112,7 @@ class HisListItem {
|
|||||||
int? kid;
|
int? kid;
|
||||||
String? tagName;
|
String? tagName;
|
||||||
int? liveStatus;
|
int? liveStatus;
|
||||||
|
bool? checked;
|
||||||
|
|
||||||
HisListItem.fromJson(Map<String, dynamic> json) {
|
HisListItem.fromJson(Map<String, dynamic> json) {
|
||||||
title = json['title'];
|
title = json['title'];
|
||||||
@ -131,6 +139,7 @@ class HisListItem {
|
|||||||
kid = json['kid'];
|
kid = json['kid'];
|
||||||
tagName = json['tag_name'];
|
tagName = json['tag_name'];
|
||||||
liveStatus = json['live_status'];
|
liveStatus = json['live_status'];
|
||||||
|
checked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
80
lib/models/video/ai.dart
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
class AiConclusionModel {
|
||||||
|
AiConclusionModel({
|
||||||
|
this.code,
|
||||||
|
this.modelResult,
|
||||||
|
this.stid,
|
||||||
|
this.status,
|
||||||
|
this.likeNum,
|
||||||
|
this.dislikeNum,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? code;
|
||||||
|
ModelResult? modelResult;
|
||||||
|
String? stid;
|
||||||
|
int? status;
|
||||||
|
int? likeNum;
|
||||||
|
int? dislikeNum;
|
||||||
|
|
||||||
|
AiConclusionModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
code = json['code'];
|
||||||
|
modelResult = ModelResult.fromJson(json['model_result']);
|
||||||
|
stid = json['stid'];
|
||||||
|
status = json['status'];
|
||||||
|
likeNum = json['like_num'];
|
||||||
|
dislikeNum = json['dislike_num'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelResult {
|
||||||
|
ModelResult({
|
||||||
|
this.resultType,
|
||||||
|
this.summary,
|
||||||
|
this.outline,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? resultType;
|
||||||
|
String? summary;
|
||||||
|
List<OutlineItem>? outline;
|
||||||
|
|
||||||
|
ModelResult.fromJson(Map<String, dynamic> json) {
|
||||||
|
resultType = json['result_type'];
|
||||||
|
summary = json['summary'];
|
||||||
|
outline = json['result_type'] == 2
|
||||||
|
? json['outline']
|
||||||
|
.map<OutlineItem>((e) => OutlineItem.fromJson(e))
|
||||||
|
.toList()
|
||||||
|
: <OutlineItem>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OutlineItem {
|
||||||
|
OutlineItem({
|
||||||
|
this.title,
|
||||||
|
this.partOutline,
|
||||||
|
});
|
||||||
|
|
||||||
|
String? title;
|
||||||
|
List<PartOutline>? partOutline;
|
||||||
|
|
||||||
|
OutlineItem.fromJson(Map<String, dynamic> json) {
|
||||||
|
title = json['title'];
|
||||||
|
partOutline = json['part_outline']
|
||||||
|
.map<PartOutline>((e) => PartOutline.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PartOutline {
|
||||||
|
PartOutline({
|
||||||
|
this.timestamp,
|
||||||
|
this.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
int? timestamp;
|
||||||
|
String? content;
|
||||||
|
|
||||||
|
PartOutline.fromJson(Map<String, dynamic> json) {
|
||||||
|
timestamp = json['timestamp'];
|
||||||
|
content = json['content'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -93,26 +93,19 @@ extension AudioQualityDesc on AudioQuality {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum VideoDecodeFormats {
|
enum VideoDecodeFormats {
|
||||||
|
DVH1,
|
||||||
AV1,
|
AV1,
|
||||||
HEVC,
|
HEVC,
|
||||||
AVC,
|
AVC,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VideoDecodeFormatsDesc on VideoDecodeFormats {
|
extension VideoDecodeFormatsDesc on VideoDecodeFormats {
|
||||||
static final List<String> _descList = [
|
static final List<String> _descList = ['DVH1', 'AV1', 'HEVC', 'AVC'];
|
||||||
'AV1',
|
|
||||||
'HEVC',
|
|
||||||
'AVC',
|
|
||||||
];
|
|
||||||
get description => _descList[index];
|
get description => _descList[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VideoDecodeFormatsCode on VideoDecodeFormats {
|
extension VideoDecodeFormatsCode on VideoDecodeFormats {
|
||||||
static final List<String> _codeList = [
|
static final List<String> _codeList = ['dvh1', 'av01', 'hev1', 'avc1'];
|
||||||
'av01',
|
|
||||||
'hev1',
|
|
||||||
'avc1',
|
|
||||||
];
|
|
||||||
get code => _codeList[index];
|
get code => _codeList[index];
|
||||||
|
|
||||||
static VideoDecodeFormats? fromCode(String code) {
|
static VideoDecodeFormats? fromCode(String code) {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:pilipala/models/video/play/quality.dart';
|
import 'package:pilipala/models/video/play/quality.dart';
|
||||||
|
|
||||||
class PlayUrlModel {
|
class PlayUrlModel {
|
||||||
@ -77,8 +79,8 @@ class Dash {
|
|||||||
double? minBufferTime;
|
double? minBufferTime;
|
||||||
List<VideoItem>? video;
|
List<VideoItem>? video;
|
||||||
List<AudioItem>? audio;
|
List<AudioItem>? audio;
|
||||||
Map? dolby;
|
Dolby? dolby;
|
||||||
Map? flac;
|
Flac? flac;
|
||||||
|
|
||||||
Dash.fromJson(Map<String, dynamic> json) {
|
Dash.fromJson(Map<String, dynamic> json) {
|
||||||
duration = json['duration'];
|
duration = json['duration'];
|
||||||
@ -87,8 +89,8 @@ class Dash {
|
|||||||
audio = json['audio'] != null
|
audio = json['audio'] != null
|
||||||
? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()
|
? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()
|
||||||
: [];
|
: [];
|
||||||
dolby = json['dolby'];
|
dolby = json['dolby'] != null ? Dolby.fromJson(json['dolby']) : null;
|
||||||
flac = json['flac'] ?? {};
|
flac = json['flac'] != null ? Flac.fromJson(json['flac']) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +130,8 @@ class VideoItem {
|
|||||||
VideoItem.fromJson(Map<String, dynamic> json) {
|
VideoItem.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
baseUrl = json['baseUrl'];
|
baseUrl = json['baseUrl'];
|
||||||
backupUrl = json['backupUrl'].toList().first;
|
backupUrl =
|
||||||
|
json['backupUrl'] != null ? json['backupUrl'].toList().first : '';
|
||||||
bandWidth = json['bandWidth'];
|
bandWidth = json['bandWidth'];
|
||||||
mimeType = json['mime_type'];
|
mimeType = json['mime_type'];
|
||||||
codecs = json['codecs'];
|
codecs = json['codecs'];
|
||||||
@ -179,7 +182,8 @@ class AudioItem {
|
|||||||
AudioItem.fromJson(Map<String, dynamic> json) {
|
AudioItem.fromJson(Map<String, dynamic> json) {
|
||||||
id = json['id'];
|
id = json['id'];
|
||||||
baseUrl = json['baseUrl'];
|
baseUrl = json['baseUrl'];
|
||||||
backupUrl = json['backupUrl'].toList().first;
|
backupUrl =
|
||||||
|
json['backupUrl'] != null ? json['backupUrl'].toList().first : '';
|
||||||
bandWidth = json['bandWidth'];
|
bandWidth = json['bandWidth'];
|
||||||
mimeType = json['mime_type'];
|
mimeType = json['mime_type'];
|
||||||
codecs = json['codecs'];
|
codecs = json['codecs'];
|
||||||
@ -218,3 +222,33 @@ class FormatItem {
|
|||||||
codecs = json['codecs'];
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -47,6 +47,11 @@ class _AboutPageState extends State<AboutPage> {
|
|||||||
'PiliPala',
|
'PiliPala',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
'使用Flutter开发的哔哩哔哩第三方客户端',
|
||||||
|
style: TextStyle(color: Theme.of(context).colorScheme.outline),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
Obx(
|
Obx(
|
||||||
() => ListTile(
|
() => ListTile(
|
||||||
@ -82,16 +87,6 @@ class _AboutPageState extends State<AboutPage> {
|
|||||||
height: 30,
|
height: 30,
|
||||||
color: Theme.of(context).colorScheme.onInverseSurface,
|
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(
|
ListTile(
|
||||||
onTap: () => _aboutController.githubUrl(),
|
onTap: () => _aboutController.githubUrl(),
|
||||||
title: const Text('Github'),
|
title: const Text('Github'),
|
||||||
@ -100,6 +95,17 @@ class _AboutPageState extends State<AboutPage> {
|
|||||||
style: subTitleStyle,
|
style: subTitleStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
onTap: () => _aboutController.panDownload(),
|
||||||
|
title: const Text('网盘下载'),
|
||||||
|
trailing: Text(
|
||||||
|
'提取码:pili',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.outline,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => _aboutController.feedback(),
|
onTap: () => _aboutController.feedback(),
|
||||||
title: const Text('问题反馈'),
|
title: const Text('问题反馈'),
|
||||||
@ -111,7 +117,7 @@ class _AboutPageState extends State<AboutPage> {
|
|||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () => _aboutController.qqChanel(),
|
onTap: () => _aboutController.qqChanel(),
|
||||||
title: const Text('QQ频道'),
|
title: const Text('QQ群'),
|
||||||
trailing: Icon(
|
trailing: Icon(
|
||||||
Icons.arrow_forward_ios,
|
Icons.arrow_forward_ios,
|
||||||
size: 16,
|
size: 16,
|
||||||
@ -123,6 +129,11 @@ class _AboutPageState extends State<AboutPage> {
|
|||||||
title: const Text('TG频道'),
|
title: const Text('TG频道'),
|
||||||
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
|
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(
|
Divider(
|
||||||
thickness: 8,
|
thickness: 8,
|
||||||
height: 30,
|
height: 30,
|
||||||
@ -141,11 +152,12 @@ class AboutController extends GetxController {
|
|||||||
late LatestDataModel remoteAppInfo;
|
late LatestDataModel remoteAppInfo;
|
||||||
RxBool isUpdate = true.obs;
|
RxBool isUpdate = true.obs;
|
||||||
RxBool isLoading = true.obs;
|
RxBool isLoading = true.obs;
|
||||||
|
late LatestDataModel data;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
init();
|
// init();
|
||||||
// 获取当前版本
|
// 获取当前版本
|
||||||
getCurrentApp();
|
getCurrentApp();
|
||||||
// 获取最新的版本
|
// 获取最新的版本
|
||||||
@ -153,16 +165,16 @@ class AboutController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取设备信息
|
// 获取设备信息
|
||||||
Future init() async {
|
// Future init() async {
|
||||||
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
// DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
|
||||||
if (Platform.isAndroid) {
|
// if (Platform.isAndroid) {
|
||||||
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
// AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
|
||||||
print(androidInfo.supportedAbis);
|
// print(androidInfo.supportedAbis);
|
||||||
} else if (Platform.isIOS) {
|
// } else if (Platform.isIOS) {
|
||||||
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
// IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
|
||||||
print(iosInfo);
|
// print(iosInfo);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 获取当前版本
|
// 获取当前版本
|
||||||
Future getCurrentApp() async {
|
Future getCurrentApp() async {
|
||||||
@ -172,8 +184,8 @@ class AboutController extends GetxController {
|
|||||||
|
|
||||||
// 获取远程版本
|
// 获取远程版本
|
||||||
Future getRemoteApp() async {
|
Future getRemoteApp() async {
|
||||||
var result = await Request().get(Api.latestApp);
|
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
|
||||||
LatestDataModel data = LatestDataModel.fromJson(result.data);
|
data = LatestDataModel.fromJson(result.data);
|
||||||
remoteAppInfo = data;
|
remoteAppInfo = data;
|
||||||
remoteVersion.value = data.tagName!;
|
remoteVersion.value = data.tagName!;
|
||||||
isUpdate.value =
|
isUpdate.value =
|
||||||
@ -183,15 +195,7 @@ class AboutController extends GetxController {
|
|||||||
|
|
||||||
// 跳转下载/本地更新
|
// 跳转下载/本地更新
|
||||||
Future onUpdate() async {
|
Future onUpdate() async {
|
||||||
// final dir = await getApplicationSupportDirectory();
|
Utils.matchVersion(data);
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转github
|
// 跳转github
|
||||||
@ -202,6 +206,14 @@ class AboutController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从网盘下载
|
||||||
|
panDownload() {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 问题反馈
|
// 问题反馈
|
||||||
feedback() {
|
feedback() {
|
||||||
launchUrl(
|
launchUrl(
|
||||||
@ -214,17 +226,9 @@ class AboutController extends GetxController {
|
|||||||
// qq频道
|
// qq频道
|
||||||
qqChanel() {
|
qqChanel() {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
const ClipboardData(text: 'https://pd.qq.com/s/css9rdwga'),
|
const ClipboardData(text: '489981949'),
|
||||||
);
|
|
||||||
SmartDialog.showToast(
|
|
||||||
'已复制,即将在浏览器打开',
|
|
||||||
displayTime: const Duration(milliseconds: 500),
|
|
||||||
).then(
|
|
||||||
(value) => launchUrl(
|
|
||||||
Uri.parse('https://pd.qq.com/s/css9rdwga'),
|
|
||||||
mode: LaunchMode.externalApplication,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
SmartDialog.showToast('已复制QQ群号');
|
||||||
}
|
}
|
||||||
|
|
||||||
// tg频道
|
// tg频道
|
||||||
@ -242,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
lib/pages/audio/controller.dart
Normal file
4
lib/pages/audio/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library pl_audio_player;
|
||||||
|
|
||||||
|
export './view.dart';
|
||||||
|
export './controller.dart';
|
||||||
539
lib/pages/audio/view.dart
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:audio_service/audio_service.dart';
|
||||||
|
import 'package:audio_session/audio_session.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
// import 'package:just_audio_background/just_audio_background.dart';
|
||||||
|
|
||||||
|
class AudioPlayerPage extends StatefulWidget {
|
||||||
|
const AudioPlayerPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AudioPlayerPage> createState() => _AudioPlayerPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioPlayerPageState extends State<AudioPlayerPage> {
|
||||||
|
static int _nextMediaId = 0;
|
||||||
|
late AudioPlayer _player;
|
||||||
|
final _playlist = ConcatenatingAudioSource(children: [
|
||||||
|
ClippingAudioSource(
|
||||||
|
start: const Duration(seconds: 0),
|
||||||
|
end: const Duration(seconds: 90),
|
||||||
|
child: AudioSource.uri(Uri.parse(
|
||||||
|
"https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3")),
|
||||||
|
tag: MediaItem(
|
||||||
|
id: '${_nextMediaId++}',
|
||||||
|
album: "Science Friday",
|
||||||
|
title: "A Salute To Head-Scratching Science (30 seconds)",
|
||||||
|
artUri: Uri.parse(
|
||||||
|
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// AudioSource.uri(
|
||||||
|
// Uri.parse(
|
||||||
|
// "https://upos-sz-mirror08c.bilivideo.com/upgcxcode/05/52/1205825205/1205825205-1-16.mp4?e=ig8euxZM2rNcNbRVhwdVhwdlhWdVhwdVhoNvNC8BqJIzNbfq9rVEuxTEnE8L5F6VnEsSTx0vkX8fqJeYTj_lta53NCM=&uipk=5&nbs=1&deadline=1693821903&gen=playurlv2&os=08cbv&oi=1865700872&trid=bfc9c19f85c545dd8f4794ff97f4f57fh&mid=17340771&platform=html5&upsig=9bf98515091bb8a80e1950a03a2a0d68&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,mid,platform&bvc=vod&nettype=0&f=h_0_0&bw=49663&logo=80000000"),
|
||||||
|
// headers: {
|
||||||
|
// 'user-agent':
|
||||||
|
// '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',
|
||||||
|
// 'referer': 'https://www.bilibili.com'
|
||||||
|
// },
|
||||||
|
// tag: MediaItem(
|
||||||
|
// id: '${_nextMediaId++}',
|
||||||
|
// album: "Science Friday",
|
||||||
|
// title: "A Salute To Head-Scratching Science",
|
||||||
|
// artUri: Uri.parse(
|
||||||
|
// "https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
AudioSource.uri(
|
||||||
|
Uri.parse("https://s3.amazonaws.com/scifri-segments/scifri201711241.mp3"),
|
||||||
|
tag: MediaItem(
|
||||||
|
id: '${_nextMediaId++}',
|
||||||
|
album: "Science Friday",
|
||||||
|
title: "From Cat Rheology To Operatic Incompetence",
|
||||||
|
artUri: Uri.parse(
|
||||||
|
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AudioSource.uri(
|
||||||
|
Uri.parse("asset:///audio/nature.mp3"),
|
||||||
|
tag: MediaItem(
|
||||||
|
id: '${_nextMediaId++}',
|
||||||
|
album: "Public Domain",
|
||||||
|
title: "Nature Sounds",
|
||||||
|
artUri: Uri.parse(
|
||||||
|
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_player = AudioPlayer();
|
||||||
|
|
||||||
|
_init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_player.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _init() async {
|
||||||
|
final session = await AudioSession.instance;
|
||||||
|
await session.configure(const AudioSessionConfiguration.speech());
|
||||||
|
// Listen to errors during playback.
|
||||||
|
_player.playbackEventStream.listen((event) {},
|
||||||
|
onError: (Object e, StackTrace stackTrace) {
|
||||||
|
print('A stream error occurred: $e');
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await _player.setAudioSource(_playlist);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
// Catch load errors: 404, invalid url ...
|
||||||
|
print("Error loading playlist: $e");
|
||||||
|
print(stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream<PositionData> get _positionDataStream =>
|
||||||
|
// Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
|
||||||
|
// _player.positionStream,
|
||||||
|
// _player.bufferedPositionStream,
|
||||||
|
// _player.durationStream,
|
||||||
|
// (position, bufferedPosition, duration) => PositionData(
|
||||||
|
// position, bufferedPosition, duration ?? Duration.zero));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: StreamBuilder<SequenceState?>(
|
||||||
|
stream: _player.sequenceStateStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final state = snapshot.data;
|
||||||
|
if (state?.sequence.isEmpty ?? true) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
final metadata = state!.currentSource!.tag as MediaItem;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Center(
|
||||||
|
child: Image.network(metadata.artUri.toString())),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(metadata.album!,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge),
|
||||||
|
Text(metadata.title),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ControlButtons(_player),
|
||||||
|
// StreamBuilder<PositionData>(
|
||||||
|
// stream: _positionDataStream,
|
||||||
|
// builder: (context, snapshot) {
|
||||||
|
// final positionData = snapshot.data;
|
||||||
|
// return SeekBar(
|
||||||
|
// duration: positionData?.duration ?? Duration.zero,
|
||||||
|
// position: positionData?.position ?? Duration.zero,
|
||||||
|
// bufferedPosition:
|
||||||
|
// positionData?.bufferedPosition ?? Duration.zero,
|
||||||
|
// onChangeEnd: (newPosition) {
|
||||||
|
// _player.seek(newPosition);
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
|
const SizedBox(height: 8.0),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
StreamBuilder<LoopMode>(
|
||||||
|
stream: _player.loopModeStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final loopMode = snapshot.data ?? LoopMode.off;
|
||||||
|
const icons = [
|
||||||
|
Icon(Icons.repeat, color: Colors.grey),
|
||||||
|
Icon(Icons.repeat, color: Colors.orange),
|
||||||
|
Icon(Icons.repeat_one, color: Colors.orange),
|
||||||
|
];
|
||||||
|
const cycleModes = [
|
||||||
|
LoopMode.off,
|
||||||
|
LoopMode.all,
|
||||||
|
LoopMode.one,
|
||||||
|
];
|
||||||
|
final index = cycleModes.indexOf(loopMode);
|
||||||
|
return IconButton(
|
||||||
|
icon: icons[index],
|
||||||
|
onPressed: () {
|
||||||
|
_player.setLoopMode(cycleModes[
|
||||||
|
(cycleModes.indexOf(loopMode) + 1) %
|
||||||
|
cycleModes.length]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
"Playlist",
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<bool>(
|
||||||
|
stream: _player.shuffleModeEnabledStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final shuffleModeEnabled = snapshot.data ?? false;
|
||||||
|
return IconButton(
|
||||||
|
icon: shuffleModeEnabled
|
||||||
|
? const Icon(Icons.shuffle, color: Colors.orange)
|
||||||
|
: const Icon(Icons.shuffle, color: Colors.grey),
|
||||||
|
onPressed: () async {
|
||||||
|
final enable = !shuffleModeEnabled;
|
||||||
|
if (enable) {
|
||||||
|
await _player.shuffle();
|
||||||
|
}
|
||||||
|
await _player.setShuffleModeEnabled(enable);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 240.0,
|
||||||
|
child: StreamBuilder<SequenceState?>(
|
||||||
|
stream: _player.sequenceStateStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final state = snapshot.data;
|
||||||
|
final sequence = state?.sequence ?? [];
|
||||||
|
return ReorderableListView(
|
||||||
|
onReorder: (int oldIndex, int newIndex) {
|
||||||
|
if (oldIndex < newIndex) newIndex--;
|
||||||
|
_playlist.move(oldIndex, newIndex);
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < sequence.length; i++)
|
||||||
|
Dismissible(
|
||||||
|
key: ValueKey(sequence[i]),
|
||||||
|
background: Container(
|
||||||
|
color: Colors.redAccent,
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.only(right: 8.0),
|
||||||
|
child: Icon(Icons.delete, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDismissed: (dismissDirection) {
|
||||||
|
_playlist.removeAt(i);
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
color: i == state!.currentIndex
|
||||||
|
? Colors.grey.shade300
|
||||||
|
: null,
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(sequence[i].tag.title as String),
|
||||||
|
onTap: () {
|
||||||
|
_player.seek(Duration.zero, index: i);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControlButtons extends StatelessWidget {
|
||||||
|
final AudioPlayer player;
|
||||||
|
|
||||||
|
const ControlButtons(this.player, {Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.volume_up),
|
||||||
|
onPressed: () {
|
||||||
|
showSliderDialog(
|
||||||
|
context: context,
|
||||||
|
title: "Adjust volume",
|
||||||
|
divisions: 10,
|
||||||
|
min: 0.0,
|
||||||
|
max: 1.0,
|
||||||
|
stream: player.volumeStream,
|
||||||
|
onChanged: player.setVolume,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StreamBuilder<SequenceState?>(
|
||||||
|
stream: player.sequenceStateStream,
|
||||||
|
builder: (context, snapshot) => IconButton(
|
||||||
|
icon: const Icon(Icons.skip_previous),
|
||||||
|
onPressed: player.hasPrevious ? player.seekToPrevious : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<PlayerState>(
|
||||||
|
stream: player.playerStateStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final playerState = snapshot.data;
|
||||||
|
final processingState = playerState?.processingState;
|
||||||
|
final playing = playerState?.playing;
|
||||||
|
if (processingState == ProcessingState.loading ||
|
||||||
|
processingState == ProcessingState.buffering) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(8.0),
|
||||||
|
width: 64.0,
|
||||||
|
height: 64.0,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
} else if (playing != true) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.play_arrow),
|
||||||
|
iconSize: 64.0,
|
||||||
|
onPressed: player.play,
|
||||||
|
);
|
||||||
|
} else if (processingState != ProcessingState.completed) {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.pause),
|
||||||
|
iconSize: 64.0,
|
||||||
|
onPressed: player.pause,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.replay),
|
||||||
|
iconSize: 64.0,
|
||||||
|
onPressed: () => player.seek(Duration.zero,
|
||||||
|
index: player.effectiveIndices!.first),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
StreamBuilder<SequenceState?>(
|
||||||
|
stream: player.sequenceStateStream,
|
||||||
|
builder: (context, snapshot) => IconButton(
|
||||||
|
icon: const Icon(Icons.skip_next),
|
||||||
|
onPressed: player.hasNext ? player.seekToNext : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
StreamBuilder<double>(
|
||||||
|
stream: player.speedStream,
|
||||||
|
builder: (context, snapshot) => IconButton(
|
||||||
|
icon: Text("${snapshot.data?.toStringAsFixed(1)}x",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
onPressed: () {
|
||||||
|
showSliderDialog(
|
||||||
|
context: context,
|
||||||
|
title: "Adjust speed",
|
||||||
|
divisions: 10,
|
||||||
|
min: 0.5,
|
||||||
|
max: 1.5,
|
||||||
|
stream: player.speedStream,
|
||||||
|
onChanged: player.setSpeed,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showSliderDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
required String title,
|
||||||
|
required int divisions,
|
||||||
|
required double min,
|
||||||
|
required double max,
|
||||||
|
String valueSuffix = '',
|
||||||
|
required Stream<double> stream,
|
||||||
|
required ValueChanged<double> onChanged,
|
||||||
|
}) {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(title, textAlign: TextAlign.center),
|
||||||
|
content: StreamBuilder<double>(
|
||||||
|
stream: stream,
|
||||||
|
builder: (context, snapshot) => SizedBox(
|
||||||
|
height: 100.0,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text('${snapshot.data?.toStringAsFixed(1)}$valueSuffix',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'Fixed',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 24.0)),
|
||||||
|
Slider(
|
||||||
|
divisions: divisions,
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
value: snapshot.data ?? 1.0,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PositionData {
|
||||||
|
final Duration position;
|
||||||
|
final Duration bufferedPosition;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
PositionData(this.position, this.bufferedPosition, this.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeekBar extends StatefulWidget {
|
||||||
|
final Duration duration;
|
||||||
|
final Duration position;
|
||||||
|
final Duration bufferedPosition;
|
||||||
|
final ValueChanged<Duration>? onChanged;
|
||||||
|
final ValueChanged<Duration>? onChangeEnd;
|
||||||
|
|
||||||
|
const SeekBar({
|
||||||
|
Key? key,
|
||||||
|
required this.duration,
|
||||||
|
required this.position,
|
||||||
|
required this.bufferedPosition,
|
||||||
|
this.onChanged,
|
||||||
|
this.onChangeEnd,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
SeekBarState createState() => SeekBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeekBarState extends State<SeekBar> {
|
||||||
|
double? _dragValue;
|
||||||
|
late SliderThemeData _sliderThemeData;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
|
||||||
|
_sliderThemeData = SliderTheme.of(context).copyWith(
|
||||||
|
trackHeight: 2.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
SliderTheme(
|
||||||
|
data: _sliderThemeData.copyWith(
|
||||||
|
thumbShape: HiddenThumbComponentShape(),
|
||||||
|
activeTrackColor: Colors.blue.shade100,
|
||||||
|
inactiveTrackColor: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
child: ExcludeSemantics(
|
||||||
|
child: Slider(
|
||||||
|
min: 0.0,
|
||||||
|
max: widget.duration.inMilliseconds.toDouble(),
|
||||||
|
value: min(widget.bufferedPosition.inMilliseconds.toDouble(),
|
||||||
|
widget.duration.inMilliseconds.toDouble()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_dragValue = value;
|
||||||
|
});
|
||||||
|
if (widget.onChanged != null) {
|
||||||
|
widget.onChanged!(Duration(milliseconds: value.round()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
if (widget.onChangeEnd != null) {
|
||||||
|
widget.onChangeEnd!(Duration(milliseconds: value.round()));
|
||||||
|
}
|
||||||
|
_dragValue = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliderTheme(
|
||||||
|
data: _sliderThemeData.copyWith(
|
||||||
|
inactiveTrackColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Slider(
|
||||||
|
min: 0.0,
|
||||||
|
max: widget.duration.inMilliseconds.toDouble(),
|
||||||
|
value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(),
|
||||||
|
widget.duration.inMilliseconds.toDouble()),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_dragValue = value;
|
||||||
|
});
|
||||||
|
if (widget.onChanged != null) {
|
||||||
|
widget.onChanged!(Duration(milliseconds: value.round()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
if (widget.onChangeEnd != null) {
|
||||||
|
widget.onChangeEnd!(Duration(milliseconds: value.round()));
|
||||||
|
}
|
||||||
|
_dragValue = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
right: 16.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
child: Text(
|
||||||
|
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
|
||||||
|
.firstMatch("$_remaining")
|
||||||
|
?.group(1) ??
|
||||||
|
'$_remaining',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration get _remaining => widget.duration - widget.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiddenThumbComponentShape extends SliderComponentShape {
|
||||||
|
@override
|
||||||
|
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(
|
||||||
|
PaintingContext context,
|
||||||
|
Offset center, {
|
||||||
|
required Animation<double> activationAnimation,
|
||||||
|
required Animation<double> enableAnimation,
|
||||||
|
required bool isDiscrete,
|
||||||
|
required TextPainter labelPainter,
|
||||||
|
required RenderBox parentBox,
|
||||||
|
required SliderThemeData sliderTheme,
|
||||||
|
required TextDirection textDirection,
|
||||||
|
required double value,
|
||||||
|
required double textScaleFactor,
|
||||||
|
required Size sizeWithOverflow,
|
||||||
|
}) {}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
|
|||||||
import 'package:pilipala/models/user/fav_folder.dart';
|
import 'package:pilipala/models/user/fav_folder.dart';
|
||||||
import 'package:pilipala/pages/video/detail/index.dart';
|
import 'package:pilipala/pages/video/detail/index.dart';
|
||||||
import 'package:pilipala/pages/video/detail/reply/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/feed_back.dart';
|
||||||
import 'package:pilipala/utils/id_utils.dart';
|
import 'package:pilipala/utils/id_utils.dart';
|
||||||
import 'package:pilipala/utils/storage.dart';
|
import 'package:pilipala/utils/storage.dart';
|
||||||
@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
|
|||||||
? int.parse(Get.parameters['seasonId']!)
|
? int.parse(Get.parameters['seasonId']!)
|
||||||
: null;
|
: null;
|
||||||
var epId = Get.parameters['epId'] != null
|
var epId = Get.parameters['epId'] != null
|
||||||
? int.parse(Get.parameters['epId']!)
|
? int.tryParse(Get.parameters['epId']!)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// 是否预渲染 骨架屏
|
// 是否预渲染 骨架屏
|
||||||
@ -257,7 +258,8 @@ class BangumiIntroController extends GetxController {
|
|||||||
VideoDetailController videoDetailCtr =
|
VideoDetailController videoDetailCtr =
|
||||||
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
|
||||||
videoDetailCtr.bvid = bvid;
|
videoDetailCtr.bvid = bvid;
|
||||||
videoDetailCtr.cid = cid;
|
videoDetailCtr.cid.value = cid;
|
||||||
|
videoDetailCtr.danmakuCid.value = cid;
|
||||||
videoDetailCtr.queryVideoUrl();
|
videoDetailCtr.queryVideoUrl();
|
||||||
// 重新请求评论
|
// 重新请求评论
|
||||||
try {
|
try {
|
||||||
@ -291,4 +293,31 @@ class BangumiIntroController extends GetxController {
|
|||||||
}
|
}
|
||||||
return result;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
|
|||||||
|
|
||||||
class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
final BangumiIntroController bangumiIntroController =
|
late BangumiIntroController bangumiIntroController;
|
||||||
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
|
late VideoDetailController videoDetailCtr;
|
||||||
BangumiInfoModel? bangumiDetail;
|
BangumiInfoModel? bangumiDetail;
|
||||||
late Future _futureBuilderFuture;
|
late Future _futureBuilderFuture;
|
||||||
|
late int cid;
|
||||||
|
late String heroTag;
|
||||||
|
|
||||||
// 添加页面缓存
|
// 添加页面缓存
|
||||||
@override
|
@override
|
||||||
@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
heroTag = Get.arguments['heroTag'];
|
||||||
|
cid = widget.cid!;
|
||||||
|
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
|
||||||
|
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||||
bangumiIntroController.bangumiDetail.listen((value) {
|
bangumiIntroController.bangumiDetail.listen((value) {
|
||||||
bangumiDetail = value;
|
bangumiDetail = value;
|
||||||
});
|
});
|
||||||
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
|
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
|
||||||
|
videoDetailCtr.cid.listen((p0) {
|
||||||
|
print('🐶🐶$p0');
|
||||||
|
cid = p0;
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -61,22 +72,25 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
|
|||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (snapshot.connectionState == ConnectionState.done) {
|
||||||
if (snapshot.data['status']) {
|
if (snapshot.data['status']) {
|
||||||
// 请求成功
|
// 请求成功
|
||||||
|
|
||||||
return BangumiInfo(
|
return BangumiInfo(
|
||||||
loadingStatus: false,
|
loadingStatus: false,
|
||||||
bangumiDetail: bangumiDetail,
|
bangumiDetail: bangumiDetail,
|
||||||
|
cid: cid,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// 请求错误
|
// 请求错误
|
||||||
return HttpError(
|
// return HttpError(
|
||||||
errMsg: snapshot.data['msg'],
|
// errMsg: snapshot.data['msg'],
|
||||||
fn: () => Get.back(),
|
// fn: () => Get.back(),
|
||||||
);
|
// );
|
||||||
|
return SizedBox();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return BangumiInfo(
|
return BangumiInfo(
|
||||||
loadingStatus: true,
|
loadingStatus: true,
|
||||||
bangumiDetail: bangumiDetail,
|
bangumiDetail: bangumiDetail,
|
||||||
cid: widget.cid,
|
cid: cid,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -117,6 +131,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
|||||||
bangumiItem = bangumiIntroController.bangumiItem;
|
bangumiItem = bangumiIntroController.bangumiItem;
|
||||||
sheetHeight = localCache.get('sheetHeight');
|
sheetHeight = localCache.get('sheetHeight');
|
||||||
cid = widget.cid!;
|
cid = widget.cid!;
|
||||||
|
print('cid: $cid');
|
||||||
|
videoDetailCtr.cid.listen((p0) {
|
||||||
|
cid = p0;
|
||||||
|
print('cid: $cid');
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收藏
|
// 收藏
|
||||||
@ -260,9 +280,15 @@ class _BangumiInfoState extends State<BangumiInfo> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
!widget.loadingStatus
|
!widget.loadingStatus
|
||||||
? widget.bangumiDetail!.areas!
|
? (widget.bangumiDetail!.areas!
|
||||||
.first['name']
|
.isNotEmpty
|
||||||
: bangumiItem!.areas!.first['name'],
|
? widget.bangumiDetail!.areas!
|
||||||
|
.first['name']
|
||||||
|
: '')
|
||||||
|
: (bangumiItem!.areas!.isNotEmpty
|
||||||
|
? bangumiItem!
|
||||||
|
.areas!.first['name']
|
||||||
|
: ''),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: t.colorScheme.outline,
|
color: t.colorScheme.outline,
|
||||||
|
|||||||
@ -113,6 +113,9 @@ class _BangumiPageState extends State<BangumiPage>
|
|||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState ==
|
if (snapshot.connectionState ==
|
||||||
ConnectionState.done) {
|
ConnectionState.done) {
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
Map data = snapshot.data as Map;
|
Map data = snapshot.data as Map;
|
||||||
List list = _bangumidController.bangumiFollowList;
|
List list = _bangumidController.bangumiFollowList;
|
||||||
if (data['status']) {
|
if (data['status']) {
|
||||||
@ -198,7 +201,7 @@ class _BangumiPageState extends State<BangumiPage>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const LoadingMore()
|
LoadingMore()
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -213,7 +216,8 @@ class _BangumiPageState extends State<BangumiPage>
|
|||||||
crossAxisSpacing: StyleString.cardSpace,
|
crossAxisSpacing: StyleString.cardSpace,
|
||||||
// 列数
|
// 列数
|
||||||
crossAxisCount: 3,
|
crossAxisCount: 3,
|
||||||
mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
|
mainAxisExtent: Get.size.width / 3 / 0.65 +
|
||||||
|
32 * MediaQuery.of(context).textScaleFactor,
|
||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
(BuildContext context, int index) {
|
(BuildContext context, int index) {
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/models/bangumi/info.dart';
|
import 'package:pilipala/models/bangumi/info.dart';
|
||||||
|
import 'package:pilipala/pages/video/detail/index.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
class BangumiPanel extends StatefulWidget {
|
class BangumiPanel extends StatefulWidget {
|
||||||
final List<EpisodeItem> pages;
|
final List<EpisodeItem> pages;
|
||||||
@ -24,12 +28,32 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
|||||||
late int currentIndex;
|
late int currentIndex;
|
||||||
final ScrollController listViewScrollCtr = ScrollController();
|
final ScrollController listViewScrollCtr = ScrollController();
|
||||||
final ScrollController listViewScrollCtr_2 = ScrollController();
|
final ScrollController listViewScrollCtr_2 = ScrollController();
|
||||||
|
Box userInfoCache = GStrorage.userInfo;
|
||||||
|
dynamic userInfo;
|
||||||
|
// 默认未开通
|
||||||
|
int vipStatus = 0;
|
||||||
|
late int cid;
|
||||||
|
String heroTag = Get.arguments['heroTag'];
|
||||||
|
late final VideoDetailController videoDetailCtr;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
|
cid = widget.cid!;
|
||||||
|
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||||
scrollToIndex();
|
scrollToIndex();
|
||||||
|
userInfo = userInfoCache.get('userInfoCache');
|
||||||
|
if (userInfo != null) {
|
||||||
|
vipStatus = userInfo.vipStatus;
|
||||||
|
}
|
||||||
|
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
|
||||||
|
|
||||||
|
videoDetailCtr.cid.listen((p0) {
|
||||||
|
cid = p0;
|
||||||
|
setState(() {});
|
||||||
|
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
|
||||||
|
scrollToIndex();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -126,7 +150,7 @@ class _BangumiPanelState extends State<BangumiPanel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void changeFucCall(item, i) async {
|
void changeFucCall(item, i) async {
|
||||||
if (item.badge != null) {
|
if (item.badge != null && vipStatus != 1) {
|
||||||
SmartDialog.showToast('需要大会员');
|
SmartDialog.showToast('需要大会员');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,9 +29,6 @@ class BangumiCardV extends StatelessWidget {
|
|||||||
return Card(
|
return Card(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: StyleString.mdRadius,
|
|
||||||
),
|
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
// onLongPress: () {
|
// onLongPress: () {
|
||||||
@ -149,7 +146,6 @@ class BangumiContent extends StatelessWidget {
|
|||||||
bangumiItem.title,
|
bangumiItem.title,
|
||||||
textAlign: TextAlign.start,
|
textAlign: TextAlign.start,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.3,
|
||||||
),
|
),
|
||||||
@ -158,6 +154,7 @@ class BangumiContent extends StatelessWidget {
|
|||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 1),
|
||||||
if (bangumiItem.indexShow != null)
|
if (bangumiItem.indexShow != null)
|
||||||
Text(
|
Text(
|
||||||
bangumiItem.indexShow,
|
bangumiItem.indexShow,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:hive/hive.dart';
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/common/widgets/http_error.dart';
|
import 'package:pilipala/common/widgets/http_error.dart';
|
||||||
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
title: Obx(
|
title: Obx(
|
||||||
() => Text(
|
() => Text(
|
||||||
'黑名单管理 (${_blackListController.blackList.length} / 5000)',
|
'黑名单管理 - ${_blackListController.total.value}',
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
dense: true,
|
dense: true,
|
||||||
// trailing: TextButton(
|
trailing: TextButton(
|
||||||
// onPressed: () {},
|
onPressed: () => _blackListController
|
||||||
// child: const Text('移除'),
|
.removeBlack(list[index].mid),
|
||||||
// ),
|
child: const Text('移除'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
|
|||||||
class BlackListController extends GetxController {
|
class BlackListController extends GetxController {
|
||||||
int currentPage = 1;
|
int currentPage = 1;
|
||||||
int pageSize = 50;
|
int pageSize = 50;
|
||||||
|
RxInt total = 0.obs;
|
||||||
RxList<BlackListItem> blackList = [BlackListItem()].obs;
|
RxList<BlackListItem> blackList = [BlackListItem()].obs;
|
||||||
|
|
||||||
Future queryBlacklist({type = 'init'}) async {
|
Future queryBlacklist({type = 'init'}) async {
|
||||||
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
|
|||||||
if (result['status']) {
|
if (result['status']) {
|
||||||
if (type == 'init') {
|
if (type == 'init') {
|
||||||
blackList.value = result['data'].list;
|
blackList.value = result['data'].list;
|
||||||
|
total.value = result['data'].total;
|
||||||
} else {
|
} else {
|
||||||
blackList.addAll(result['data'].list);
|
blackList.addAll(result['data'].list);
|
||||||
}
|
}
|
||||||
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
|
|||||||
}
|
}
|
||||||
return result;
|
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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
lib/pages/danmaku/controller.dart
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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 = [];
|
||||||
|
// 已请求的段落标记
|
||||||
|
List<int> hasrequestSeg = [];
|
||||||
|
int currentSegIndex = 1;
|
||||||
|
int currentDmIndex = 0;
|
||||||
|
|
||||||
|
void calcSegment() {
|
||||||
|
dmSegList.clear();
|
||||||
|
// 视频分段数
|
||||||
|
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
|
||||||
|
dmSegList = List<DmSegMobileReply>.generate(
|
||||||
|
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
|
||||||
|
// 当前分段
|
||||||
|
try {
|
||||||
|
currentSegIndex =
|
||||||
|
(playerController.position.value.inSeconds / (60 * 6)).ceil();
|
||||||
|
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<DmSegMobileReply>> queryDanmaku() async {
|
||||||
|
// dmSegList.clear();
|
||||||
|
DmSegMobileReply result =
|
||||||
|
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex);
|
||||||
|
if (result.elems.isNotEmpty) {
|
||||||
|
result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
|
||||||
|
// dmSegList.add(result);
|
||||||
|
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
|
||||||
|
dmSegList[currentSegIndex - 1] = result;
|
||||||
|
}
|
||||||
|
if (dmSegList.isNotEmpty) {
|
||||||
|
findClosestPositionIndex(playerController.position.value.inMilliseconds);
|
||||||
|
}
|
||||||
|
return dmSegList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 查询当前最接近的弹幕
|
||||||
|
void findClosestPositionIndex(int position) {
|
||||||
|
int segIndex = (position / (6 * 60 * 1000)).ceil() - 1;
|
||||||
|
if (segIndex < 0) segIndex = 0;
|
||||||
|
List elems = dmSegList[segIndex].elems;
|
||||||
|
|
||||||
|
if (segIndex < dmSegList.length) {
|
||||||
|
int left = 0;
|
||||||
|
int right = elems.length;
|
||||||
|
|
||||||
|
while (left < right) {
|
||||||
|
int mid = (right + left) ~/ 2;
|
||||||
|
var midPosition = elems[mid].progress;
|
||||||
|
|
||||||
|
if (midPosition >= position) {
|
||||||
|
right = mid;
|
||||||
|
} else {
|
||||||
|
left = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSegIndex = segIndex;
|
||||||
|
currentDmIndex = right;
|
||||||
|
} else {
|
||||||
|
currentSegIndex = segIndex;
|
||||||
|
currentDmIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
lib/pages/danmaku/index.dart
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
library pldanmaku;
|
||||||
|
|
||||||
|
export './controller.dart';
|
||||||
|
export 'view.dart';
|
||||||
180
lib/pages/danmaku/view.dart
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
|
import 'package:ns_danmaku/ns_danmaku.dart';
|
||||||
|
import 'package:pilipala/pages/danmaku/index.dart';
|
||||||
|
import 'package:pilipala/plugin/pl_player/index.dart';
|
||||||
|
import 'package:pilipala/utils/danmaku.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
|
/// 传入播放器控制器,监听播放进度,加载对应弹幕
|
||||||
|
class PlDanmaku extends StatefulWidget {
|
||||||
|
final int cid;
|
||||||
|
final PlPlayerController playerController;
|
||||||
|
|
||||||
|
const PlDanmaku({
|
||||||
|
super.key,
|
||||||
|
required this.cid,
|
||||||
|
required this.playerController,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlDanmaku> createState() => _PlDanmakuState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlDanmakuState extends State<PlDanmaku> {
|
||||||
|
late PlPlayerController playerController;
|
||||||
|
late PlDanmakuController _plDanmakuController;
|
||||||
|
DanmakuController? _controller;
|
||||||
|
bool danmuPlayStatus = true;
|
||||||
|
Box setting = GStrorage.setting;
|
||||||
|
late bool enableShowDanmaku;
|
||||||
|
late List blockTypes;
|
||||||
|
late double showArea;
|
||||||
|
late double opacityVal;
|
||||||
|
late double fontSizeVal;
|
||||||
|
late double danmakuSpeedVal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
enableShowDanmaku =
|
||||||
|
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
|
||||||
|
_plDanmakuController =
|
||||||
|
PlDanmakuController(widget.cid, widget.playerController);
|
||||||
|
if (mounted) {
|
||||||
|
playerController = widget.playerController;
|
||||||
|
_plDanmakuController.videoDuration = playerController.duration.value;
|
||||||
|
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
|
||||||
|
_plDanmakuController
|
||||||
|
..calcSegment()
|
||||||
|
..queryDanmaku();
|
||||||
|
}
|
||||||
|
playerController
|
||||||
|
..addStatusLister(playerListener)
|
||||||
|
..addPositionListener(videoPositionListen);
|
||||||
|
}
|
||||||
|
playerController.isOpenDanmu.listen((p0) {
|
||||||
|
if (p0) {
|
||||||
|
if (_plDanmakuController.dmSegList.isEmpty) {
|
||||||
|
_plDanmakuController
|
||||||
|
..calcSegment()
|
||||||
|
..queryDanmaku();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
blockTypes = playerController.blockTypes;
|
||||||
|
showArea = playerController.showArea;
|
||||||
|
opacityVal = playerController.opacityVal;
|
||||||
|
fontSizeVal = playerController.fontSizeVal;
|
||||||
|
danmakuSpeedVal = playerController.danmakuSpeedVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放器状态监听
|
||||||
|
void playerListener(PlayerStatus? status) {
|
||||||
|
if (status == PlayerStatus.paused) {
|
||||||
|
_controller!.pause();
|
||||||
|
}
|
||||||
|
if (status == PlayerStatus.playing) {
|
||||||
|
_controller!.onResume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoPositionListen(Duration position) {
|
||||||
|
if (!danmuPlayStatus) {
|
||||||
|
_controller!.onResume();
|
||||||
|
danmuPlayStatus = true;
|
||||||
|
}
|
||||||
|
if (!playerController.isOpenDanmu.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PlDanmakuController ctr = _plDanmakuController;
|
||||||
|
int currentPosition = position.inMilliseconds;
|
||||||
|
blockTypes = playerController.blockTypes;
|
||||||
|
// 根据position判断是否有已缓存弹幕。没有则请求对应段
|
||||||
|
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
|
||||||
|
segIndex = segIndex < 1 ? 1 : segIndex;
|
||||||
|
if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
|
||||||
|
!ctr.hasrequestSeg.contains(segIndex - 1)) {
|
||||||
|
ctr.hasrequestSeg.add(segIndex - 1);
|
||||||
|
ctr.currentSegIndex = segIndex;
|
||||||
|
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
|
||||||
|
ctr.queryDanmaku();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 超出分段数返回
|
||||||
|
if (ctr.currentSegIndex >= ctr.dmSegList.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctr.dmSegList.isEmpty ||
|
||||||
|
ctr.dmSegList[ctr.currentSegIndex].elems.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 超出当前分段的弹幕总数返回
|
||||||
|
if (ctr.currentDmIndex >= ctr.dmSegList[ctr.currentSegIndex].elems.length) {
|
||||||
|
ctr.currentDmIndex = 0;
|
||||||
|
ctr.currentSegIndex++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var element = ctr.dmSegList[ctr.currentSegIndex].elems[ctr.currentDmIndex];
|
||||||
|
var delta = currentPosition - element.progress;
|
||||||
|
|
||||||
|
if (delta >= 0 && delta < 200) {
|
||||||
|
// 屏蔽彩色弹幕
|
||||||
|
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
|
||||||
|
_controller!.addItems([
|
||||||
|
DanmakuItem(
|
||||||
|
element.content,
|
||||||
|
color: DmUtils.decimalToColor(element.color),
|
||||||
|
time: element.progress,
|
||||||
|
type: DmUtils.getPosition(element.mode),
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
ctr.currentDmIndex++;
|
||||||
|
} else {
|
||||||
|
if (!playerController.isOpenDanmu.value) {
|
||||||
|
_controller!.pause();
|
||||||
|
danmuPlayStatus = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctr.findClosestPositionIndex(position.inMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
playerController.removePositionListener(videoPositionListen);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(builder: (context, box) {
|
||||||
|
double initDuration = box.maxWidth / 12;
|
||||||
|
return Obx(
|
||||||
|
() => AnimatedOpacity(
|
||||||
|
opacity: playerController.isOpenDanmu.value ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 100),
|
||||||
|
child: DanmakuView(
|
||||||
|
createdController: (DanmakuController e) async {
|
||||||
|
widget.playerController.danmakuController = _controller = e;
|
||||||
|
},
|
||||||
|
option: DanmakuOption(
|
||||||
|
fontSize: 15 * fontSizeVal,
|
||||||
|
area: showArea,
|
||||||
|
opacity: opacityVal,
|
||||||
|
hideTop: blockTypes.contains(5),
|
||||||
|
hideScroll: blockTypes.contains(2),
|
||||||
|
hideBottom: blockTypes.contains(4),
|
||||||
|
duration: initDuration /
|
||||||
|
(danmakuSpeedVal * widget.playerController.playbackSpeed),
|
||||||
|
),
|
||||||
|
statusChanged: (isPlaying) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import 'package:pilipala/models/dynamics/result.dart';
|
|||||||
import 'package:pilipala/models/dynamics/up.dart';
|
import 'package:pilipala/models/dynamics/up.dart';
|
||||||
import 'package:pilipala/models/live/item.dart';
|
import 'package:pilipala/models/live/item.dart';
|
||||||
import 'package:pilipala/utils/feed_back.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/storage.dart';
|
||||||
import 'package:pilipala/utils/utils.dart';
|
import 'package:pilipala/utils/utils.dart';
|
||||||
|
|
||||||
@ -50,17 +51,21 @@ class DynamicsController extends GetxController {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
bool flag = false;
|
bool flag = false;
|
||||||
RxInt initialValue = 1.obs;
|
RxInt initialValue = 0.obs;
|
||||||
Box userInfoCache = GStrorage.userInfo;
|
Box userInfoCache = GStrorage.userInfo;
|
||||||
RxBool userLogin = false.obs;
|
RxBool userLogin = false.obs;
|
||||||
var userInfo;
|
var userInfo;
|
||||||
RxBool isLoadingDynamic = false.obs;
|
RxBool isLoadingDynamic = false.obs;
|
||||||
|
Box setting = GStrorage.setting;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
userInfo = userInfoCache.get('userInfoCache');
|
userInfo = userInfoCache.get('userInfoCache');
|
||||||
userLogin.value = userInfo != null;
|
userLogin.value = userInfo != null;
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
initialValue.value =
|
||||||
|
setting.get(SettingBoxKey.defaultDynamicType, defaultValue: 0);
|
||||||
|
dynamicsType = DynamicsType.values[initialValue.value].obs;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future queryFollowDynamic({type = 'init'}) async {
|
Future queryFollowDynamic({type = 'init'}) async {
|
||||||
@ -70,6 +75,10 @@ class DynamicsController extends GetxController {
|
|||||||
if (type == 'init') {
|
if (type == 'init') {
|
||||||
dynamicsList.clear();
|
dynamicsList.clear();
|
||||||
}
|
}
|
||||||
|
// 下拉刷新数据渲染时会触发onLoad
|
||||||
|
if (type == 'onLoad' && page == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
isLoadingDynamic.value = true;
|
isLoadingDynamic.value = true;
|
||||||
var res = await DynamicsHttp.followDynamic(
|
var res = await DynamicsHttp.followDynamic(
|
||||||
page: type == 'init' ? 1 : page,
|
page: type == 'init' ? 1 : page,
|
||||||
@ -79,6 +88,10 @@ class DynamicsController extends GetxController {
|
|||||||
);
|
);
|
||||||
isLoadingDynamic.value = false;
|
isLoadingDynamic.value = false;
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
|
if (type == 'onLoad' && res['data'].items.isEmpty) {
|
||||||
|
SmartDialog.showToast('没有更多了');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (type == 'init') {
|
if (type == 'init') {
|
||||||
dynamicsList.value = res['data'].items;
|
dynamicsList.value = res['data'].items;
|
||||||
} else {
|
} else {
|
||||||
@ -91,7 +104,7 @@ class DynamicsController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSelectType(value) async {
|
onSelectType(value) async {
|
||||||
dynamicsType.value = filterTypeList[value - 1]['value'];
|
dynamicsType.value = filterTypeList[value]['value'];
|
||||||
dynamicsList.value = [DynamicItemModel()];
|
dynamicsList.value = [DynamicItemModel()];
|
||||||
page = 1;
|
page = 1;
|
||||||
initialValue.value = value;
|
initialValue.value = value;
|
||||||
@ -101,16 +114,21 @@ class DynamicsController extends GetxController {
|
|||||||
|
|
||||||
pushDetail(item, floor, {action = 'all'}) async {
|
pushDetail(item, floor, {action = 'all'}) async {
|
||||||
feedBack();
|
feedBack();
|
||||||
|
|
||||||
|
/// 点击评论action 直接查看评论
|
||||||
if (action == 'comment') {
|
if (action == 'comment') {
|
||||||
Get.toNamed('/dynamicDetail',
|
Get.toNamed('/dynamicDetail',
|
||||||
arguments: {'item': item, 'floor': floor, 'action': action});
|
arguments: {'item': item, 'floor': floor, 'action': action});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
switch (item!.type) {
|
switch (item!.type) {
|
||||||
|
/// 转发的动态
|
||||||
case 'DYNAMIC_TYPE_FORWARD':
|
case 'DYNAMIC_TYPE_FORWARD':
|
||||||
Get.toNamed('/dynamicDetail',
|
Get.toNamed('/dynamicDetail',
|
||||||
arguments: {'item': item, 'floor': floor});
|
arguments: {'item': item, 'floor': floor});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/// 图文动态查看
|
||||||
case 'DYNAMIC_TYPE_DRAW':
|
case 'DYNAMIC_TYPE_DRAW':
|
||||||
Get.toNamed('/dynamicDetail',
|
Get.toNamed('/dynamicDetail',
|
||||||
arguments: {'item': item, 'floor': floor});
|
arguments: {'item': item, 'floor': floor});
|
||||||
@ -126,17 +144,42 @@ class DynamicsController extends GetxController {
|
|||||||
SmartDialog.showToast(err.toString());
|
SmartDialog.showToast(err.toString());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/// 专栏文章查看
|
||||||
case 'DYNAMIC_TYPE_ARTICLE':
|
case 'DYNAMIC_TYPE_ARTICLE':
|
||||||
String title = item.modules.moduleDynamic.major.opus.title;
|
String title = item.modules.moduleDynamic.major.opus.title;
|
||||||
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
|
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
|
||||||
Get.toNamed(
|
if (url.contains('opus') || url.contains('read')) {
|
||||||
'/webview',
|
RegExp digitRegExp = RegExp(r'\d+');
|
||||||
parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title},
|
Iterable<Match> matches = digitRegExp.allMatches(url);
|
||||||
);
|
String number = matches.first.group(0)!;
|
||||||
|
if (url.contains('read')) {
|
||||||
|
number = 'cv$number';
|
||||||
|
}
|
||||||
|
Get.toNamed('/htmlRender', parameters: {
|
||||||
|
'url': url.startsWith('//') ? url.split('//').last : url,
|
||||||
|
'title': title,
|
||||||
|
'id': number,
|
||||||
|
'dynamicType': url.split('//').last.split('/')[1]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Get.toNamed(
|
||||||
|
'/webview',
|
||||||
|
parameters: {
|
||||||
|
'url': 'https:$url',
|
||||||
|
'type': 'note',
|
||||||
|
'pageTitle': title
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'DYNAMIC_TYPE_PGC':
|
case 'DYNAMIC_TYPE_PGC':
|
||||||
print('番剧');
|
print('番剧');
|
||||||
|
SmartDialog.showToast('暂未支持的类型,请联系开发者');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/// 纯文字动态查看
|
||||||
case 'DYNAMIC_TYPE_WORD':
|
case 'DYNAMIC_TYPE_WORD':
|
||||||
print('纯文本');
|
print('纯文本');
|
||||||
Get.toNamed('/dynamicDetail',
|
Get.toNamed('/dynamicDetail',
|
||||||
@ -160,10 +203,19 @@ class DynamicsController extends GetxController {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/// TODO
|
/// 合集查看
|
||||||
case 'DYNAMIC_TYPE_UGC_SEASON':
|
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;
|
break;
|
||||||
|
|
||||||
|
/// 番剧查看
|
||||||
case 'DYNAMIC_TYPE_PGC_UNION':
|
case 'DYNAMIC_TYPE_PGC_UNION':
|
||||||
print('DYNAMIC_TYPE_PGC_UNION 番剧');
|
print('DYNAMIC_TYPE_PGC_UNION 番剧');
|
||||||
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
|
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
|
||||||
@ -219,6 +271,7 @@ class DynamicsController extends GetxController {
|
|||||||
|
|
||||||
onRefresh() async {
|
onRefresh() async {
|
||||||
page = 1;
|
page = 1;
|
||||||
|
print('onRefresh');
|
||||||
await queryFollowUp();
|
await queryFollowUp();
|
||||||
await queryFollowDynamic();
|
await queryFollowDynamic();
|
||||||
}
|
}
|
||||||
@ -238,7 +291,7 @@ class DynamicsController extends GetxController {
|
|||||||
void resetSearch() {
|
void resetSearch() {
|
||||||
mid.value = -1;
|
mid.value = -1;
|
||||||
dynamicsType.value = DynamicsType.values[0];
|
dynamicsType.value = DynamicsType.values[0];
|
||||||
initialValue.value = 1;
|
initialValue.value = 0;
|
||||||
SmartDialog.showToast('还原默认加载');
|
SmartDialog.showToast('还原默认加载');
|
||||||
dynamicsList.value = [DynamicItemModel()];
|
dynamicsList.value = [DynamicItemModel()];
|
||||||
queryFollowDynamic();
|
queryFollowDynamic();
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:hive/hive.dart';
|
||||||
import 'package:pilipala/http/reply.dart';
|
import 'package:pilipala/http/reply.dart';
|
||||||
import 'package:pilipala/models/common/reply_sort_type.dart';
|
import 'package:pilipala/models/common/reply_sort_type.dart';
|
||||||
import 'package:pilipala/models/video/reply/item.dart';
|
import 'package:pilipala/models/video/reply/item.dart';
|
||||||
import 'package:pilipala/utils/feed_back.dart';
|
import 'package:pilipala/utils/feed_back.dart';
|
||||||
|
import 'package:pilipala/utils/storage.dart';
|
||||||
|
|
||||||
class DynamicDetailController extends GetxController {
|
class DynamicDetailController extends GetxController {
|
||||||
DynamicDetailController(this.oid, this.type);
|
DynamicDetailController(this.oid, this.type);
|
||||||
@ -15,10 +18,12 @@ class DynamicDetailController extends GetxController {
|
|||||||
RxString noMore = ''.obs;
|
RxString noMore = ''.obs;
|
||||||
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
|
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
|
||||||
RxInt acount = 0.obs;
|
RxInt acount = 0.obs;
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
|
||||||
ReplySortType sortType = ReplySortType.time;
|
ReplySortType _sortType = ReplySortType.time;
|
||||||
RxString sortTypeTitle = ReplySortType.time.titles.obs;
|
RxString sortTypeTitle = ReplySortType.time.titles.obs;
|
||||||
RxString sortTypeLabel = ReplySortType.time.labels.obs;
|
RxString sortTypeLabel = ReplySortType.time.labels.obs;
|
||||||
|
Box setting = GStrorage.setting;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
@ -29,6 +34,11 @@ class DynamicDetailController extends GetxController {
|
|||||||
acount.value =
|
acount.value =
|
||||||
int.parse(item!.modules!.moduleStat!.comment!.count ?? '0');
|
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 {
|
Future queryReplyList({reqType = 'init'}) async {
|
||||||
@ -39,7 +49,7 @@ class DynamicDetailController extends GetxController {
|
|||||||
oid: oid!,
|
oid: oid!,
|
||||||
pageNum: currentPage + 1,
|
pageNum: currentPage + 1,
|
||||||
type: type!,
|
type: type!,
|
||||||
sort: sortType.index,
|
sort: _sortType.index,
|
||||||
);
|
);
|
||||||
if (res['status']) {
|
if (res['status']) {
|
||||||
List<ReplyItemModel> replies = res['data'].replies;
|
List<ReplyItemModel> replies = res['data'].replies;
|
||||||
@ -76,20 +86,20 @@ class DynamicDetailController extends GetxController {
|
|||||||
// 排序搜索评论
|
// 排序搜索评论
|
||||||
queryBySort() {
|
queryBySort() {
|
||||||
feedBack();
|
feedBack();
|
||||||
switch (sortType) {
|
switch (_sortType) {
|
||||||
case ReplySortType.time:
|
case ReplySortType.time:
|
||||||
sortType = ReplySortType.like;
|
_sortType = ReplySortType.like;
|
||||||
break;
|
break;
|
||||||
case ReplySortType.like:
|
case ReplySortType.like:
|
||||||
sortType = ReplySortType.reply;
|
_sortType = ReplySortType.reply;
|
||||||
break;
|
break;
|
||||||
case ReplySortType.reply:
|
case ReplySortType.reply:
|
||||||
sortType = ReplySortType.time;
|
_sortType = ReplySortType.time;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
sortTypeTitle.value = sortType.titles;
|
sortTypeTitle.value = _sortType.titles;
|
||||||
sortTypeLabel.value = sortType.labels;
|
sortTypeLabel.value = _sortType.labels;
|
||||||
replyList.clear();
|
replyList.clear();
|
||||||
queryReplyList(reqType: 'init');
|
queryReplyList(reqType: 'init');
|
||||||
}
|
}
|
||||||
|
|||||||