Compare commits

..

133 Commits

Author SHA1 Message Date
00dc919a86 v1.0.13 更新 2023-12-17 23:10:43 +08:00
1351803661 Update main.yml flutter version 2023-12-17 23:01:31 +08:00
3a39571ab4 Merge branch 'fix' 2023-12-17 22:59:44 +08:00
51254f5719 fix: 直播音轨 2023-12-17 22:58:00 +08:00
370a2ddcf7 feat: 视频详情页稍后再看 issues #254 2023-12-17 22:38:07 +08:00
9c1c405d19 Merge branch 'main' into fix 2023-12-17 19:35:02 +08:00
3d6d0b0c44 opt: 长按倍速逻辑优化 issues #240 2023-12-17 16:11:53 +08:00
b82c43c303 Merge branch 'main' into design 2023-12-17 15:26:18 +08:00
8ef3c1d9bb mod: 未登录不显示消息入口 2023-12-17 15:16:34 +08:00
a6ab72cadd feat: 消息分页 2023-12-17 14:55:52 +08:00
a43c071eb5 Merge branch 'main' into feature-notice 2023-12-16 23:59:31 +08:00
e9a356c483 feat: 合集列表自动跳转指定index 2023-12-16 23:11:24 +08:00
52ab78f332 feat: up主页显示获赞数 issues #160 2023-12-16 22:35:01 +08:00
12f90a411b fix: 首页推荐点击加载更多无响应 issues #270 2023-12-16 22:12:12 +08:00
6a888ad72b Merge branch 'fix' 2023-12-16 22:04:18 +08:00
5d4ffd665e Merge branch 'design' 2023-12-16 21:57:30 +08:00
f135a2beae fix: 外观设置超过3列无法刷新 issues #271 2023-12-16 21:37:05 +08:00
676bbe2665 Merge branch 'main' into fix 2023-12-16 21:02:31 +08:00
172ea0fbb6 fix: tabbar alignment 2023-12-16 21:02:16 +08:00
b4b64d9864 fix: duration null error 2023-12-16 20:57:18 +08:00
26ba5bc567 mod 2023-12-16 20:51:53 +08:00
f5be50aaa4 Merge pull request #288 from GuMengYu/setting-style
improve: 设置页面样式改进
2023-12-16 20:46:11 +08:00
0ca877bd25 Merge pull request #279 from orz12/post_danmaku
添加基础发送弹幕功能
2023-12-16 20:39:04 +08:00
7afc973b3e Merge pull request #285 from KoolShow/fix_typo
修复副标题风格不统一
2023-12-16 20:33:28 +08:00
a14283260e improve: 设置页面样式改进
- 下拉选择改为dialog, 参考 Android 系统内部设置关于选项的行为
- 一些列表对齐问题
- 字体设置预览文字居中
2023-12-15 13:22:51 +08:00
173695ace6 修复副标题风格不统一
修复 播放设置 自动PiP播放 选项副标题风格不统一
2023-12-13 17:57:41 +08:00
ebbd280768 添加基础发送弹幕功能 2023-12-11 11:56:54 +08:00
bda56169b0 mod 2023-12-04 00:01:07 +08:00
c85f5abcdb mod: 登录时获取accessKey 2023-12-03 00:58:41 +08:00
4217fa26e2 Merge branch 'fix' 2023-12-02 23:11:40 +08:00
f8ca41e4d1 mod 2023-12-02 23:11:22 +08:00
4550ce2637 feat: ai总结开关 2023-12-02 23:10:11 +08:00
6ebfe5872e feat: 首页顶栏&底栏固定 issues #243 2023-12-02 22:41:24 +08:00
7ed91a72c6 Merge branch 'fix' 2023-12-02 21:55:40 +08:00
7f7e1b2035 mod 2023-12-02 21:35:55 +08:00
00f84e1a1c mod: 依赖升级 2023-12-02 21:33:46 +08:00
ebb1d78dbb fix: accessKey获取 2023-12-01 21:21:09 +08:00
427d1385db Merge branch 'design' 2023-11-27 00:43:53 +08:00
e73e02cf13 mod: 个人主页 2023-11-27 00:41:22 +08:00
1f1804b472 mod: 修改版本号 v1.0.11 -> v1.0.12 2023-11-14 07:52:45 +08:00
96455092d1 v1.0.12更新 2023-11-14 00:19:35 +08:00
93854d5b33 Merge branch 'main' of github.com:guozhigq/pilipala 2023-11-14 00:16:33 +08:00
bbfb3175fe fix: 代理导致的网络异常 issues #242 2023-11-14 00:06:16 +08:00
98c2a4243d fix: duration导致的弹幕加载问题 issues #241 2023-11-13 23:53:20 +08:00
c965c42a32 Update README.md 2023-11-12 23:28:40 +08:00
0ed4e33934 fix: 动态页背景色 2023-11-12 17:20:42 +08:00
e5342c386f Merge branch 'fix' 2023-11-12 16:27:26 +08:00
447c968395 Merge branch 'main' of github.com:guozhigq/pilipala 2023-11-12 16:27:10 +08:00
56b754e8d1 fix: iOS端没有声音 2023-11-12 16:26:41 +08:00
685cc35bb7 Update README.md 2023-11-12 16:22:31 +08:00
95bd0ddfee Update README.md 2023-11-12 16:04:42 +08:00
39a2c3f91f Update README.md 2023-11-12 15:58:42 +08:00
5d6a935f3d mod: issues #101 2023-11-12 15:24:35 +08:00
db5132a568 fix: app后台时自动pip 2023-11-12 15:04:58 +08:00
ea38305793 fix: 直播倍速异常 2023-11-12 14:36:22 +08:00
9fa9b5c1f3 merge main 2023-11-12 12:02:31 +08:00
27e268b2a0 v1.0.11更新 2023-11-12 11:52:51 +08:00
1a3da13a4d Merge branch 'fix' into alpha 2023-11-12 00:10:46 +08:00
5edcc756a0 fix: 横屏状态设置面板滑动 2023-11-12 00:10:24 +08:00
a8e57d9b0e Merge branch 'design' into alpha 2023-11-11 23:40:11 +08:00
96523a99ce feat: 长按删除搜索记录 2023-11-11 23:39:54 +08:00
d105718fbf feat: app端登录-开发中 2023-11-11 23:18:19 +08:00
63992c6ec1 Merge branch 'design' into alpha 2023-11-06 00:05:12 +08:00
13ce50f730 mod: 代理设置 2023-11-06 00:04:54 +08:00
eaff4def1c mod: 画面比例 issues #229 2023-11-04 21:07:27 +08:00
3613e27643 Merge pull request #227 from Daydreamer-riri/dev/session
fix: 修复主动暂停后其他音频停止导致视频恢复播放
2023-11-03 23:43:52 +08:00
720e9f0040 fix: 修复主动暂停后其他音频停止导致视频恢复播放 2023-10-29 01:57:39 +08:00
e844870c34 fix: 搜索建议异常 2023-10-29 00:38:10 +08:00
fd4eb0fad1 mod: 优化弹幕请求 2023-10-29 00:03:57 +08:00
e01292a8f9 fix: 部分投稿不连播 issues #217 2023-10-28 23:51:24 +08:00
1804e6ab00 Merge branch 'main' into fix 2023-10-28 23:27:00 +08:00
0adc3257d2 Merge pull request #226 from Daydreamer-riri/dev/session
feat: 支持视频播放时躲避其他通知
2023-10-28 17:13:18 +08:00
2348c14008 feat: 支持不开启后台播放时的音频打断 支持有通知是减小音量通知结束时回复 2023-10-28 14:52:29 +08:00
e65e6229ad Merge pull request #225 from Daydreamer-riri/dev/media_notification
chore: 保持构建时android图标资源不被剥离
2023-10-27 18:19:23 +08:00
41617a6c44 chore: 保持构建时android图标资源不被剥离 2023-10-27 18:06:04 +08:00
ed35970d01 mod: extended_image依赖降级 2023-10-27 00:15:06 +08:00
6668b6e520 Merge pull request #220 from Daydreamer-riri/main
feat: 新增媒体通知
feat: 新增通知小图标
feat: 关闭后台播放时不显示媒体通知
2023-10-27 00:10:41 +08:00
2a4ad969d3 fix: 动态番剧头像跳转issues #224 2023-10-26 23:59:30 +08:00
bf905fa46e feat: 支持其他音频打断视频播放 2023-10-26 20:43:08 +08:00
d8d7ab22c9 feat: 新增通知小图标 | 快进回退使用矢量图标
默认启动器图标边距太大,显示图标太小
2023-10-26 12:39:40 +08:00
1c3f8beeb1 feat: 关闭后台播放时不显示媒体通知 2023-10-26 10:57:11 +08:00
819619563e fix: 分p视频返回后未关闭通知 2023-10-25 20:55:12 +08:00
cfd2038e36 feat: 修复分P视频媒体通知错乱问题 2023-10-25 20:48:08 +08:00
79be397f91 feat: 番剧支持媒体通知 2023-10-25 20:24:38 +08:00
0a5dea0535 feat: 新增媒体通知 2023-10-25 16:27:14 +08:00
eda8a5c6a7 fix: 关注列表不足一屏时无法下拉刷新 2023-10-24 23:47:50 +08:00
101ae2e991 Merge branch 'feature-media_kit' into alpha 2023-10-24 23:01:24 +08:00
c41679d6f5 fix: 部分机型音量不一致 2023-10-24 23:00:47 +08:00
6b5b1a8e31 Merge branch 'design' into alpha 2023-10-24 08:19:58 +08:00
1c370fb224 feat: 底栏样式 issues #189 2023-10-24 08:19:38 +08:00
fd97bd7455 Merge branch 'design' into alpha 2023-10-23 23:34:48 +08:00
81bf8d915c feat: 应用切换至后台自动画中画 issues #212 2023-10-23 23:34:24 +08:00
2db5c45158 Merge branch 'fix' into alpha 2023-10-23 23:08:32 +08:00
2cd8e86864 fix: issues #214 2023-10-23 23:07:37 +08:00
8b28417962 fix: issues #210 2023-10-23 23:00:35 +08:00
5d79c7ebbf mod: 插件升级 2023-10-23 22:42:28 +08:00
943553a4db Merge branch 'feature-media_kit' into alpha 2023-10-22 19:19:28 +08:00
59f7c52611 fix: 音量不一致问题 2023-10-22 19:19:08 +08:00
b5209de56f merge ai总结 2023-10-22 17:57:00 +08:00
dc2bd04143 feat: ai总结 2023-10-22 17:54:34 +08:00
7cae946f21 Merge branch 'fix' into alpha 2023-10-22 16:19:19 +08:00
50b5f221e8 fix: 页面快速返回时异常 issues #175 2023-10-22 16:19:06 +08:00
2f901afd2f Merge branch 'fix' into alpha 2023-10-22 14:13:54 +08:00
48030d5ee7 fix: 带问号的评论内容匹配 2023-10-22 14:11:43 +08:00
7b09c112c0 Merge branch 'design' into alpha 2023-10-22 11:26:57 +08:00
8aa38a36c6 mod: 修改已关注upup分组仅自己可见 2023-10-22 11:24:51 +08:00
b9e255044e Merge branch 'design' into alpha 2023-10-22 10:32:27 +08:00
445a37d305 mod: 首页多列加载更多 2023-10-22 10:32:03 +08:00
9fd5193259 fix: 动态重复加载 2023-10-22 10:21:00 +08:00
ab7dd149d3 mod: 切换至前台继续播放 2023-10-22 09:43:27 +08:00
5c6b8624d7 feat: 已关注up分组 issues #203 2023-10-21 23:01:24 +08:00
88e6eb607c mod: 播放速度调节优化 issues #201 2023-10-21 22:40:05 +08:00
f0851c9737 merge design 2023-10-21 22:17:19 +08:00
3eb461f0cc Merge branch 'feature-media_kit' into alpha 2023-10-21 22:16:14 +08:00
aa95d9020d mod: 画面比例调整优化 2023-10-21 22:15:18 +08:00
f30fb7a71c mod: 视频详情页样式 2023-10-21 21:50:38 +08:00
5ac700bfef Merge branch 'fix' into alpha 2023-10-21 21:40:09 +08:00
fe64967a87 Merge branch 'design' into alpha 2023-10-21 21:38:02 +08:00
eec052c47e mod: 音量亮度滑动控制条调整 2023-10-21 21:37:22 +08:00
b4cc542a4d mod: 播放器插件升级 2023-10-21 21:02:39 +08:00
8782462603 fix: 尝试修复记忆播放 2023-10-21 21:00:22 +08:00
b71558173e mod: 取消自动备份 2023-10-21 13:00:52 +08:00
9744ec88a0 mod: CDN优化 issues #70 2023-10-21 01:11:38 +08:00
a97f57642e fix: 首页动态跳转 2023-10-17 23:20:27 +08:00
d0590933e0 fix: 长按取消后弹幕速度异常 2023-10-17 23:00:48 +08:00
4272b8141a merge main 2023-10-17 08:26:33 +08:00
41e9cfcbbb fix: 修改版本号 2023-10-16 23:45:45 +08:00
45bd4fc6d5 v1.0.10 更新 2023-10-16 23:29:37 +08:00
789d95e728 fix: 长按倍速后不恢复 2023-10-16 23:16:41 +08:00
43e2a2a10a Merge branch 'main' into feature-notice 2023-10-16 00:07:13 +08:00
86c87dc1d5 fix: 首页动态 2023-10-16 00:06:36 +08:00
3d6c270070 fix: request github ua 2023-10-15 23:57:22 +08:00
b272d25157 feat: 私信查看 2023-10-15 16:12:09 +08:00
168 changed files with 6796 additions and 1967 deletions

View File

@ -36,7 +36,7 @@ jobs:
if: steps.cache-flutter.outputs.cache-hit != 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: 3.10.6
flutter-version: 3.16.4
channel: any
- name: 下载项目依赖

View File

@ -3,16 +3,23 @@
</div>
<div align="center">
<h1>PiliPala</h1>
<div align="center">
![GitHub repo size](https://img.shields.io/github/repo-size/guozhigq/pilipala)
![GitHub Repo stars](https://img.shields.io/github/stars/guozhigq/pilipala)
![GitHub all releases](https://img.shields.io/github/downloads/guozhigq/pilipala/total)
</div>
<p>使用Flutter开发的BiliBili第三方客户端</p>
<br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/510shots_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" />
<br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/main_screen.png" width="96%" alt="home" />
<br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/510shots_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" />
<br/>
<img src="https://github.com/guozhigq/pilipala/blob/main/assets/sreenshot/main_screen.png" width="96%" alt="home" />
<br/>
</div>
## 开发环境

View File

@ -39,9 +39,13 @@
android:label="PiliPala"
android:name="${applicationName}"
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
android:name=".MainActivity"
android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
@ -222,6 +226,24 @@
</intent-filter>
</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.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@ -234,6 +256,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_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.
Android 13 or higher.

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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>

View File

@ -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>

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/*" />

BIN
assets/images/ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -0,0 +1,4 @@
## 1.0.10
### 修复
+ 长按倍速抬起后未恢复默认倍速

26
change_log/1.0.11.1112.md Normal file
View File

@ -0,0 +1,26 @@
## 1.0.11
### 新功能
+ 适配了原生媒体通知栏 @Daydreamer-riri
+ 视频主题图标 @Daydreamer-riri
+ 关闭软件后自动画中画播放
+ UP主分组管理
+ md2样式底栏
+
### 修复
+ 历史记录记忆播放
+ 部分类型视频连播
+ 播放速度选择框不支持返回手势
+ 播放速度选择框不支持返回手势
+ 视频播放速度总是显示1.0X
+ 评论页面计数错误
+ 退出视频还有声音
### 优化
+ 视频加载速度
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

11
change_log/1.0.12.1114.md Normal file
View File

@ -0,0 +1,11 @@
## 1.0.12
### 修复
+ iOS端视频播放时没有声音
+ 超过6分钟弹幕不显示
+ 视频详情页网络异常
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

View File

@ -1,6 +1,10 @@
PODS:
- appscheme (1.0.4):
- Flutter
- audio_service (0.0.1):
- Flutter
- audio_session (0.0.1):
- Flutter
- auto_orientation (0.0.1):
- Flutter
- connectivity_plus (0.0.1):
@ -14,6 +18,10 @@ PODS:
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- gt3_flutter_plugin (0.0.8):
- Flutter
- GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3)
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
@ -54,11 +62,14 @@ PODS:
DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- auto_orientation (from `.symlinks/plugins/auto_orientation/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
@ -80,11 +91,16 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- FMDB
- GT3Captcha-iOS
- ReachabilitySwift
EXTERNAL SOURCES:
appscheme:
:path: ".symlinks/plugins/appscheme/ios"
audio_service:
:path: ".symlinks/plugins/audio_service/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
auto_orientation:
:path: ".symlinks/plugins/auto_orientation/ios"
connectivity_plus:
@ -95,6 +111,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
@ -132,26 +150,30 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
@ -159,4 +181,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
COCOAPODS: 1.12.1
COCOAPODS: 1.14.3

View File

@ -140,6 +140,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -156,7 +157,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@ -268,6 +269,23 @@
shellPath = /bin/sh;
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 */
/* Begin PBXSourcesBuildPhase section */

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -103,5 +103,9 @@
</array>
</dict>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

View File

@ -9,7 +9,11 @@ class StyleString {
}
class Constants {
static const String appKey = '27eb53fc9058f8c3';
// 27eb53fc9058f8c3 移动端 Android
// 4409e2ce8ffd12b8 TV端
static const String appKey = '4409e2ce8ffd12b8';
// 59b43e04ad6965f34319062b478f83dd TV端
static const String appSec = '59b43e04ad6965f34319062b478f83dd';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';

View File

@ -1,37 +1,5 @@
import 'package:flutter/material.dart';
// Widget pBadge(
// text,
// context,
// double? top,
// double? right,
// double? bottom,
// double? left, {
// type = 'primary',
// }) {
// Color bgColor = Theme.of(context).colorScheme.primary;
// Color color = Theme.of(context).colorScheme.onPrimary;
// if (type == 'gray') {
// bgColor = Colors.black54.withOpacity(0.4);
// color = Colors.white;
// }
// return Positioned(
// top: top,
// left: left,
// right: right,
// bottom: bottom,
// child: Container(
// padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
// decoration:
// BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
// child: Text(
// text,
// style: TextStyle(fontSize: 11, color: color),
// ),
// ),
// );
// }
class PBadge extends StatelessWidget {
final String? text;
final double? top;

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
class ContentContainer extends StatelessWidget {
final Widget? contentWidget;
final Widget? bottomWidget;
final bool isScrollable;
final Clip? childClipBehavior;
const ContentContainer(
{Key? key,
this.contentWidget,
this.bottomWidget,
this.isScrollable = true,
this.childClipBehavior})
: super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
clipBehavior: childClipBehavior ?? Clip.hardEdge,
physics: isScrollable ? null : NeverScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: constraints.copyWith(
minHeight: constraints.maxHeight,
maxHeight: double.infinity,
),
child: IntrinsicHeight(
child: Column(
children: <Widget>[
if (contentWidget != null)
Expanded(
child: contentWidget!,
)
else
Spacer(),
if (bottomWidget != null) bottomWidget!,
],
),
),
),
);
},
);
}
}

View File

@ -17,6 +17,10 @@ class VideoCardH extends StatelessWidget {
final Function()? longPress;
final Function()? longPressEnd;
final String source;
final bool showOwner;
final bool showView;
final bool showDanmaku;
final bool showPubdate;
const VideoCardH({
Key? key,
@ -24,6 +28,10 @@ class VideoCardH extends StatelessWidget {
this.longPress,
this.longPressEnd,
this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
}) : super(key: key);
@override
@ -103,7 +111,14 @@ class VideoCardH extends StatelessWidget {
},
),
),
VideoContent(videoItem: videoItem, source: source)
VideoContent(
videoItem: videoItem,
source: source,
showOwner: showOwner,
showView: showView,
showDanmaku: showDanmaku,
showPubdate: showPubdate,
)
],
),
);
@ -119,8 +134,20 @@ class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final String source;
const VideoContent(
{super.key, required this.videoItem, this.source = 'normal'});
final bool showOwner;
final bool showView;
final bool showDanmaku;
final bool showPubdate;
const VideoContent({
super.key,
required this.videoItem,
this.source = 'normal',
this.showOwner = true,
this.showView = true,
this.showDanmaku = true,
this.showPubdate = false,
});
@override
Widget build(BuildContext context) {
@ -179,34 +206,40 @@ class VideoContent extends StatelessWidget {
// ),
// ),
// const SizedBox(height: 4),
Row(
children: [
Text(
videoItem.owner.name,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
if (showPubdate)
Text(
Utils.dateFormat(videoItem.pubdate!),
style: TextStyle(
fontSize: 11, color: Theme.of(context).colorScheme.outline),
),
if (showOwner)
Row(
children: [
Text(
videoItem.owner.name,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
),
],
),
],
),
Row(
children: [
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8),
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku,
),
// Text(
// Utils.dateFormat(videoItem.pubdate!),
// style: TextStyle(
// fontSize: 11,
// color: Theme.of(context).colorScheme.outline),
// )
if (showView) ...[
StatView(
theme: 'gray',
view: videoItem.stat.view,
),
const SizedBox(width: 8),
],
if (showDanmaku)
StatDanMu(
theme: 'gray',
danmu: videoItem.stat.danmaku,
),
const Spacer(),
// SizedBox(
// width: 20,

View File

@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
@ -27,6 +28,11 @@ class VideoCardV extends StatelessWidget {
this.longPressEnd,
}) : super(key: key);
bool isStringNumeric(String str) {
RegExp numericRegex = RegExp(r'^\d+$');
return numericRegex.hasMatch(str);
}
void onPushDetail(heroTag) async {
String goto = videoItem.goto;
switch (goto) {
@ -64,12 +70,44 @@ class VideoCardV extends StatelessWidget {
break;
// 动态
case 'picture':
Get.toNamed('/htmlRender', parameters: {
'url': videoItem.uri,
'title': videoItem.title,
'id': videoItem.param.toString(),
'dynamicType': '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:
SmartDialog.showToast(videoItem.goto);
@ -295,8 +333,10 @@ class VideoStat extends StatelessWidget {
color: Theme.of(context).colorScheme.outline,
),
children: [
TextSpan(text: '${videoItem.stat.view}观看'),
TextSpan(text: '${videoItem.stat.danmu}弹幕'),
if (videoItem.stat.view != '-')
TextSpan(text: '${videoItem.stat.view}观看'),
if (videoItem.stat.danmu != '-')
TextSpan(text: '${videoItem.stat.danmu}弹幕'),
],
),
);

View File

@ -215,7 +215,7 @@ class Api {
// 粉丝
// vmid 用户id pn 页码 ps 每页个数最大50 order: desc
// order_type 排序规则 最近访问传空,最常访问传 attention
static const String fans = 'https://api.bilibili.com/x/relation/fans';
static const String fans = '/x/relation/fans';
// 直播
// ?page=1&page_size=30&platform=web
@ -312,6 +312,10 @@ class Api {
static const String webDanmaku = '/x/v2/dm/web/seg.so';
//发送视频弹幕
//https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/action.md
static const String shootDanmaku = '/x/v2/dm/post';
// up主分组
static const String followUpTag = '/x/relation/tags';
@ -321,4 +325,145 @@ class Api {
// 获取指定分组下的up
static const String followUpGroup = '/x/relation/tag';
/// 私聊
/// 'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions?
/// session_type=1&
/// group_fold=1&
/// unfollow_fold=0&
/// sort_rule=2&
/// build=0&
/// mobi_app=web&
/// w_rid=8641d157fb9a9255eb2159f316ee39e2&
/// wts=1697305010
static const String sessionList =
'https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions';
/// 私聊用户信息
/// uids
/// build=0&mobi_app=web
static const String sessionAccountList =
'https://api.vc.bilibili.com/account/v1/user/cards';
/// https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs?
/// talker_id=400787461&
/// session_type=1&
/// size=20&
/// sender_device_id=1&
/// build=0&
/// mobi_app=web&
/// web_location=333.1296&
/// w_rid=cfe3bf58c9fe181bbf4dd6c75175e6b0&
/// wts=1697350697
static const String sessionMsg =
'https://api.vc.bilibili.com/svr_sync/v1/svr_sync/fetch_session_msgs';
/// 标记已读 POST
/// talker_id:
/// session_type: 1
/// ack_seqno: 920224140918926
/// build: 0
/// mobi_app: web
/// csrf_token:
/// csrf:
static const String updateAck =
'https://api.vc.bilibili.com/session_svr/v1/session_svr/update_ack';
// 获取某个动态详情
// 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';
/// cookie转access_key
static const cookieToKey =
'https://passport.bilibili.com/x/passport-tv-login/h5/qrcode/confirm';
/// 申请二维码(TV端)
static const getTVCode =
'https://passport.snm0516.aisee.tv/x/passport-tv-login/qrcode/auth_code';
///扫码登录TV端
static const qrcodePoll =
'https://passport.bilibili.com/x/passport-tv-login/qrcode/poll';
/// 置顶视频
static const getTopVideoApi = '/x/space/top/arc';
/// 主页 - 最近投币的视频
/// vmid
/// gaia_source = main_web
/// web_location
/// w_rid
/// wts
static const getRecentCoinVideoApi = '/x/space/coin/video';
/// 最近点赞的视频
static const getRecentLikeVideoApi = '/x/space/like/video';
/// 最近追番
static const getRecentBangumiApi = '/x/space/bangumi/follow/list';
/// 用户专栏
static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series';
/// 获赞数 播放数
/// mid
static const getMemberViewApi = '/x/space/upstat';
/// 查询某个专栏
/// mid
/// season_id
/// sort_reverse
/// page_num
/// page_size
static const getSeasonDetailApi =
'/x/polymer/web-space/seasons_archives_list';
}

View File

@ -24,4 +24,72 @@ class DanmakaHttp {
);
return DmSegMobileReply.fromBuffer(response.data);
}
static Future shootDanmaku({
int type = 1,//弹幕类选择(1视频弹幕 2漫画弹幕)
required int oid,// 视频cid
required String msg,//弹幕文本(长度小于 100 字符)
int mode = 1,// 弹幕类型(1滚动弹幕 4底端弹幕 5顶端弹幕 6逆向弹幕(不能使用) 7高级弹幕 8代码弹幕不能使用 9BAS弹幕pool必须为2)
// String? aid,// 稿件avid
// String? bvid,// bvid与aid必须有一个
required String bvid,
int? progress,// 弹幕出现在视频内的时间单位为毫秒默认为0
int? color,// 弹幕颜色(默认白色16777215
int? fontsize,// 弹幕字号默认25
int? pool,// 弹幕池选择0普通池 1字幕池 2特殊池代码/BAS弹幕默认普通池0
//int? rnd,// 当前时间戳*1000000若无此项则发送弹幕冷却时间限制为90s若有此项则发送弹幕冷却时间限制为5s
int? colorful,//60001专属渐变彩色需要会员
int? checkbox_type,//是否带 UP 身份标识0普通4带有标识
// String? csrf,//CSRF Token位于 Cookie Cookie 方式必要
// String? access_key,// APP 登录 Token APP 方式必要
}) async {
// 构建参数对象
// assert(aid != null || bvid != null);
// assert(csrf != null || access_key != null);
assert(msg.length < 100);
// 构建参数对象
var params = <String, dynamic>{
'type': type,
'oid': oid,
'msg': msg,
'mode': mode,
//'aid': aid,
'bvid': bvid,
'progress': progress,
'color': color,
'fontsize': fontsize,
'pool': pool,
'rnd': DateTime.now().microsecondsSinceEpoch,
'colorful': colorful,
'checkbox_type': checkbox_type,
'csrf': await Request.getCsrf(),
// 'access_key': access_key,
}..removeWhere((key, value) => value == null);
var response = await Request().post(
Api.shootDanmaku,
data: params,
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
if (response.statusCode != 200) {
return {
'status': false,
'data': [],
'msg': '弹幕发送失败,状态码:${response.statusCode}',
};
}
if (response.data['code'] == 0) {
return {
'status': true,
'data': response.data['data'],
};
} else {
return {
'status': false,
'data': [],
'msg': "${response.data['code']}: ${response.data['message']}",
};
}
}
}

View File

@ -86,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'],
};
}
}
}

View File

@ -9,37 +9,57 @@ class HtmlHttp {
"https://www.bilibili.com/opus/$id",
extra: {'ua': 'pc'},
);
Document rootTree = parse(response.data);
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
// 头像
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
avatar = 'https:${avatar.split('@')[0]}';
String uname =
authorHeader.querySelector('.fixed-author-header__author__name')!.text;
// 动态详情
Element opusDetail = appDom.querySelector('.opus-detail')!;
// 发布时间
String updateTime =
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
//
String opusContent =
opusDetail.querySelector('.opus-module-content')!.innerHtml;
String commentId = opusDetail
.querySelector('.bili-comment-container')!
.className
.split(' ')[1]
.split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
'uname': uname,
'updateTime': updateTime,
'content': opusContent,
'commentId': int.parse(commentId)
};
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

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'dart:async';
import 'package:dio/dio.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:pilipala/utils/storage.dart';
@ -17,6 +18,11 @@ class Request {
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
Box setting = GStrorage.setting;
static Box localCache = GStrorage.localCache;
late dynamic enableSystemProxy;
late String systemProxyHost;
late String systemProxyPort;
/// 设置cookie
static setCookie() async {
@ -41,8 +47,8 @@ class Request {
log("setCookie, ${e.toString()}");
}
}
setOptionsHeaders(userInfo);
}
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
if (cookie.isEmpty) {
try {
@ -67,8 +73,10 @@ class Request {
return token;
}
static setOptionsHeaders(userInfo) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
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';
@ -92,6 +100,13 @@ class Request {
headers: {},
);
enableSystemProxy =
setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false);
systemProxyHost =
localCache.get(LocalCacheKey.systemProxyHost, defaultValue: '');
systemProxyPort =
localCache.get(LocalCacheKey.systemProxyPort, defaultValue: '');
dio = Dio(options)
/// fix 第三方登录 302重定向 跟iOS代理问题冲突
@ -102,6 +117,23 @@ class Request {
),
);
/// 设置代理
if (enableSystemProxy) {
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
// Config the client.
client.findProxy = (uri) {
// return 'PROXY host:port';
return 'PROXY $systemProxyHost:$systemProxyPort';
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
},
);
}
//添加拦截器
dio.interceptors.add(ApiInterceptor());

177
lib/http/login.dart Normal file
View 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);
}
}

View File

@ -1,9 +1,16 @@
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/seasons.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class MemberHttp {
@ -215,4 +222,243 @@ class MemberHttp {
};
}
}
// 获取up置顶
static Future getTopVideo(String? vmid) async {
var res = await Request().get(Api.getTopVideoApi);
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'],
};
}
}
// 获取uo专栏
static Future getMemberSeasons(int? mid, int? pn, int? ps) async {
var res = await Request().get(Api.getMemberSeasonsApi, data: {
'mid': mid,
'page_num': pn,
'page_size': ps,
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 最近投币
static Future getRecentCoinVideo({required int mid}) async {
Map params = await WbiSign().makSign({
'mid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
});
var res = await Request().get(
Api.getRecentCoinVideoApi,
data: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberCoinsDataModel>((e) => MemberCoinsDataModel.fromJson(e))
.toList(),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 最近点赞
static Future getRecentLikeVideo({required int mid}) async {
Map params = await WbiSign().makSign({
'mid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
});
var res = await Request().get(
Api.getRecentLikeVideoApi,
data: {
'vmid': mid,
'gaia_source': 'main_web',
'web_location': 333.999,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': MemberSeasonsDataModel.fromJson(res.data['data']['items_lists'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 查看某个专栏
static Future getSeasonDetail({
required int mid,
required int seasonId,
bool sortReverse = false,
required int pn,
required int ps,
}) async {
var res = await Request().get(
Api.getSeasonDetailApi,
data: {
'mid': mid,
'season_id': seasonId,
'sort_reverse': sortReverse,
'page_num': pn,
'page_size': ps,
},
);
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': MemberSeasonsList.fromJson(res.data['data'])
};
} catch (err) {
print(err);
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取TV authCode
static Future getTVCode() async {
SmartDialog.showLoading();
var params = {
'appkey': Constants.appKey,
'local_id': '0',
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.getTVCode, queryParameters: {...params, 'sign': sign});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']['auth_code'],
'msg': '操作成功'
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取access_key
static Future cookieToKey() async {
var authCodeRes = await getTVCode();
if (authCodeRes['status']) {
var res = await Request().post(Api.cookieToKey, queryParameters: {
'auth_code': authCodeRes['data'],
'build': 708200,
'csrf': await Request.getCsrf(),
});
await Future.delayed(const Duration(milliseconds: 300));
await qrcodePoll(authCodeRes['data']);
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}
static Future qrcodePoll(authCode) async {
var params = {
'appkey': Constants.appKey,
'auth_code': authCode.toString(),
'local_id': '0',
'ts': (DateTime.now().millisecondsSinceEpoch ~/ 1000).toString(),
};
String sign = Utils.appSign(
params,
Constants.appKey,
Constants.appSec,
);
var res = await Request()
.post(Api.qrcodePoll, queryParameters: {...params, 'sign': sign});
SmartDialog.dismiss();
if (res.data['code'] == 0) {
String accessKey = res.data['data']['access_token'];
Box localCache = GStrorage.localCache;
Box userInfoCache = GStrorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache');
localCache.put(
LocalCacheKey.accessKey, {'mid': userInfo.mid, 'value': accessKey});
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取up播放数、点赞数
static Future memberView({required int mid}) async {
var res = await Request().get(Api.getMemberViewApi, data: {'mid': mid});
if (res.data['code'] == 0) {
return {'status': true, 'data': res.data['data']};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

85
lib/http/msg.dart Normal file
View File

@ -0,0 +1,85 @@
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/msg/account.dart';
import 'package:pilipala/models/msg/session.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class MsgHttp {
// 会话列表
static Future sessionList({int? endTs}) async {
Map<String, dynamic> params = {
'session_type': 1,
'group_fold': 1,
'unfollow_fold': 0,
'sort_rule': 2,
'build': 0,
'mobi_app': 'web',
};
if (endTs != null) {
params['end_ts'] = endTs;
}
Map signParams = await WbiSign().makSign(params);
var res = await Request().get(Api.sessionList, data: signParams);
if (res.data['code'] == 0) {
return {
'status': true,
'data': SessionDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
static Future accountList(uids) async {
var res = await Request().get(Api.sessionAccountList, data: {
'uids': uids,
'build': 0,
'mobi_app': 'web',
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<AccountListModel>((e) => AccountListModel.fromJson(e))
.toList(),
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
static Future sessionMsg({
int? talkerId,
}) async {
Map params = await WbiSign().makSign({
'talker_id': talkerId,
'session_type': 1,
'size': 20,
'sender_device_id': 1,
'build': 0,
'mobi_app': 'web',
});
var res = await Request().get(Api.sessionMsg, data: params);
if (res.data['code'] == 0) {
return {
'status': true,
'data': SessionMsgDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'date': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -39,16 +39,25 @@ class SearchHttp {
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) {
if (res.data['result'] is Map) {
res.data['result']['term'] = term;
if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
if (resultMap['result'] is Map) {
resultMap['result']['term'] = term;
}
return {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
return {
'status': true,
'data': res.data['result'] is Map
? SearchSuggestModel.fromJson(res.data['result'])
: [],
};
} else {
return {
'status': false,

View File

@ -199,7 +199,7 @@ class UserHttp {
}
}
// 获取用户凭证
// 获取用户凭证 失效
static Future thirdLogin() async {
var res = await Request().get(
'https://passport.bilibili.com/login/app/third',

View File

@ -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_rec_video_item.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_detail_res.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/wbi_sign.dart';
/// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果
@ -420,4 +422,23 @@ class VideoHttp {
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']),
};
}
}
}

View File

@ -16,6 +16,7 @@ import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
import 'package:pilipala/pages/main/view.dart';
import 'package:pilipala/services/service_locator.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/data.dart';
import 'package:pilipala/utils/storage.dart';
@ -28,6 +29,7 @@ void main() async {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) async {
await GStrorage.init();
await setupServiceLocator();
runApp(const MyApp());
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);

View File

@ -118,7 +118,7 @@ class RcmdStat {
RcmdStat.fromJson(Map<String, dynamic> json) {
view = json["cover_left_text_1"];
danmu = json['cover_left_text_2'];
danmu = json['cover_left_text_2'] ?? '-';
}
}

View 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"];
}
}

View File

@ -0,0 +1,89 @@
class MemberCoinsDataModel {
MemberCoinsDataModel({
this.aid,
this.bvid,
this.cid,
this.coins,
this.copyright,
this.ctime,
this.desc,
this.duration,
this.owner,
this.pic,
this.pubLocation,
this.pubdate,
this.resourceType,
this.state,
this.subtitle,
this.time,
this.title,
this.tname,
this.videos,
this.view,
this.danmaku,
});
int? aid;
String? bvid;
int? cid;
int? coins;
int? copyright;
int? ctime;
String? desc;
int? duration;
Owner? owner;
String? pic;
String? pubLocation;
int? pubdate;
String? resourceType;
int? state;
String? subtitle;
int? time;
String? title;
String? tname;
int? videos;
int? view;
int? danmaku;
MemberCoinsDataModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
cid = json['cid'];
coins = json['coins'];
copyright = json['copyright'];
ctime = json['ctime'];
desc = json['desc'];
duration = json['duration'];
owner = Owner.fromJson(json['owner']);
pic = json['pic'];
pubLocation = json['pub_location'];
pubdate = json['pubdate'];
resourceType = json['resource_type'];
state = json['state'];
subtitle = json['subtitle'];
time = json['time'];
title = json['title'];
tname = json['tname'];
videos = json['videos'];
view = json['stat']['view'];
danmaku = json['stat']['danmaku'];
}
}
class Owner {
Owner({
this.mid,
this.name,
this.face,
});
int? mid;
String? name;
String? face;
Owner.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'];
face = json['face'];
}
}

View File

@ -0,0 +1,108 @@
class MemberSeasonsDataModel {
MemberSeasonsDataModel({
this.page,
this.seasonsList,
});
Map? page;
List<MemberSeasonsList>? seasonsList;
MemberSeasonsDataModel.fromJson(Map<String, dynamic> json) {
page = json['page'];
seasonsList = json['seasons_list'] != null
? json['seasons_list']
.map<MemberSeasonsList>((e) => MemberSeasonsList.fromJson(e))
.toList()
: [];
}
}
class MemberSeasonsList {
MemberSeasonsList({
this.archives,
this.meta,
this.recentAids,
this.page,
});
List<MemberArchiveItem>? archives;
MamberMeta? meta;
List? recentAids;
Map? page;
MemberSeasonsList.fromJson(Map<String, dynamic> json) {
archives = json['archives'] != null
? json['archives']
.map<MemberArchiveItem>((e) => MemberArchiveItem.fromJson(e))
.toList()
: [];
meta = MamberMeta.fromJson(json['meta']);
page = json['page'];
}
}
class MemberArchiveItem {
MemberArchiveItem({
this.aid,
this.bvid,
this.ctime,
this.duration,
this.pic,
this.cover,
this.pubdate,
this.view,
this.title,
});
int? aid;
String? bvid;
int? ctime;
int? duration;
String? pic;
String? cover;
int? pubdate;
int? view;
String? title;
MemberArchiveItem.fromJson(Map<String, dynamic> json) {
aid = json['aid'];
bvid = json['bvid'];
ctime = json['ctime'];
duration = json['duration'];
pic = json['pic'];
cover = json['pic'];
pubdate = json['pubdate'];
view = json['stat']['view'];
title = json['title'];
}
}
class MamberMeta {
MamberMeta({
this.cover,
this.description,
this.mid,
this.name,
this.ptime,
this.seasonId,
this.total,
});
String? cover;
String? description;
int? mid;
String? name;
int? ptime;
int? seasonId;
int? total;
MamberMeta.fromJson(Map<String, dynamic> json) {
cover = json['cover'];
description = json['description'];
mid = json['mid'];
name = json['name'];
ptime = json['ptime'];
seasonId = json['season_id'];
total = json['total'];
}
}

View File

@ -0,0 +1,80 @@
class AccountListModel {
AccountListModel({
this.mid,
this.name,
this.sex,
this.face,
this.sign,
this.rank,
this.level,
this.silence,
this.vip,
this.pendant,
this.nameplate,
this.official,
this.birthday,
this.isFakeAccount,
this.isDeleted,
this.inRegAudit,
this.faceNft,
this.faceNftNew,
this.isSeniorMember,
this.digitalId,
this.digitalType,
this.attestation,
this.expertInfo,
this.honours,
});
int? mid;
String? name;
String? sex;
String? face;
String? sign;
int? rank;
int? level;
int? silence;
Map? vip;
Map? pendant;
Map? nameplate;
Map? official;
int? birthday;
int? isFakeAccount;
int? isDeleted;
int? inRegAudit;
int? faceNft;
int? faceNftNew;
int? isSeniorMember;
String? digitalId;
int? digitalType;
Map? attestation;
Map? expertInfo;
Map? honours;
AccountListModel.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
name = json['name'] ?? '';
sex = json['sex'];
face = json['face'];
sign = json['sign'];
rank = json['rank'];
level = json['level'];
silence = json['silence'];
vip = json['vip'];
pendant = json['pendant'];
nameplate = json['nameplate'];
official = json['official'];
birthday = json['birthday'];
isFakeAccount = json['is_fake_account'];
isDeleted = json['is_deleted'];
inRegAudit = json['in_reg_audit'];
faceNft = json['face_nft'];
faceNftNew = json['face_nft_new'];
isSeniorMember = json['is_senior_member'];
digitalId = json['digital_id'];
digitalType = json['digital_type'];
attestation = json['attestation'];
expertInfo = json['expert_info'];
honours = json['honours'];
}
}

226
lib/models/msg/session.dart Normal file
View File

@ -0,0 +1,226 @@
import 'dart:convert';
import 'package:pilipala/models/msg/account.dart';
class SessionDataModel {
SessionDataModel({
this.sessionList,
this.hasMore,
});
List? sessionList;
int? hasMore;
SessionDataModel.fromJson(Map<String, dynamic> json) {
sessionList = json['session_list']
?.map<SessionList>((e) => SessionList.fromJson(e))
.toList();
hasMore = json['has_more'];
}
}
class SessionList {
SessionList({
this.talkerId,
this.sessionType,
this.atSeqno,
this.topTs,
this.groupName,
this.groupCover,
this.isFollow,
this.isDnd,
this.ackSeqno,
this.ackTs,
this.sessionTs,
this.unreadCount,
this.lastMsg,
this.groupType,
this.canFold,
this.status,
this.maxSeqno,
this.newPushMsg,
this.setting,
this.isGuardian,
this.isIntercept,
this.isTrust,
this.systemMsgType,
this.liveStatus,
this.bizMsgUnreadCount,
// this.userLabel,
});
int? talkerId;
int? sessionType;
int? atSeqno;
int? topTs;
String? groupName;
String? groupCover;
int? isFollow;
int? isDnd;
int? ackSeqno;
int? ackTs;
int? sessionTs;
int? unreadCount;
LastMsg? lastMsg;
int? groupType;
int? canFold;
int? status;
int? maxSeqno;
int? newPushMsg;
int? setting;
int? isGuardian;
int? isIntercept;
int? isTrust;
int? systemMsgType;
int? liveStatus;
int? bizMsgUnreadCount;
// int? userLabel;
AccountListModel? accountInfo;
SessionList.fromJson(Map<String, dynamic> json) {
talkerId = json["talker_id"];
sessionType = json["session_type"];
atSeqno = json["at_seqno"];
topTs = json["top_ts"];
groupName = json["group_name"];
groupCover = json["group_cover"];
isFollow = json["is_follow"];
isDnd = json["is_dnd"];
ackSeqno = json["ack_seqno"];
ackTs = json["ack_ts"];
sessionTs = json["session_ts"];
unreadCount = json["unread_count"];
lastMsg =
json["last_msg"] != null ? LastMsg.fromJson(json["last_msg"]) : null;
groupType = json["group_type"];
canFold = json["can_fold"];
status = json["status"];
maxSeqno = json["max_seqno"];
newPushMsg = json["new_push_msg"];
setting = json["setting"];
isGuardian = json["is_guardian"];
isIntercept = json["is_intercept"];
isTrust = json["is_trust"];
systemMsgType = json["system_msg_type"];
liveStatus = json["live_status"];
bizMsgUnreadCount = json["biz_msg_unread_count"];
// userLabel = json["user_label"];
}
}
class LastMsg {
LastMsg({
this.senderIid,
this.receiverType,
this.receiverId,
this.msgType,
this.content,
this.msgSeqno,
this.timestamp,
this.atUids,
this.msgKey,
this.msgStatus,
this.notifyCode,
this.newFaceVersion,
});
int? senderIid;
int? receiverType;
int? receiverId;
int? msgType;
Map? content;
int? msgSeqno;
int? timestamp;
String? atUids;
int? msgKey;
int? msgStatus;
String? notifyCode;
int? newFaceVersion;
LastMsg.fromJson(Map<String, dynamic> json) {
senderIid = json['sender_uid'];
receiverType = json['receiver_type'];
receiverId = json['receiver_id'];
msgType = json['msg_type'];
content = jsonDecode(json['content']);
msgSeqno = json['msg_seqno'];
timestamp = json['timestamp'];
atUids = json['at_uids'];
msgKey = json['msg_key'];
msgStatus = json['msg_status'];
notifyCode = json['notify_code'];
newFaceVersion = json['new_face_version'];
}
}
class SessionMsgDataModel {
SessionMsgDataModel({
this.messages,
this.hasMore,
this.minSeqno,
this.maxSeqno,
this.eInfos,
});
List<MessageItem>? messages;
int? hasMore;
int? minSeqno;
int? maxSeqno;
List? eInfos;
SessionMsgDataModel.fromJson(Map<String, dynamic> json) {
messages = json['messages']
.map<MessageItem>((e) => MessageItem.fromJson(e))
.toList();
hasMore = json['has_more'];
minSeqno = json['min_seqno'];
maxSeqno = json['max_seqno'];
eInfos = json['e_infos'];
}
}
class MessageItem {
MessageItem({
this.senderUid,
this.receiverType,
this.receiverId,
this.msgType,
this.content,
this.msgSeqno,
this.timestamp,
this.atUids,
this.msgKey,
this.msgStatus,
this.notifyCode,
this.newFaceVersion,
});
int? senderUid;
int? receiverType;
int? receiverId;
int? msgType;
Map? content;
int? msgSeqno;
int? timestamp;
List? atUids;
int? msgKey;
int? msgStatus;
String? notifyCode;
int? newFaceVersion;
MessageItem.fromJson(Map<String, dynamic> json) {
senderUid = json['sender_uid'];
receiverType = json['receiver_type'];
receiverId = json['receiver_id'];
// 1 文本 2 图片 18 系统提示 10 系统通知
msgType = json['msg_type'];
content = jsonDecode(json['content']);
msgSeqno = json['msg_seqno'];
timestamp = json['timestamp'];
atUids = json['at_uids'];
msgKey = json['msg_key'];
msgStatus = json['msg_status'];
notifyCode = json['notify_code'];
newFaceVersion = json['new_face_version'];
}
}

80
lib/models/video/ai.dart Normal file
View 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'];
}
}

View File

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:pilipala/models/video/play/quality.dart';
class PlayUrlModel {

View File

@ -1,6 +1,3 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -34,11 +31,6 @@ class _AboutPageState extends State<AboutPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Divider(
thickness: 8,
height: 10,
color: Theme.of(context).colorScheme.onInverseSurface,
),
Image.asset(
'assets/images/logo/logo_android_2.png',
width: 150,
@ -83,9 +75,9 @@ class _AboutPageState extends State<AboutPage> {
// ),
// ),
Divider(
thickness: 8,
thickness: 1,
height: 30,
color: Theme.of(context).colorScheme.onInverseSurface,
color: Theme.of(context).colorScheme.outlineVariant,
),
ListTile(
onTap: () => _aboutController.githubUrl(),
@ -134,11 +126,6 @@ class _AboutPageState extends State<AboutPage> {
title: const Text('赞助'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
Divider(
thickness: 8,
height: 30,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
),
),

View File

@ -5,7 +5,6 @@ import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';

View File

@ -6,6 +6,7 @@ import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
@ -35,6 +36,8 @@ class _BangumiPageState extends State<BangumiPage>
scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
_futureBuilderFutureFollow = _bangumidController.queryBangumiFollow();
scrollController.addListener(
@ -51,8 +54,10 @@ class _BangumiPageState extends State<BangumiPage>
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
searchBarStream.add(false);
}
},
);
@ -201,7 +206,7 @@ class _BangumiPageState extends State<BangumiPage>
},
),
),
const LoadingMore()
LoadingMore()
],
),
);

View File

@ -10,6 +10,8 @@ class PlDanmakuController {
// 按 6min 分段
int segCount = 0;
List<DmSegMobileReply> dmSegList = [];
// 已请求的段落标记
List<int> hasrequestSeg = [];
int currentSegIndex = 1;
int currentDmIndex = 0;

View File

@ -95,7 +95,13 @@ class _PlDanmakuState extends State<PlDanmaku> {
// 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty) {
print('🌹🌹: ${segIndex}');
print('🌹🌹: ${ctr.dmSegList.length}');
print('🌹🌹: ${ctr.hasrequestSeg.contains(segIndex - 1)}');
if (segIndex - 1 >= ctr.dmSegList.length ||
(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();

View File

@ -231,6 +231,15 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
),
SliverToBoxAdapter(
child: Container(
height: 6,
color: Theme.of(context)
.colorScheme
.onInverseSurface
.withOpacity(0.5),
),
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {

View File

@ -17,6 +17,10 @@ class AuthorPanel extends StatelessWidget {
children: [
GestureDetector(
onTap: () {
// 番剧
if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
return;
}
feedBack();
Get.toNamed(
'/member?mid=${item.modules.moduleAuthor.mid}',

View File

@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
// 富文本
InlineSpan richNode(item, context) {
@ -11,13 +9,11 @@ InlineSpan richNode(item, context) {
TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary);
List<InlineSpan> spanChilds = [];
String contentType = 'desc';
dynamic richTextNodes;
if (item.modules.moduleDynamic.desc != null) {
richTextNodes = item.modules.moduleDynamic.desc.richTextNodes;
} else if (item.modules.moduleDynamic.major != null) {
contentType = 'major';
// 动态页面 richTextNodes 层级可能与主页动态层级不同
richTextNodes =
item.modules.moduleDynamic.major.opus.summary.richTextNodes;

View File

@ -56,67 +56,73 @@ class _UpPanelState extends State<UpPanel> {
floating: true,
pinned: false,
delegate: _SliverHeaderDelegate(
height: 90,
child: Container(
height: 90,
color: Theme.of(context).colorScheme.background,
child: Row(
children: [
Expanded(
child: ListView(
scrollDirection: Axis.horizontal,
controller: scrollController,
children: [
const SizedBox(width: 10),
if (liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
),
],
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i)
],
const SizedBox(width: 10),
],
),
),
Material(
child: InkWell(
onTap: () => {
feedBack(),
Get.toNamed('/follow?mid=${userInfo.mid}')
},
child: Container(
height: 100,
padding: const EdgeInsets.only(left: 10, right: 10),
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withOpacity(0.3),
child: Center(
height: 124,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(left: 16, right: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('最新关注'),
GestureDetector(
onTap: () {
feedBack();
Get.toNamed('/follow?mid=${userInfo.mid}');
},
child: Container(
padding: const EdgeInsets.only(top: 5, bottom: 5),
child: Text(
'全部',
'查看全部',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer),
color: Theme.of(context).colorScheme.outline),
),
),
),
),
],
),
],
)),
),
),
Container(
height: 90,
color: Theme.of(context).colorScheme.background,
child: Row(
children: [
Expanded(
child: ListView(
scrollDirection: Axis.horizontal,
controller: scrollController,
children: [
const SizedBox(width: 10),
if (liveList.isNotEmpty) ...[
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
VerticalDivider(
indent: 20,
endIndent: 40,
width: 26,
color: Theme.of(context)
.colorScheme
.primary
.withOpacity(0.5),
),
],
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i)
],
const SizedBox(width: 10),
],
),
),
],
),
),
],
)),
);
}

View File

@ -2,16 +2,19 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/pages/follow/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/group_panel.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart';
class FollowItem extends StatelessWidget {
final FollowItemModel item;
const FollowItem({super.key, required this.item});
final FollowController? ctr;
const FollowItem({super.key, required this.item, this.ctr});
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(item!.mid);
String heroTag = Utils.makeHeroTag(item.mid);
return ListTile(
onTap: () {
feedBack();
@ -39,7 +42,29 @@ class FollowItem extends StatelessWidget {
overflow: TextOverflow.ellipsis,
),
dense: true,
trailing: const SizedBox(width: 6),
trailing: ctr!.isOwner.value
? SizedBox(
height: 34,
child: TextButton(
onPressed: () async {
await Get.bottomSheet(
GroupPanel(mid: item.mid!),
isScrollControlled: true,
);
},
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(15, 0, 15, 0),
foregroundColor: Theme.of(context).colorScheme.outline,
backgroundColor:
Theme.of(context).colorScheme.onInverseSurface, // 设置按钮背景色
),
child: const Text(
'已关注',
style: TextStyle(fontSize: 12),
),
),
)
: const SizedBox(),
);
}
}

View File

@ -84,7 +84,10 @@ class _FollowListState extends State<FollowList> {
),
);
} else {
return FollowItem(item: list[index]);
return FollowItem(
item: list[index],
ctr: widget.ctr,
);
}
},
)

View File

@ -89,6 +89,7 @@ class _OwnerFollowListState extends State<OwnerFollowList>
return Obx(
() => followList.isNotEmpty
? ListView.builder(
physics: const AlwaysScrollableScrollPhysics(),
controller: scrollController,
itemCount: followList.length + 1,
itemBuilder: (BuildContext context, int index) {
@ -101,7 +102,10 @@ class _OwnerFollowListState extends State<OwnerFollowList>
MediaQuery.of(context).padding.bottom),
);
} else {
return FollowItem(item: followList[index]);
return FollowItem(
item: followList[index],
ctr: widget.ctr,
);
}
},
)

View File

@ -11,7 +11,6 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/history_search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';

View File

@ -131,7 +131,6 @@ class _HistorySearchPageState extends State<HistorySearchPage> {
onChoose: null,
onUpdateMultiple: () => null,
);
;
}
},
)

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
@ -15,6 +17,10 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
var userInfo;
Box setting = GStrorage.setting;
late final StreamController<bool> searchBarStream =
StreamController<bool>.broadcast();
late bool hideSearchBar;
@override
void onInit() {
@ -33,6 +39,8 @@ class HomeController extends GetxController with GetTickerProviderStateMixin {
length: tabs.length,
vsync: this,
);
hideSearchBar =
setting.get(SettingBoxKey.hideSearchBar, defaultValue: true);
}
void onRefresh() {

View File

@ -1,7 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
@ -18,11 +19,17 @@ class _HomePageState extends State<HomePage>
with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
final HomeController _homeController = Get.put(HomeController());
List videoList = [];
Stream<bool> stream = Get.find<MainController>().bottomBarStream.stream;
late Stream<bool> stream;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
stream = _homeController.searchBarStream.stream;
}
showUserBottonSheet() {
feedBack();
showModalBottomSheet(
@ -46,7 +53,9 @@ class _HomePageState extends State<HomePage>
body: Column(
children: [
CustomAppBar(
stream: stream,
stream: _homeController.hideSearchBar
? stream
: StreamController<bool>.broadcast().stream,
ctr: _homeController,
callback: showUserBottonSheet,
),
@ -65,6 +74,7 @@ class _HomePageState extends State<HomePage>
dividerColor: Colors.transparent,
enableFeedback: true,
splashBorderRadius: BorderRadius.circular(10),
tabAlignment: TabAlignment.center,
onTap: (value) {
feedBack();
if (_homeController.initialIndex == value) {
@ -118,7 +128,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
duration: const Duration(milliseconds: 500),
height: snapshot.data
? MediaQuery.of(context).padding.top + 52
: MediaQuery.of(context).padding.top,
: MediaQuery.of(context).padding.top - 10,
child: Container(
padding: EdgeInsets.only(
left: 20,
@ -129,7 +139,13 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
child: Row(
children: [
const Expanded(child: SearchPage()),
const SizedBox(width: 10),
if (ctr!.userLogin.value) ...[
const SizedBox(width: 6),
IconButton(
onPressed: () => Get.toNamed('/whisper'),
icon: const Icon(Icons.notifications_none))
],
const SizedBox(width: 6),
Obx(
() => ctr!.userLogin.value
? Stack(

View File

@ -9,6 +9,7 @@ import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/controller.dart';
import 'package:pilipala/pages/main/index.dart';
@ -35,6 +36,8 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
scrollController = _hotController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
@ -49,8 +52,10 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
searchBarStream.add(false);
}
},
);

View File

@ -9,7 +9,6 @@ import 'package:pilipala/common/widgets/html_render.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
@ -139,7 +138,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
IconButton(
onPressed: () {
Get.toNamed('/webview', parameters: {
'url': 'https:$url',
'url': url.startsWith('http') ? url : 'https:$url',
'type': 'url',
'pageTitle': title,
});

View File

@ -9,7 +9,9 @@ import 'package:pilipala/common/skeleton/video_card_v.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'controller.dart';
import 'widgets/live_item.dart';
@ -37,6 +39,8 @@ class _LivePageState extends State<LivePage>
scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
StreamController<bool> searchBarStream =
Get.find<HomeController>().searchBarStream;
scrollController.addListener(
() {
if (scrollController.position.pixels >=
@ -51,8 +55,10 @@ class _LivePageState extends State<LivePage>
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
searchBarStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
searchBarStream.add(false);
}
},
);
@ -66,6 +72,7 @@ class _LivePageState extends State<LivePage>
@override
Widget build(BuildContext context) {
super.build(context);
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
@ -118,7 +125,7 @@ class _LivePageState extends State<LivePage>
},
),
),
const LoadingMore()
LoadingMore(ctr: _liveController)
],
),
),
@ -180,24 +187,3 @@ class _LivePageState extends State<LivePage>
);
}
}
class LoadingMore extends StatelessWidget {
const LoadingMore({super.key});
@override
Widget build(BuildContext context) {
return SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).padding.bottom + 80,
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Center(
child: Text(
'加载中...',
style: TextStyle(
color: Theme.of(context).colorScheme.outline, fontSize: 13),
),
),
),
);
}
}

View File

@ -52,7 +52,6 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
@override
Widget build(BuildContext context) {
final videoHeight = MediaQuery.of(context).size.width * 9 / 16;
Widget childWhenDisabled = Scaffold(
primary: true,
appBar: AppBar(

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:floating/floating.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/liveRoom/index.dart';
@ -43,10 +42,6 @@ class _BottomControlState extends State<BottomControl> {
@override
Widget build(BuildContext context) {
const textStyle = TextStyle(
color: Colors.white,
fontSize: 12,
);
return AppBar(
backgroundColor: Colors.transparent,
foregroundColor: Colors.white,

View File

@ -0,0 +1,204 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/login.dart';
import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart';
import 'package:pilipala/models/login/index.dart';
class LoginPageController extends GetxController {
final GlobalKey mobFormKey = GlobalKey<FormState>();
final GlobalKey passwordFormKey = GlobalKey<FormState>();
final GlobalKey msgCodeFormKey = GlobalKey<FormState>();
final TextEditingController mobTextController = TextEditingController();
final TextEditingController passwordTextController = TextEditingController();
final TextEditingController msgCodeTextController = TextEditingController();
final FocusNode mobTextFieldNode = FocusNode();
final FocusNode passwordTextFieldNode = FocusNode();
final FocusNode msgCodeTextFieldNode = FocusNode();
final PageController pageViewController = PageController();
RxInt currentIndex = 0.obs;
final Gt3FlutterPlugin captcha = Gt3FlutterPlugin();
// 默认密码登录
RxInt loginType = 0.obs;
// 监听pageView切换
void onPageChange(int index) {
currentIndex.value = index;
}
// 输入手机号 下一页
void nextStep() async {
if ((mobFormKey.currentState as FormState).validate()) {
await pageViewController.animateToPage(
1,
duration: const Duration(microseconds: 3000),
curve: Curves.easeInOut,
);
passwordTextFieldNode.requestFocus();
}
}
// 上一页
void previousPage() async {
passwordTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200));
pageViewController.animateToPage(
0,
duration: const Duration(microseconds: 300),
curve: Curves.easeInOut,
);
}
// 切换登录方式
void changeLoginType() {
loginType.value = loginType.value == 0 ? 1 : 0;
if (loginType.value == 0) {
passwordTextFieldNode.requestFocus();
} else {
msgCodeTextFieldNode.requestFocus();
}
}
// app端密码登录
void loginInByAppPassword() async {
if ((passwordFormKey.currentState as FormState).validate()) {
var webKeyRes = await LoginHttp.getWebKey();
if (webKeyRes['status']) {
String rhash = webKeyRes['data']['hash'];
String key = webKeyRes['data']['key'];
LoginHttp.loginInByMobPwd(
tel: mobTextController.text,
password: passwordTextController.text,
key: key,
rhash: rhash,
);
} else {
SmartDialog.showToast(webKeyRes['msg']);
}
}
}
// 验证码登录
void loginInByCode() {
if ((msgCodeFormKey.currentState as FormState).validate()) {}
}
// app端验证码
void getMsgCode() async {
getCaptcha((data) async {
CaptchaDataModel captchaData = data;
var res = await LoginHttp.sendAppSmsCode(
cid: 86,
tel: 13734077064,
token: captchaData.token!,
challenge: captchaData.geetest!.challenge!,
validate: captchaData.validate!,
seccode: captchaData.seccode!,
);
print(res);
});
}
// 申请极验验证码
Future getCaptcha(oncall) async {
SmartDialog.showLoading(msg: '请求中...');
var result = await LoginHttp.queryCaptcha();
if (result['status']) {
CaptchaDataModel captchaData = result['data'];
var registerData = Gt3RegisterData(
challenge: captchaData.geetest!.challenge,
gt: captchaData.geetest!.gt!,
success: true,
);
captcha.addEventHandler(onShow: (Map<String, dynamic> message) async {
SmartDialog.dismiss();
}, onClose: (Map<String, dynamic> message) async {
SmartDialog.showToast('关闭验证');
}, onResult: (Map<String, dynamic> message) async {
debugPrint("Captcha result: $message");
String code = message["code"];
if (code == "1") {
// 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询
SmartDialog.showToast('验证成功');
captchaData.validate = message['result']['geetest_validate'];
captchaData.seccode = message['result']['geetest_seccode'];
captchaData.geetest!.challenge =
message['result']['geetest_challenge'];
oncall(captchaData);
} else {
// 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried.
debugPrint("Captcha result code : $code");
}
}, onError: (Map<String, dynamic> message) async {
String code = message["code"];
// 处理验证中返回的错误 Handling errors returned in verification
if (Platform.isAndroid) {
// Android 平台
if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else if (code == "201") {
// 网络无法访问 Network inaccessible
} else if (code == "202") {
// Json 解析错误 Analysis error
} else if (code == "204") {
// WebView 加载超时,请检查是否混淆极验 SDK Load timed out
} else if (code == "204_1") {
// WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log
} else if (code == "204_2") {
// WebView 加载 SSLError
} else if (code == "206") {
// gettype 接口错误或返回为 null API error or return null
} else if (code == "207") {
// getphp 接口错误或返回为 null API error or return null
} else if (code == "208") {
// ajax 接口错误或返回为 null API error or return null
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/android
}
}
if (Platform.isIOS) {
// iOS 平台
if (code == "-1009") {
// 网络无法访问 Network inaccessible
} else if (code == "-1004") {
// 无法查找到 HOST Unable to find HOST
} else if (code == "-1002") {
// 非法的 URL Illegal URL
} else if (code == "-1001") {
// 网络超时 Network timeout
} else if (code == "-999") {
// 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation
} else if (code == "-21") {
// 使用了重复的 challenge Duplicate challenges are used
// 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached
} else if (code == "-20") {
// 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again
} else if (code == "-10") {
// 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification
} else if (code == "-2") {
// Dart 调用异常 Call exception
} else if (code == "-1") {
// Gt3RegisterData 参数不合法 Parameter is invalid
} else {
// 更多错误码参考开发文档 More error codes refer to the development document
// https://docs.geetest.com/sensebot/apirefer/errorcode/ios
}
}
});
captcha.startCaptcha(registerData);
} else {}
}
}

View File

@ -0,0 +1,4 @@
library login;
export './controller.dart';
export 'view.dart';

362
lib/pages/login/view.dart Normal file
View File

@ -0,0 +1,362 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'controller.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final LoginPageController _loginPageCtr = Get.put(LoginPageController());
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: Obx(
() => _loginPageCtr.currentIndex.value == 0
? IconButton(
onPressed: () async {
_loginPageCtr.mobTextFieldNode.unfocus();
await Future.delayed(const Duration(milliseconds: 200));
Get.back();
},
icon: const Icon(Icons.close_outlined),
)
: IconButton(
onPressed: () => _loginPageCtr.previousPage(),
icon: const Icon(Icons.arrow_back),
),
),
),
body: PageView(
physics: const NeverScrollableScrollPhysics(),
controller: _loginPageCtr.pageViewController,
onPageChanged: (int index) => _loginPageCtr.onPageChange(index),
children: [
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 10,
bottom: MediaQuery.of(context).padding.bottom + 10,
),
child: Form(
key: _loginPageCtr.mobFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text(
'登录',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
Row(
children: [
Text(
'请使用您的 BiliBili 账号登录。',
style: Theme.of(context).textTheme.titleSmall!,
),
GestureDetector(
onTap: () {},
child: const Icon(Icons.info_outline, size: 16),
)
],
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: TextFormField(
controller: _loginPageCtr.mobTextController,
focusNode: _loginPageCtr.mobTextFieldNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: '输入手机号码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "手机号码不能为空";
},
onSaved: (val) {
print(val);
},
onEditingComplete: () {
_loginPageCtr.nextStep();
},
),
),
GestureDetector(
onTap: () {
Get.offNamed(
'/webview',
parameters: {
'url':
'https://passport.bilibili.com/h5-app/passport/login',
'type': 'login',
'pageTitle': '登录bilibili',
},
);
},
child: Padding(
padding: const EdgeInsets.only(left: 2),
child: Text(
'使用网页端登录',
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(onPressed: () {}, child: const Text('中国大陆')),
TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
onPressed: () => _loginPageCtr.nextStep(),
child: const Text('下一步'),
)
],
),
],
),
),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 10,
bottom: MediaQuery.of(context).padding.bottom + 10,
),
child: Obx(
() => _loginPageCtr.loginType.value == 0
? Form(
key: _loginPageCtr.passwordFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Text(
'密码登录',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1);
}),
),
onPressed: () =>
_loginPageCtr.changeLoginType(),
icon: const Icon(Icons.swap_vert_outlined),
)
],
),
Text(
'请输入您的 BiliBili 密码。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: TextFormField(
controller: _loginPageCtr.passwordTextController,
focusNode: _loginPageCtr.passwordTextFieldNode,
keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration(
isDense: true,
labelText: '输入密码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "密码不能为空";
},
onSaved: (val) {
print(val);
},
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _loginPageCtr.previousPage(),
child: const Text('上一步'),
),
const SizedBox(width: 15),
TextButton(
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
onPressed: () =>
_loginPageCtr.loginInByAppPassword(),
child: const Text('确认登录'),
)
],
),
],
),
)
: Form(
key: _loginPageCtr.msgCodeFormKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Row(
children: [
Text(
'验证码登录',
style: Theme.of(context)
.textTheme
.titleLarge!
.copyWith(
letterSpacing: 1,
height: 2.1,
fontSize: 34,
fontWeight: FontWeight.w500),
),
const SizedBox(width: 4),
IconButton(
style: ButtonStyle(
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Theme.of(context)
.colorScheme
.primary
.withOpacity(0.1);
}),
),
onPressed: () =>
_loginPageCtr.changeLoginType(),
icon: const Icon(Icons.swap_vert_outlined),
)
],
),
Text(
'请输入收到到验证码。',
style: Theme.of(context).textTheme.titleSmall!,
),
Container(
margin: const EdgeInsets.only(top: 38, bottom: 15),
child: Stack(
children: [
TextFormField(
controller:
_loginPageCtr.msgCodeTextController,
focusNode: _loginPageCtr.msgCodeTextFieldNode,
maxLength: 6,
keyboardType: TextInputType.number,
decoration: InputDecoration(
isDense: true,
labelText: '输入验证码',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6.0),
),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty
? null
: "验证码不能为空";
},
onSaved: (val) {
print(val);
},
),
Positioned(
right: 8,
top: 4,
child: Center(
child: TextButton(
onPressed: () =>
_loginPageCtr.getMsgCode(),
child: const Text('获取验证码'),
),
),
),
],
),
),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => _loginPageCtr.previousPage(),
child: const Text('上一步'),
),
const SizedBox(width: 15),
TextButton(
style: TextButton.styleFrom(
padding:
const EdgeInsets.fromLTRB(20, 0, 20, 0),
foregroundColor:
Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
onPressed: () => _loginPageCtr.loginInByCode(),
child: const Text('确认登录'),
)
],
),
],
),
),
),
),
],
),
);
}
}

View File

@ -55,6 +55,7 @@ class MainController extends GetxController {
StreamController<bool>.broadcast();
Box setting = GStrorage.setting;
DateTime? _lastPressedAt;
late bool hideTabBar;
@override
void onInit() {
@ -62,6 +63,7 @@ class MainController extends GetxController {
if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) {
Utils.checkUpdata();
}
hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true);
}
Future<bool> onBackPressed(BuildContext context) {

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
@ -29,6 +31,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
late Animation<double>? _slideAnimation;
int selectedIndex = 0;
int? _lastSelectTime; //上次点击时间
Box setting = GStrorage.setting;
late bool enableMYBar;
@override
void initState() {
@ -45,6 +49,7 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
Tween(begin: 0.8, end: 1.0).animate(_animationController!);
_lastSelectTime = DateTime.now().millisecondsSinceEpoch;
_pageController = PageController(initialPage: selectedIndex);
enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true);
}
void setIndex(int value) async {
@ -110,8 +115,8 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
MediaQuery.of(context).size.width * 9 / 16;
localCache.put('sheetHeight', sheetHeight);
localCache.put('statusBarHeight', statusBarHeight);
return WillPopScope(
onWillPop: () => _mainController.onBackPressed(context),
return PopScope(
onPopInvoked: (bool status) => _mainController.onBackPressed(context),
child: Scaffold(
extendBody: true,
body: FadeTransition(
@ -139,26 +144,45 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
),
),
bottomNavigationBar: StreamBuilder(
stream: _mainController.bottomBarStream.stream,
stream: _mainController.hideTabBar
? _mainController.bottomBarStream.stream
: StreamController<bool>.broadcast().stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 1000),
duration: const Duration(milliseconds: 500),
offset: Offset(0, snapshot.data ? 0 : 1),
child: NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: e['icon'],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
child: enableMYBar
? NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return NavigationDestination(
icon: e['icon'],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
)
: BottomNavigationBar(
currentIndex: selectedIndex,
onTap: (value) => setIndex(value),
iconSize: 16,
selectedFontSize: 12,
unselectedFontSize: 12,
items: [
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
icon: e['icon'],
activeIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),
],
),
);
},
),

View File

@ -1,4 +0,0 @@
library archive_panel;
export './controller.dart';
export 'index.dart';

View File

@ -1,240 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/pages/member/archive/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pull_to_refresh_notification/pull_to_refresh_notification.dart';
class ArchivePanel extends StatefulWidget {
final int? mid;
const ArchivePanel({super.key, this.mid});
@override
State<ArchivePanel> createState() => _ArchivePanelState();
}
class _ArchivePanelState extends State<ArchivePanel>
with AutomaticKeepAliveClientMixin {
DateTime lastRefreshTime = DateTime.now();
late final LoadMoreListSource source;
late final ArchiveController _archiveController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
print('🐶🐶: ${widget.mid}');
_archiveController = Get.put(ArchiveController(widget.mid),
tag: Utils.makeHeroTag(widget.mid));
source = LoadMoreListSource(_archiveController);
}
@override
Widget build(BuildContext context) {
super.build(context);
return PullToRefreshNotification(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1));
return true;
},
maxDragOffset: 50,
child: GlowNotificationWidget(
Column(
children: <Widget>[
// 下拉刷新指示器
// PullToRefreshContainer(
// (PullToRefreshScrollNotificationInfo? info) {
// return PullToRefreshHeader(info, lastRefreshTime);
// },
// ),
Padding(
padding:
const EdgeInsets.only(left: 14, top: 8, bottom: 8, right: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('排序方式'),
SizedBox(
height: 35,
width: 85,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
// _archiveController.order = 'click';
// _archiveController.pn = 1;
_archiveController.toggleSort();
source.refresh(true);
// LoadMoreListSource().loadData();
},
child: Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
_archiveController.currentOrder['label']!,
key: ValueKey<String>(
_archiveController.currentOrder['label']!),
),
),
),
),
),
],
),
),
Expanded(
child: LoadingMoreList<VListItemModel>(
ListConfig<VListItemModel>(
sourceList: source,
itemBuilder:
(BuildContext c, VListItemModel item, int index) {
if (index == 0) {
return Column(
children: [
const SizedBox(height: 6),
VideoCardH(videoItem: item)
],
);
} else {
return VideoCardH(videoItem: item);
}
},
indicatorBuilder: _buildIndicator,
),
),
)
],
),
showGlowLeading: false,
),
);
}
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
TextStyle style =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
Widget? widget;
switch (status) {
case IndicatorStatus.none:
widget = Container(height: 0.0);
break;
case IndicatorStatus.loadingMoreBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(true, widget);
break;
case IndicatorStatus.error:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(true, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.noMoreLoad:
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.empty:
widget = Text('用户没有投稿', style: style);
widget = _setbackground(true, widget);
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, {double height = 100}) {
widget = Padding(
padding: height == double.infinity
? EdgeInsets.zero
: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Container(
width: double.infinity,
height: height,
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: widget,
),
);
return widget;
}
Widget getIndicator(BuildContext context) {
final TargetPlatform platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? const CupertinoActivityIndicator(
animating: true,
radius: 16.0,
)
: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}
class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
late ArchiveController ctr;
LoadMoreListSource(this.ctr);
bool forceRefresh = false;
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await ctr.getMemberArchive();
if (res['status']) {
if (ctr.pn == 2) {
clear();
}
addAll(res['data'].list.vlist);
}
if (length < res['data'].page['count']) {
isSuccess = true;
} else {
isSuccess = false;
}
return isSuccess;
}
@override
Future<bool> refresh([bool clearBeforeRequest = false]) async {
// _hasMore = true;
// pageindex = 1;
// //force to refresh list when you don't want clear list before request
// //for the case, if your list already has 20 items.
forceRefresh = !clearBeforeRequest;
var result = await super.refresh(clearBeforeRequest);
forceRefresh = false;
return result;
}
}

View File

@ -6,6 +6,7 @@ import 'package:pilipala/http/member.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart';
@ -13,16 +14,17 @@ import 'package:share_plus/share_plus.dart';
class MemberController extends GetxController {
late int mid;
Rx<MemberInfoModel> memberInfo = MemberInfoModel().obs;
Map? userStat;
late Map userStat;
RxString face = ''.obs;
String? heroTag;
Box userInfoCache = GStrorage.userInfo;
late int ownerMid;
// 投稿列表
RxList<VListItemModel>? archiveList = [VListItemModel()].obs;
var userInfo;
dynamic userInfo;
RxInt attribute = (-1).obs;
RxString attributeText = '关注'.obs;
RxList<MemberCoinsDataModel> recentCoinsList = <MemberCoinsDataModel>[].obs;
@override
void onInit() {
@ -38,6 +40,7 @@ class MemberController extends GetxController {
// 获取用户信息
Future<Map<String, dynamic>> getInfo() async {
await getMemberStat();
await getMemberView();
var res = await MemberHttp.memberInfo(mid: mid);
if (res['status']) {
memberInfo.value = res['data'];
@ -55,13 +58,14 @@ class MemberController extends GetxController {
return res;
}
// Future getMemberCardInfo() async {
// var res = await MemberHttp.memberCardInfo(mid: mid);
// if (res['status']) {
// print(userStat);
// }
// return res;
// }
// 获取用户播放数 获赞数
Future<Map<String, dynamic>> getMemberView() async {
var res = await MemberHttp.memberView(mid: mid);
if (res['status']) {
userStat.addAll(res['data']);
}
return res;
}
// 关注/取关up
Future actionRelationMod() async {
@ -173,4 +177,35 @@ class MemberController extends GetxController {
void shareUser() {
Share.share('${memberInfo.value.name} - https://space.bilibili.com/$mid');
}
// 请求专栏
Future getMemberSeasons() async {
if (userInfo == null) return;
var res = await MemberHttp.getMemberSeasons(mid, 1, 10);
if (!res['status']) {
SmartDialog.showToast("用户专栏请求异常:${res['msg']}");
}
return res;
}
// 请求投币视频
Future getRecentCoinVideo() async {
if (userInfo == null) return;
var res = await MemberHttp.getRecentCoinVideo(mid: mid);
recentCoinsList.value = res['data'];
return res;
}
// 跳转查看动态
void pushDynamicsPage() => Get.toNamed('/memberDynamics?mid=$mid');
// 跳转查看投稿
void pushArchivesPage() => Get.toNamed('/memberArchive?mid=$mid');
// 跳转查看专栏
void pushSeasonsPage() {}
// 跳转查看最近投币
void pushRecentCoinsPage() async {
if (recentCoinsList.isNotEmpty) {}
}
}

View File

@ -1,26 +0,0 @@
import 'package:get/get.dart';
import 'package:pilipala/http/member.dart';
class MemberDynamicPanelController extends GetxController {
MemberDynamicPanelController(this.mid);
int? mid;
String offset = '';
int count = 0;
@override
void onInit() {
super.onInit();
mid ??= int.parse(Get.parameters['mid']!);
}
Future getMemberDynamic() async {
var res = await MemberHttp.memberDynamic(
offset: offset,
mid: mid,
);
if (res['status']) {
offset = res['data'].offset;
}
return res;
}
}

View File

@ -1,149 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:pilipala/utils/utils.dart';
import 'controller.dart';
class MemberDynamicPanel extends StatefulWidget {
final int? mid;
const MemberDynamicPanel({super.key, this.mid});
@override
State<MemberDynamicPanel> createState() => _MemberDynamicPanelState();
}
class _MemberDynamicPanelState extends State<MemberDynamicPanel>
with AutomaticKeepAliveClientMixin {
DateTime lastRefreshTime = DateTime.now();
late final LoadMoreListSource source;
late final MemberDynamicPanelController _dynamicController;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
_dynamicController = Get.put(MemberDynamicPanelController(widget.mid),
tag: Utils.makeHeroTag(widget.mid));
source = LoadMoreListSource(_dynamicController);
}
@override
Widget build(BuildContext context) {
super.build(context);
return LoadingMoreList<DynamicItemModel>(
ListConfig<DynamicItemModel>(
sourceList: source,
itemBuilder: (BuildContext c, DynamicItemModel item, int index) {
return DynamicPanel(item: item);
},
indicatorBuilder: _buildIndicator,
),
);
}
Widget _buildIndicator(BuildContext context, IndicatorStatus status) {
TextStyle style =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
Widget? widget;
switch (status) {
case IndicatorStatus.none:
widget = Container(height: 0.0);
break;
case IndicatorStatus.loadingMoreBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.fullScreenBusying:
widget = Text('加载中...', style: style);
widget = _setbackground(true, widget);
break;
case IndicatorStatus.error:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.fullScreenError:
/// TODO 异常逻辑
widget = Text('没有更多了', style: style);
widget = _setbackground(true, widget);
widget = GestureDetector(
onTap: () {},
child: widget,
);
break;
case IndicatorStatus.noMoreLoad:
widget = Text('没有更多了', style: style);
widget = _setbackground(false, widget, height: 60.0);
break;
case IndicatorStatus.empty:
widget = Text('用户没有投稿', style: style);
widget = _setbackground(true, widget);
break;
}
return widget;
}
Widget _setbackground(bool full, Widget widget, {double height = 100}) {
widget = Padding(
padding: height == double.infinity
? EdgeInsets.zero
: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Container(
width: double.infinity,
height: height,
color: Theme.of(context).colorScheme.background,
alignment: Alignment.center,
child: widget,
),
);
return widget;
}
Widget getIndicator(BuildContext context) {
final TargetPlatform platform = Theme.of(context).platform;
return platform == TargetPlatform.iOS
? const CupertinoActivityIndicator(
animating: true,
radius: 16.0,
)
: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
);
}
}
class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
late MemberDynamicPanelController ctr;
LoadMoreListSource(this.ctr);
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await ctr.getMemberDynamic();
if (res['status']) {
addAll(res['data'].items);
}
if (res['data'].hasMore) {
isSuccess = true;
} else {
isSuccess = false;
}
return isSuccess;
}
}

View File

@ -1,16 +1,16 @@
import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/member/archive/view.dart';
import 'package:pilipala/pages/member/dynamic/index.dart';
import 'package:pilipala/pages/member/index.dart';
import 'package:pilipala/utils/utils.dart';
import 'widgets/conis.dart';
import 'widgets/profile.dart';
import 'widgets/seasons.dart';
class MemberPage extends StatefulWidget {
const MemberPage({super.key});
@ -23,9 +23,10 @@ class _MemberPageState extends State<MemberPage>
with SingleTickerProviderStateMixin {
late String heroTag;
late MemberController _memberController;
Future? _futureBuilderFuture;
late Future _futureBuilderFuture;
late Future _memberSeasonsFuture;
late Future _memberCoinsFuture;
final ScrollController _extendNestCtr = ScrollController();
late TabController _tabController;
final StreamController<bool> appbarStream = StreamController<bool>();
late int mid;
@ -35,12 +36,13 @@ class _MemberPageState extends State<MemberPage>
mid = int.parse(Get.parameters['mid']!);
heroTag = Get.arguments['heroTag'] ?? Utils.makeHeroTag(mid);
_memberController = Get.put(MemberController(), tag: heroTag);
_tabController = TabController(length: 3, vsync: this, initialIndex: 2);
_futureBuilderFuture = _memberController.getInfo();
_memberSeasonsFuture = _memberController.getMemberSeasons();
_memberCoinsFuture = _memberController.getRecentCoinVideo();
_extendNestCtr.addListener(
() {
double offset = _extendNestCtr.position.pixels;
if (offset > 230) {
if (offset > 100) {
appbarStream.add(true);
} else {
appbarStream.add(false);
@ -59,183 +61,222 @@ class _MemberPageState extends State<MemberPage>
Widget build(BuildContext context) {
return Scaffold(
primary: true,
body: ExtendedNestedScrollView(
controller: _extendNestCtr,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
pinned: false,
primary: true,
elevation: 0,
scrolledUnderElevation: 1,
forceElevated: innerBoxIsScrolled,
expandedHeight: 290,
titleSpacing: 0,
title: StreamBuilder(
stream: appbarStream.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
body: Column(
children: [
AppBar(
title: StreamBuilder(
stream: appbarStream.stream,
initialData: false,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 500),
child: Row(
children: [
Row(
children: [
Obx(
() => NetworkImgLayer(
width: 35,
height: 35,
type: 'avatar',
src: _memberController.face.value,
),
const SizedBox(width: 10),
Obx(
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
fontSize: 14),
),
),
const SizedBox(width: 10),
Obx(
() => Text(
_memberController.memberInfo.value.name ?? '',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
fontSize: 14),
),
],
)
],
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid !=
_memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
),
],
)
],
),
);
},
),
actions: [
IconButton(
onPressed: () => Get.toNamed(
'/memberSearch?mid=${Get.parameters['mid']}&uname=${_memberController.memberInfo.value.name!}'),
icon: const Icon(Icons.search_outlined),
),
PopupMenuButton(
icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
if (_memberController.ownerMid != _memberController.mid) ...[
PopupMenuItem(
onTap: () => _memberController.shareUser(),
onTap: () => _memberController.blockUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined, size: 19),
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid !=
_memberController.mid
? '分享UP主'
: '分享我的主页'),
Text(_memberController.attribute.value != 128
? '加入黑名单'
: '移除黑名单'),
],
),
),
)
],
PopupMenuItem(
onTap: () => _memberController.shareUser(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined, size: 19),
const SizedBox(width: 10),
Text(_memberController.ownerMid != _memberController.mid
? '分享UP主'
: '分享我的主页'),
],
),
),
],
),
const SizedBox(width: 4),
],
),
Expanded(
child: SingleChildScrollView(
controller: _extendNestCtr,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 20,
),
const SizedBox(width: 4),
],
flexibleSpace: FlexibleSpaceBar(
background: Stack(
child: Column(
children: [
profileWidget(),
/// 动态链接
ListTile(
onTap: _memberController.pushDynamicsPage,
title: const Text('Ta的动态'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
/// 视频
ListTile(
onTap: _memberController.pushArchivesPage,
title: const Text('Ta的投稿'),
trailing:
const Icon(Icons.arrow_forward_outlined, size: 19),
),
/// 专栏
ListTile(
onTap: () {},
title: const Text('Ta的专栏'),
),
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberSeasonsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
if (data['data'].seasonsList.isEmpty) {
return commenWidget('用户没有设置专栏');
} else {
return MemberSeasonsPanel(data: data['data']);
}
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
/// 收藏
/// 追番
/// 最近投币
Obx(
() => _memberController.face.value != ''
? Positioned.fill(
bottom: 10,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fitWidth,
image: NetworkImage(
_memberController.face.value),
alignment: Alignment.topCenter,
isAntiAlias: true,
),
),
foregroundDecoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Theme.of(context)
.colorScheme
.background
.withOpacity(0.44),
Theme.of(context).colorScheme.background,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0, 0.46],
),
),
),
() => _memberController.recentCoinsList.isNotEmpty
? ListTile(
onTap: () {},
title: const Text('最近投币的视频'),
// trailing: const Icon(Icons.arrow_forward_outlined,
// size: 19),
)
: const SizedBox(),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
height: 20,
child: Container(
color: Theme.of(context).colorScheme.background,
MediaQuery.removePadding(
removeTop: true,
removeBottom: true,
context: context,
child: Padding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace,
right: StyleString.safeSpace,
),
child: FutureBuilder(
future: _memberCoinsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
if (snapshot.data['status']) {
Map data = snapshot.data as Map;
return MemberCoinsPanel(data: data['data']);
} else {
// 请求错误
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
),
profileWidget(),
// 最近点赞
// ListTile(
// onTap: () {},
// title: const Text('最近点赞的视频'),
// trailing:
// const Icon(Icons.arrow_forward_outlined, size: 19),
// ),
],
),
),
),
];
},
pinnedHeaderSliverHeightBuilder: () {
return MediaQuery.of(context).padding.top + kToolbarHeight;
},
onlyOneScrollInBody: true,
body: Column(
children: [
SizedBox(
width: double.infinity,
height: 50,
child: TabBar(controller: _tabController, tabs: const [
Tab(text: '主页'),
Tab(text: '动态'),
Tab(text: '投稿'),
]),
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
const Text('主页'),
MemberDynamicPanel(mid: mid),
ArchivePanel(mid: mid),
],
))
],
),
),
],
),
);
}
Widget profileWidget() {
return Padding(
padding: const EdgeInsets.only(left: 18, right: 18),
padding: const EdgeInsets.only(left: 18, right: 18, bottom: 20),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
@ -250,7 +291,7 @@ class _MemberPageState extends State<MemberPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
profile(_memberController),
const SizedBox(height: 14),
const SizedBox(height: 20),
Row(
children: [
Flexible(
@ -260,7 +301,7 @@ class _MemberPageState extends State<MemberPage>
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyLarge!
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
)),
const SizedBox(width: 2),
@ -332,29 +373,11 @@ class _MemberPageState extends State<MemberPage>
softWrap: true,
),
],
const SizedBox(height: 4),
const SizedBox(height: 6),
if (_memberController.memberInfo.value.sign != '')
SelectableText(
_memberController.memberInfo.value.sign!,
maxLines: _memberController
.memberInfo.value.official!['title'] !=
''
? 1
: 2,
style: const TextStyle(
overflow: TextOverflow.ellipsis),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
content: SelectableText(_memberController
.memberInfo.value.sign!),
);
},
);
},
)
),
],
),
],
@ -371,4 +394,22 @@ class _MemberPageState extends State<MemberPage>
),
);
}
Widget commenWidget(msg) {
return Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 30,
),
child: Center(
child: Text(
msg,
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/models/member/coin.dart';
import 'package:pilipala/pages/member_coin/widgets/item.dart';
class MemberCoinsPanel extends StatelessWidget {
final List<MemberCoinsDataModel>? data;
const MemberCoinsPanel({super.key, this.data});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, boxConstraints) {
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // Use a fixed count for GridView
crossAxisSpacing: StyleString.safeSpace,
mainAxisSpacing: StyleString.safeSpace,
childAspectRatio: 0.94,
),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: data!.length,
itemBuilder: (context, i) {
return MemberCoinsItem(coinItem: data![i]);
},
);
},
);
}
}

Some files were not shown because too many files have changed in this diff Show More