Compare commits

..

298 Commits

Author SHA1 Message Date
cb3fd24cf7 merge main 2023-11-12 12:09:16 +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
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
86c87dc1d5 fix: 首页动态 2023-10-16 00:06:36 +08:00
3d6c270070 fix: request github ua 2023-10-15 23:57:22 +08:00
f214c45448 v1.0.9 更新 2023-10-15 23:10:07 +08:00
5f26e19c62 Merge branch 'feature-media_kit' into alpha 2023-10-15 22:55:12 +08:00
960104929f Merge branch 'design' into alpha 2023-10-15 20:05:38 +08:00
f25dab2eb8 mod: 修改版本检查规则 2023-10-15 20:05:16 +08:00
15947e45da fix: 视频播放速度超过4.0就没有声音了 issues #191 2023-10-15 17:02:13 +08:00
6c983cf849 Merge branch 'fix' into alpha 2023-10-15 16:49:38 +08:00
4d5f3eb14a fix: 动态ADDITIONAL_TYPE_RESERVE异常 2023-10-15 16:49:20 +08:00
94aef39f7b Merge branch 'design' into alpha 2023-10-14 22:59:42 +08:00
424bdd9fff mod: issues #177 2023-10-14 22:59:25 +08:00
c77c8e683d Merge branch 'design' into alpha 2023-10-14 22:15:16 +08:00
ad1ced51f2 mod: 个人主页签名溢出 issues #154 2023-10-14 22:14:55 +08:00
c0c1a3a59a Merge branch 'design' into alpha 2023-10-14 21:34:01 +08:00
690b168a45 feat: 收藏夹搜索 issues #95 2023-10-14 21:33:40 +08:00
7f7919d585 Merge branch 'fix' into alpha 2023-10-14 16:18:49 +08:00
76974bd874 fix: 黑名单数量 2023-10-14 16:18:26 +08:00
f8173b0b5f Merge branch 'fix' into alpha 2023-10-14 15:46:47 +08:00
7ecfbac786 fix: issues #166 2023-10-14 15:46:03 +08:00
c794eb465c Merge branch 'design' into alpha 2023-10-14 14:11:46 +08:00
7adbf76362 Merge branch 'fix' into alpha 2023-10-14 14:11:34 +08:00
353287e053 mod: 动态图片渲染 2023-10-14 14:11:16 +08:00
856d699fd7 fix: 历史记录内容溢出 2023-10-14 11:48:50 +08:00
15914e5961 Merge branch 'design' into alpha 2023-10-11 23:49:32 +08:00
0e5b1633be feat: 默认倍速、自定义倍速 2023-10-11 23:49:13 +08:00
24f22f8afa Merge branch 'feature-media_kit' into alpha 2023-10-10 00:05:43 +08:00
7c38340fc6 mod: 快进范围一分半 2023-10-10 00:05:20 +08:00
79f661e5da Merge branch 'design' into alpha 2023-10-09 08:29:56 +08:00
3ba90d6c85 Merge branch 'feature-media_kit' into alpha 2023-10-09 08:24:49 +08:00
85e86f1d61 mod: 优化快进手势阈值 2023-10-09 08:24:07 +08:00
ec58d060bf fix: 滑动快进过快 2023-10-08 23:39:21 +08:00
7576f39010 mod: 弹幕速度 2023-10-08 23:24:22 +08:00
77f47b8242 fix: 历史记录删除失败 2023-10-08 22:44:49 +08:00
4b3e791370 mod: 历史记录删除逻辑 2023-10-08 22:34:29 +08:00
f25f5c28d9 merge design 2023-10-05 10:53:56 +08:00
7ca367869b Merge branch 'fix' into alpha 2023-10-05 10:30:26 +08:00
7222ca4425 fix: 动态数据类型 2023-10-05 10:29:39 +08:00
692d596818 feat: 自定义列数 2023-10-04 23:15:46 +08:00
47e3cf46e4 fix: 倍速未还原、上一视频无法记忆播放 2023-10-03 22:12:11 +08:00
6b2229dddc Merge branch 'feature-media_kit' into alpha 2023-10-02 10:06:38 +08:00
1e202979d3 mod: 升级播放器依赖 2023-10-02 10:06:04 +08:00
82ad1662aa Merge branch 'main' into feature-media_kit 2023-10-02 10:03:49 +08:00
7feda8d187 Merge branch 'design' into alpha 2023-10-01 21:13:11 +08:00
d83b4bc59e feat: 历史记录搜索 issues #27 2023-10-01 21:12:18 +08:00
1d1d4f8c7d Merge branch 'design' into alpha 2023-10-01 15:53:47 +08:00
1061ffca3d feat: 免登录观看1080P视频(默认开启) issues #149 2023-10-01 15:53:27 +08:00
52ee5b36be mod: 历史记录多选选中样式 2023-10-01 14:28:57 +08:00
87807466ff Merge branch 'design' into alpha 2023-10-01 11:46:50 +08:00
227cfb637e Merge branch 'fix' into alpha 2023-10-01 11:46:44 +08:00
2c4ee083ef mod: 媒体库收藏夹样式溢出 2023-10-01 11:46:28 +08:00
2ef3a8cd25 feat: 历史记录多选删除 2023-10-01 10:35:03 +08:00
3a19b089c5 Merge branch 'fix' into alpha 2023-09-29 23:14:10 +08:00
21e6d1aa52 Merge branch 'design' into alpha 2023-09-29 23:13:54 +08:00
10965fae73 fix: 评论区视频链接跳转 2023-09-29 23:13:28 +08:00
8f987e8352 feat: 视频动态稍后再看功能 issues #150 2023-09-29 22:17:31 +08:00
4e147b6f18 feat: 按分组查看up issues #150 2023-09-29 21:41:01 +08:00
6d982bdba2 feat: 视频详情页关注分组 2023-09-28 19:58:00 +08:00
3edce0c4ec Merge branch 'feature-media_kit' into alpha 2023-09-28 10:08:38 +08:00
2eb7b388e1 mod: 播放器插件升级 2023-09-28 10:08:11 +08:00
2fd23aa20d fix: 搜索视频标题显示乱码 issues #165 2023-09-27 23:38:00 +08:00
2ecd1d3dab feat: 视频搜索黑名单屏蔽 2023-09-27 23:30:15 +08:00
26d8ab5b43 feat: 播放顺序、视频详情操作栏样式 2023-09-26 22:51:21 +08:00
8fa59f8f58 fix: issues #135 2023-09-24 01:20:34 +08:00
6ea4626288 Merge branch 'design' into alpha 2023-09-24 00:57:44 +08:00
329f158155 Merge branch 'fix' into alpha 2023-09-24 00:57:09 +08:00
d6b6df3eed fix: issues #157 2023-09-24 00:56:55 +08:00
3f50aab12d mod: 关闭弹幕时停止判断 2023-09-24 00:47:45 +08:00
227da31857 Merge branch 'design' into alpha 2023-09-24 00:34:36 +08:00
7ad6b25abe feat: UP主投稿搜索 2023-09-24 00:34:20 +08:00
f79e4765c2 feat: UP主投稿排序查询 2023-09-23 00:48:22 +08:00
e8671dee6b Merge branch 'design' into alpha 2023-09-20 23:53:57 +08:00
97268c36dc feat: 首页刷新逻辑 issues #133 2023-09-20 23:52:04 +08:00
d1272efad4 mod: 补充链接打开形式 2023-09-20 23:31:11 +08:00
ba815bccda feat: 视频简介链接匹配 2023-09-20 23:26:03 +08:00
75fb81b959 Merge branch 'design' into alpha 2023-09-19 23:58:46 +08:00
a48d15ee73 feat: 转发的投稿动态跳转 2023-09-19 23:58:30 +08:00
620d7214df Merge branch 'fix' into alpha 2023-09-19 23:39:19 +08:00
7458c33173 fix: 屏幕帧率 issues #99 #115 2023-09-19 23:39:00 +08:00
97fa047c60 feat: 搜索专栏 2023-09-19 00:06:00 +08:00
9f4b928257 Merge branch 'fix' into alpha 2023-09-18 23:52:14 +08:00
afcc5a9a02 fix: 删除专栏、直播的历史记录 2023-09-18 23:44:35 +08:00
7181db66bd fix: 我的页面跳转粉丝页 2023-09-18 23:29:17 +08:00
820a1e9162 Merge branch 'fix' 2023-09-18 07:55:51 +08:00
566f75f760 mod: 优化未登录时用户跳转 2023-09-18 07:55:31 +08:00
4c49f466db update .lock 2023-09-18 07:46:35 +08:00
8f97431665 fix: 视频全屏时的安全区域 2023-09-18 07:39:56 +08:00
f543be562a fix: 关注/粉丝/全部跳转异常、个人主页请求异常 2023-09-18 07:18:07 +08:00
93383a5c65 v1.0.8 更新 2023-09-17 23:51:14 +08:00
fd57ebc4cc Merge branch 'design' into alpha 2023-09-17 23:49:50 +08:00
9de9b885bc mod: 隐藏搜索专栏 2023-09-17 23:49:34 +08:00
c2db6a50f0 merge fix 2023-09-17 23:39:51 +08:00
427bd2eb79 fix: 自动更新请求异常 2023-09-17 23:37:17 +08:00
8c01de47e4 Merge branch 'fix' into alpha 2023-09-17 22:58:25 +08:00
83b27e7231 fix: 退出pip评论空白 2023-09-17 22:57:54 +08:00
fc767fab97 Merge branch 'fix' into alpha 2023-09-17 22:41:24 +08:00
7db9d290f5 fix: 未开启自动播放时历史记录00:00 2023-09-17 22:41:04 +08:00
bb66de29d4 Merge branch 'fix' into alpha 2023-09-17 22:30:46 +08:00
dd97636494 fix: 弹幕数量少于实际数量&优化弹幕请求 issues #78 2023-09-17 22:30:22 +08:00
95f5ac6a71 Merge branch 'fix' into alpha 2023-09-17 20:07:48 +08:00
7fa7152245 fix: 用户页异常&头像渲染、搜索建议词 2023-09-17 20:07:26 +08:00
41df90561b fix: 记录弹幕屏蔽设置 2023-09-17 14:32:07 +08:00
5c68772f7b fix: toast显示、seek后历史记录不请求 2023-09-17 14:20:11 +08:00
262f244a98 Merge branch 'fix' into alpha 2023-09-17 13:01:12 +08:00
c91cfedfe2 mod: 直播列表刷新逻辑&样式 2023-09-17 12:53:37 +08:00
1d9372b4f1 fix: 个人主页、关注、粉丝页面渲染异常issues #91 2023-09-16 23:14:11 +08:00
e9095932ed fix: 视频简介渲染异常、二楼新回复的评论渲染异常 2023-09-16 22:51:04 +08:00
3daa06a198 feat: 专栏文章渲染 2023-09-16 21:47:27 +08:00
380ada9ae0 Merge branch 'design' into alpha 2023-09-16 14:32:31 +08:00
76bd5550c7 Merge branch 'fix' into alpha 2023-09-16 01:18:45 +08:00
54c66d54da fix: 热搜词渲染空白 2023-09-16 01:18:25 +08:00
481d5e77d7 fix: 搜索建议词为空 2023-09-15 00:08:39 +08:00
33413cdb51 fix: 第三方登录重定向、有效状态码整理 2023-09-14 23:35:08 +08:00
277c7a25cb feat: 搜索结果增加专栏 issues #112 2023-09-13 22:50:08 +08:00
2c9b3e8854 Merge branch 'design' into alpha 2023-09-12 22:51:02 +08:00
fff54a55a1 feat: 用户拉黑功能 issues #107 2023-09-12 22:49:59 +08:00
838467451b feat: 动态主楼评论 2023-09-12 18:54:56 +08:00
8803fbd777 Merge branch 'fix' into alpha 2023-09-12 16:04:50 +08:00
7c9b5bb891 Merge branch 'design' into alpha 2023-09-12 11:29:28 +08:00
7867af0f85 feat: 图片保存到PiliPala目录(Android) issues #94 2023-09-12 00:11:16 +08:00
6d2e0f2049 fix: audio null 、 播放器面板响应式优化 2023-09-11 23:21:13 +08:00
74ec4cccea fix: 全屏时忽略左右安全区域 issues #80 2023-09-11 18:06:28 +08:00
bd568c4945 Merge branch 'design' into alpha 2023-09-11 17:50:01 +08:00
097ab4310a merge fix 2023-09-11 17:49:46 +08:00
09ff01905e feat: 移除黑名单、隐藏黑名单上限显示 issues #90 2023-09-11 17:46:27 +08:00
ef38844798 feat: 删除已观看历史记录 issues #81 2023-09-11 17:36:05 +08:00
1922a91575 fix: 番剧详情渲染错误 2023-09-11 16:09:07 +08:00
3c17d18acf fix: 第三方登录302重定向失效、ua获取 2023-09-11 15:41:53 +08:00
4cf2fc3c23 fix: firstAudio Bad element 2023-09-10 00:15:40 +08:00
b9a47da92b fix: 未开启自动播放时播放按钮丢失 issues #82 2023-09-10 00:03:22 +08:00
c16106d676 fix: 自动全屏时headerControl丢失 issues #79 2023-09-09 23:52:29 +08:00
0e39453558 fix: 详情页hero取值、请求contentType 2023-09-09 09:41:42 +08:00
8ff4259972 v1.0.7 更新 2023-09-08 22:01:25 +08:00
5082dc6d59 mod: 直播功能注释 2023-09-08 21:51:29 +08:00
627df8e6ad Merge branch 'feature-fullScreen' 2023-09-08 21:04:01 +08:00
2467fd0dea mod: 合并design htmlRender 2023-09-08 18:38:23 +08:00
c6f6af4628 Merge branch 'fix' into alpha 2023-09-08 18:35:36 +08:00
9e907f9151 fix: 转发动态点击、动态详情页加tag 2023-09-08 18:35:05 +08:00
22e17d437b fix: 动态内容图片预览 2023-09-08 18:04:13 +08:00
8a06ce65a5 Merge branch 'fix' into alpha 2023-09-08 17:21:33 +08:00
72ff3fdab0 fix and fix dynamics render 2023-09-08 17:21:11 +08:00
517ca032d2 feat: 动态页自渲染 2023-09-08 16:46:51 +08:00
396f9fbbac Merge branch 'design' into alpha 2023-09-08 13:19:24 +08:00
c0332c74d7 mod: 图片预览优化 issues #61 2023-09-08 13:19:11 +08:00
2669b41ede mod: 修改全屏方式 issues #59 2023-09-07 23:24:05 +08:00
81dace96d7 Merge branch 'design' into alpha 2023-09-07 22:57:12 +08:00
d693d7ad6c Merge branch 'fix' into alpha 2023-09-07 22:57:06 +08:00
8c02a566f6 mod: 还原音轨加载方式(某些资源无声) 2023-09-07 22:56:52 +08:00
a2420d0bef fix: 默认音轨质量 2023-09-07 22:52:46 +08:00
29d3f78da9 Merge branch 'fix' into alpha 2023-09-07 20:31:59 +08:00
a864bea3f4 fix: 专栏动态内容重复 2023-09-07 20:31:45 +08:00
070156da86 Merge branch 'fix' into alpha 2023-09-07 20:04:12 +08:00
69f846760d fix: 尝试解决网络异常导致的白屏 issues #67 2023-09-07 20:03:48 +08:00
2ca79003bf Merge branch 'feature-pip' into alpha 2023-09-07 19:26:00 +08:00
18af065a1e Merge branch 'design' into alpha 2023-09-07 19:25:49 +08:00
0ad54d8c0b mod: 推荐视频增加时长显示 issues #71、推荐栏刷新逻辑 2023-09-07 19:25:35 +08:00
0dfcd4ed40 feat: 视频、直播pip Android端 2023-09-07 18:58:58 +08:00
5b953ae0be feat: 简易后台播放 2023-09-07 08:49:07 +08:00
392980f0e8 fix: 退出后重进,双击不能继续播放 issues #68 2023-09-06 23:08:02 +08:00
4c938ed8aa fix: 动态数据渲染异常 2023-09-06 22:47:54 +08:00
7f961e998c mod: 移除main代理 2023-09-06 21:14:57 +08:00
e6b307ddd7 Merge branch 'fix' into alpha 2023-09-06 21:10:50 +08:00
f5b4ad33c6 fix: iOS端开启代理后请求异常 2023-09-05 21:36:16 +08:00
3e8216923f just_audio 2023-09-05 12:40:42 +08:00
a2d4613293 merge design 2023-09-04 15:17:19 +08:00
1bebb32a0d mod: 直播页面控制条 2023-09-04 15:01:02 +08:00
217b036ee3 Merge branch 'fix' into alpha 2023-09-04 13:14:52 +08:00
fa95ae0cce mereg feature-media_kit 2023-09-04 13:12:24 +08:00
977bac84c3 mod: 升级播放器依赖、取消buffer遮罩 2023-09-04 13:11:22 +08:00
0f134b8dca mod: 修改取消收藏的逻辑 issues#60 2023-09-04 12:41:28 +08:00
daec283bdf Merge branch 'design' into alpha 2023-09-04 11:17:12 +08:00
6f84eefbe4 feat: 转发内容加上视频标题 issues#63 2023-09-04 11:16:52 +08:00
4a7f2f027f Merge branch 'design' into alpha 2023-09-04 11:11:27 +08:00
a39f81ac2a feat: 弹幕设置 2023-09-04 11:10:54 +08:00
0cb580ba8e fix: 锁定状态、未开启自动播放时返回逻辑 2023-09-03 16:30:03 +08:00
c7187f2456 Merge branch 'design' into alpha 2023-09-03 14:37:03 +08:00
cd38c0799d fix: 竖屏状态下系统状态栏不隐藏 issues#58 2023-09-03 14:36:44 +08:00
aa63007c8a feat: 代理设置 2023-09-03 13:46:51 +08:00
b9b1ac7ec5 feat: 增加设置项 2023-09-03 13:22:20 +08:00
4036262bed fix: 设置项重复、更新内容高度溢出 2023-09-02 21:17:58 +08:00
e35f39e353 v1.0.6 更新 2023-09-02 19:26:54 +08:00
ff47aff7cf mod: 播放停止/完成时,appBar可收起 2023-09-02 17:18:08 +08:00
72b1116b57 mod: 首页显示播放量、弹幕数 2023-09-02 17:07:18 +08:00
a0d71491be fix: merge design 2023-09-02 15:25:55 +08:00
b000a400c4 mod: 图片预览状态栏图标隐藏 2023-09-02 15:24:30 +08:00
be11598753 Merge branch 'fix' into alpha 2023-09-02 14:24:05 +08:00
2d122374f6 fix: 杜比、无损音频 2023-09-02 14:23:00 +08:00
c88b89110b Merge branch 'design' into alpha 2023-08-31 22:42:07 +08:00
9b1bb8c566 mod: 登录逻辑优化 2023-08-31 22:41:19 +08:00
01df8622e0 mod: 关注、粉丝页面优化 2023-08-31 21:42:59 +08:00
ab8c88c696 Merge branch 'design' into alpha 2023-08-31 11:46:46 +08:00
08e4e64764 feat: 番剧、投稿、直播外链跳转至Pili 2023-08-31 11:45:47 +08:00
6f2ffcf2a1 Merge branch 'fix' into alpha 2023-08-31 09:56:15 +08:00
3c1fa82010 fix: 数据格式null 2023-08-31 09:55:57 +08:00
4f6dd68954 fix: 在线观看人数字段类型 2023-08-31 00:08:57 +08:00
f06507925f Merge branch 'feature-danmaku' into alpha 2023-08-30 23:44:38 +08:00
a49c400a8e mod: 设置弹幕开关 2023-08-30 23:44:09 +08:00
1a60f74863 Merge branch 'fix' into alpha 2023-08-30 17:53:38 +08:00
a304df2670 fix: issues #47 2023-08-30 17:53:02 +08:00
1e1e2e5a77 fix: 评论区优化 2023-08-30 17:46:51 +08:00
e1c69ac550 fix: 弹幕停留 2023-08-30 13:58:54 +08:00
f6c7143d2d mod: 双击快进/快退 2023-08-30 11:45:28 +08:00
b485399517 feat: 快速收藏 2023-08-30 11:04:01 +08:00
108c37f0d7 feat: 移除单个稍后再看 2023-08-30 09:36:19 +08:00
fceb55aaa3 feat: 两次退出确认 2023-08-30 09:18:34 +08:00
52e44fb95b mod: 视频锁定逻辑优化 2023-08-30 09:01:21 +08:00
dfbe3b1f6c feat: 简单实现弹幕功能 2023-08-29 23:10:22 +08:00
e90a9f0323 Merge branch 'main' of github.com:guozhigq/pilipala 2023-08-29 11:25:41 +08:00
184088f96d feat: 可否增加打开视频自动全屏的功能 issues #37 2023-08-29 11:24:07 +08:00
73e8972a76 Update README.md 2023-08-28 23:40:04 +08:00
c15646867c fix: issues #42 收藏夹只显示前50个视频 2023-08-28 18:21:09 +08:00
f7e9fbaea7 fix: Hero导致videoPlayer渲染两次 2023-08-28 17:15:30 +08:00
5f730af347 Merge branch 'main' into feature-danmaku 2023-08-27 22:01:29 +08:00
ce1daec3c5 fix: 关闭自动播放时播放器初始化逻辑、播放器插件回滚(高版本页面回退时不会自动继续播放) 2023-08-27 22:00:46 +08:00
67a7113bd7 Merge branch 'main' into feature-danmaku 2023-08-27 18:40:39 +08:00
4e2808fab7 feat: appscheme 2023-08-27 18:12:15 +08:00
a1900c7362 mod: 评论链接跳转视频 2023-08-27 14:22:12 +08:00
e8cd0d5330 Merge branch 'feature-media_kit' 2023-08-27 12:23:23 +08:00
9528a6f462 feat: 隐藏热搜热榜的功能 issues#35 2023-08-27 12:12:51 +08:00
b9e78bf2ec feat: 首页单列模式 2023-08-27 11:51:36 +08:00
d8abfde52d mod: 倍速&底栏样式 2023-08-26 23:40:05 +08:00
1d0b91f80b mod: 导航条沉浸 2023-08-26 21:25:23 +08:00
2dd967da4d Merge branch 'main' into feature-danmaku 2023-08-26 15:59:04 +08:00
cafde6cc04 Merge branch 'main' into feature-danmaku 2023-08-22 14:28:19 +08:00
c8810b458f merge main 2023-08-19 15:33:24 +08:00
0003c057cd mod: protobuf编译文件生成 2023-08-07 13:53:41 +08:00
216 changed files with 27041 additions and 3520 deletions

View File

@ -29,6 +29,14 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
```
<br/>
## 技术交流
Telegram: https://t.me/+lm_oOVmF0RJiODk1
<br/>
## 功能
@ -100,6 +108,7 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
- [x] 主题模式:亮色/暗色/跟随系统
- [x] 震动反馈(可选)
- [x] 高帧率
- [x] 自动全屏
- [ ] 等等
<br/>
@ -117,11 +126,6 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码
感谢使用
<br/>
## 技术交流
Telegram https://t.me/+lm_oOVmF0RJiODk1
<br/>

View File

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.guozhigq.pilipala">
<queries>
<intent>
@ -20,13 +21,32 @@
"android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<application
<queries>
<!-- If your app checks for http support -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
<application
android:label="PiliPala"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
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"
@ -44,11 +64,208 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- ADD THIS "SERVICE" element -->
<service
android:name="com.ryanheise.audioservice.AudioService"
android:exported="true"
android:foregroundServiceType="mediaPlayback"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<!-- ADD THIS "RECEIVER" element -->
<receiver
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
android:exported="true"
tools:ignore="Instantiatable">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bilibili" android:host="forward" />
<data android:scheme="bilibili" android:host="comment"
android:pathPattern="/detail/.*/.*/.*" />
<data android:scheme="bilibili" android:host="uper" />
<data android:scheme="bilibili" android:host="article"
android:pathPattern="/readlist" />
<data android:scheme="bilibili" android:host="advertise" android:path="/home" />
<data android:scheme="bilibili" android:host="clip" />
<data android:scheme="bilibili" android:host="search" />
<data android:scheme="bilibili" android:host="stardust-search" />
<data android:scheme="bilibili" android:host="music" />
<data android:scheme="bilibili" android:host="bangumi"
android:pathPattern="/season.*" />
<data android:scheme="bilibili" android:host="bangumi" android:pathPattern="/.*" />
<data android:scheme="bilibili" android:host="pictureshow"
android:pathPrefix="/creative_center" />
<data android:scheme="bilibili" android:host="cliparea" />
<data android:scheme="bilibili" android:host="im" />
<data android:scheme="bilibili" android:host="im" android:path="/notifications" />
<data android:scheme="bilibili" android:host="following" />
<data android:scheme="bilibili" android:host="following"
android:pathPattern="/detail/.*" />
<data android:scheme="bilibili" android:host="following"
android:path="/publishInfo/" />
<data android:scheme="bilibili" android:host="laser" android:pathPattern="/.*" />
<data android:scheme="bilibili" android:host="livearea" />
<data android:scheme="bilibili" android:host="live" />
<data android:scheme="bilibili" android:host="catalog" />
<data android:scheme="bilibili" android:host="browser" />
<data android:scheme="bilibili" android:host="user_center" />
<data android:scheme="bilibili" android:host="login" />
<data android:scheme="bilibili" android:host="space" />
<data android:scheme="bilibili" android:host="author" />
<data android:scheme="bilibili" android:host="tag" />
<data android:scheme="bilibili" android:host="rank" />
<data android:scheme="bilibili" android:host="external" />
<data android:scheme="bilibili" android:host="blank" />
<data android:scheme="bilibili" android:host="home" />
<data android:scheme="bilibili" android:host="root" />
<data android:scheme="bilibili" android:host="video" />
<data android:scheme="bilibili" android:host="story" />
<data android:scheme="bilibili" android:host="podcast" />
<data android:scheme="bilibili" android:host="search" />
<data android:scheme="bilibili" android:host="main" android:path="/favorite" />
<data android:scheme="bilibili" android:host="pgc" android:path="/theater/match" />
<data android:scheme="bilibili" android:host="pgc" android:path="/theater/square" />
<data android:scheme="bilibili" android:host="m.bilibili.com"
android:path="/topic-detail" />
<data android:scheme="bilibili" android:host="article" />
<data android:scheme="bilibili" android:host="pegasus"
android:pathPattern="/channel/v2/.*" />
<data android:scheme="bilibili" android:host="feed" android:pathPattern="/channel" />
<data android:scheme="bilibili" android:host="vip" />
<data android:scheme="bilibili" android:host="user_center" android:path="/vip" />
<data android:scheme="bilibili" android:host="history" />
<data android:scheme="bilibili" android:host="charge" android:path="/rank" />
<data android:scheme="bilibili" android:host="assistant" />
<data android:scheme="bilibili" android:host="assistant" />
<data android:scheme="bilibili" android:host="feedback" />
<data android:scheme="bilibili" android:host="auth" android:path="/launch" />
<data android:scheme="http" android:host="live.bilibili.com"
android:pathPattern="/live/.*" />
<data android:scheme="https" android:host="live.bilibili.com"
android:pathPattern="/live/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.tv"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/story/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/story/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/i/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/bangumi/i/.*" />
<data android:scheme="http" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" />
<data android:scheme="https" android:host="bangumi.bilibili.com"
android:pathPattern="/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/bangumi/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/bangumi/play/ss.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ss.*" />
<data android:scheme="http" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="https" android:host="m.bilibili.com"
android:pathPattern="/cheese/play/ep.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/read/cv.*" />
<data android:scheme="http" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="https" android:host="www.bilibili.com" android:path="/review/" />
<data android:scheme="http" android:host="bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.cn"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/video/.*" />
<data android:scheme="http" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
<data android:scheme="https" android:host="www.bilibili.com"
android:pathPattern="/mobile/video/.*" />
</intent-filter>
</activity>
<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
@ -56,12 +273,13 @@
android:value="2" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<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.
@ -69,4 +287,8 @@
-->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
</manifest>

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/*" />

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
@ -14,5 +14,6 @@
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowLayoutInDisplayCutoutMode" tools:targetApi="o_mr1">shortEdges</item>
</style>
</resources>

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上查看
问题反馈、功能建议请查看「关于」页面。

34
change_log/1.0.6.0902.md Normal file
View File

@ -0,0 +1,34 @@
## 1.0.6
问题反馈、功能建议请查看「关于」页面。
### 新功能
+ 首页单列布局
+ 首页推荐展示播放量、弹幕数
+ 简单弹幕功能实现(持续开发中...
+ 评论区搜索关键词开关 issues#46
+ 热搜榜隐藏功能 issues#35
+ 自动全屏 issues#37
+ 快速收藏功能
+ 双击快进/快退开关
+ 评论链接跳转视频
+ 支持移除单个稍后再看
+ app scheme外链跳转
### 修复
+ 杜比、无损音频切换
+ 收藏夹展示 issues#42
+ 搜索建议次 issues#47
### 优化
+ 倍速选择优化
+ 导航条沉浸
+ 取消Hero动画
+ 视频锁定逻辑
+ 登录逻辑优化
+ 图片预览样式
+ +评论区用户点击范围
+ 关注、粉丝页面优化
+ 关闭自动播放时播放器初始化逻辑

22
change_log/1.0.7.0908.md Normal file
View File

@ -0,0 +1,22 @@
## 1.0.7
默认倍速、直播弹幕、专栏等功能开发中
### 新功能
+ 弹幕设置、屏蔽功能
+ 不是很完美的后台播放功能
+ 不是很完美的画中画(pip)功能Android端
### 修复
+ 动态页面加载异常
+ 网络异常时页面空白
+ 竖屏全屏状态栏问题
+ iOS端代理请求异常
### 优化
+ 图片预览
+ 全屏播放时自动旋转
+ 转发内容增加视频标题
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

24
change_log/1.0.8.0917.md Normal file
View File

@ -0,0 +1,24 @@
## 1.0.8
直播弹幕、循环播放等功能开发中
### 新功能
+ 用户拉黑功能
+ gif图片保存
+ 删除已看历史记录
### 修复
+ 弹幕数量较少
+ 弹幕屏蔽设置自动记忆
+ 动态页面渲染
+ 用户主页数据错乱
+ 大家都在搜空白
+ 默认自动全屏,顶部操作栏丢失
### 优化
+ 全屏状态栏区域显示优化
+ 图片保存至PiliPala文件夹
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

28
change_log/1.0.9.1015.md Normal file
View File

@ -0,0 +1,28 @@
## 1.0.9
### 新功能
+ 自定义倍速、默认倍速
+ 历史记录搜索
+ 收藏夹搜索
+ 历史记录多选删除
+ 视频循环播放
+ 免登录看1080P
+ 评论区视频链接跳转
+ up主分组
+ up主投稿搜索
### 修复
+ 搜索视频标题乱码
+ 屏幕帧率
+ 动态页面渲染
### 优化
+ 快进手势
+ 视频简介链接匹配
+ 视频全屏时安全区域
更多更新日志可在Github上查看
问题反馈、功能建议请查看「关于」页面。

View File

@ -37,5 +37,11 @@ end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'AUDIO_SESSION_MICROPHONE=0'
]
end
end
end

View File

@ -1,4 +1,10 @@
PODS:
- appscheme (1.0.4):
- Flutter
- audio_service (0.0.1):
- Flutter
- audio_session (0.0.1):
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
@ -10,8 +16,15 @@ PODS:
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- image_gallery_saver (2.0.2):
- gt3_flutter_plugin (0.0.8):
- Flutter
<<<<<<< HEAD
- just_audio (0.0.1):
- Flutter
=======
- GT3Captcha-iOS
- GT3Captcha-iOS (0.15.8.3)
>>>>>>> main
- media_kit_libs_ios_video (1.0.4):
- Flutter
- media_kit_native_event_loop (1.0.0):
@ -26,6 +39,8 @@ PODS:
- permission_handler_apple (9.1.1):
- Flutter
- ReachabilitySwift (5.0.0)
- saver_gallery (0.0.1):
- Flutter
- screen_brightness_ios (0.1.0):
- Flutter
- share_plus (0.0.1):
@ -33,6 +48,10 @@ PODS:
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- status_bar_control (3.2.1):
- Flutter
- system_proxy (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- volume_controller (0.0.1):
@ -45,20 +64,31 @@ PODS:
- Flutter
DEPENDENCIES:
- appscheme (from `.symlinks/plugins/appscheme/ios`)
- audio_service (from `.symlinks/plugins/audio_service/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_volume_controller (from `.symlinks/plugins/flutter_volume_controller/ios`)
<<<<<<< HEAD
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- just_audio (from `.symlinks/plugins/just_audio/ios`)
=======
- gt3_flutter_plugin (from `.symlinks/plugins/gt3_flutter_plugin/ios`)
>>>>>>> main
- media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
- media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`)
- media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- saver_gallery (from `.symlinks/plugins/saver_gallery/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- status_bar_control (from `.symlinks/plugins/status_bar_control/ios`)
- system_proxy (from `.symlinks/plugins/system_proxy/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
@ -68,9 +98,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"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
@ -79,8 +116,15 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_volume_controller:
:path: ".symlinks/plugins/flutter_volume_controller/ios"
<<<<<<< HEAD
image_gallery_saver:
:path: ".symlinks/plugins/image_gallery_saver/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/ios"
=======
gt3_flutter_plugin:
:path: ".symlinks/plugins/gt3_flutter_plugin/ios"
>>>>>>> main
media_kit_libs_ios_video:
:path: ".symlinks/plugins/media_kit_libs_ios_video/ios"
media_kit_native_event_loop:
@ -93,12 +137,18 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
saver_gallery:
:path: ".symlinks/plugins/saver_gallery/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
status_bar_control:
:path: ".symlinks/plugins/status_bar_control/ios"
system_proxy:
:path: ".symlinks/plugins/system_proxy/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
volume_controller:
@ -111,12 +161,21 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
SPEC CHECKSUMS:
appscheme: b1c3f8862331cb20430cf9e0e4af85dbc1572ad8
audio_service: f509d65da41b9521a61f1c404dd58651f265a567
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_volume_controller: e4d5832f08008180f76e30faf671ffd5a425e529
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
<<<<<<< HEAD
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
=======
gt3_flutter_plugin: bfa1f26e9a09dc00401514be5ed437f964cabf23
GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6
>>>>>>> main
media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1
media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a
media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e
@ -124,15 +183,18 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
status_bar_control: 7c84146799e6a076315cc1550f78ef53aae3e446
system_proxy: bec1a5c5af67dd3e3ebf43979400a8756c04cc44
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9
wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47
webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7
webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a
PODFILE CHECKSUM: cc1f88378b4bfcf93a6ce00d2c587857c6008d3b
PODFILE CHECKSUM: fc8a34c4ba2e14d31df90bf03cf419a764f2778c
COCOAPODS: 1.12.1

View File

@ -140,6 +140,7 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
5A372F23F3CF0118D6526BAC /* [CP] Embed Pods Frameworks */,
B78851E7B29A4C3961AC483C /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -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,62 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PiliPala</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>pilipala</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key>
<string>App需要您的同意,才能访问媒体资料库</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
</dict>
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>PiliPala</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>PiliPala</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>请允许APP保存图片到相册</string>
<key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相册</string>
<key>NSAppleMusicUsageDescription</key>
<string>App需要您的同意,才能访问媒体资料库</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
<!-- Add Scheme related information -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>http</string>
<string>https</string>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>m.bilibili.com</string>
<string>bilibili.com</string>
<string>www.bilibili.com</string>
<string>bangumi.bilibili.com</string>
<string>bilibili.cn</string>
<string>www.bilibili.cn</string>
<string>bangumi.bilibili.cn</string>
<string>bilibili.tv</string>
<string>www.bilibili.tv</string>
<string>bangumi.bilibili.tv</string>
<string>miniapp.bilibili.com</string>
<string>live.bilibili.com</string>
</array>
</dict>
</array>
</dict>
<!-- 当其他应用程序或系统通过 bilibili -->
<dict>
<key>CFBundleURLName</key>
<string>bilibili</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bilibili</string>
</array>
</dict>
</array>
<<<<<<< HEAD
<!-- audio service配置 -->
=======
>>>>>>> main
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
</dict>
</plist>

View File

@ -45,11 +45,6 @@ class VideoCardVSkeleton extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 12),
color: Theme.of(context).colorScheme.onInverseSurface,
),
Container(
width: 80,
height: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
),
),

View File

@ -0,0 +1,96 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// ignore: must_be_immutable
class HtmlRender extends StatelessWidget {
String? htmlContent;
final int? imgCount;
final List? imgList;
HtmlRender({
this.htmlContent,
this.imgCount,
this.imgList,
super.key,
});
@override
Widget build(BuildContext context) {
return Html(
data: htmlContent,
onLinkTap: (url, buildContext, attributes) => {},
extensions: [
TagExtension(
tagsToExtend: {"img"},
builder: (extensionContext) {
try {
Map attributes = extensionContext.attributes;
List key = attributes.keys.toList();
String? imgUrl = key.contains('src')
? attributes['src']
: attributes['data-src'];
if (imgUrl!.startsWith('//')) {
imgUrl = 'https:$imgUrl';
}
if (imgUrl.startsWith('http://')) {
imgUrl = imgUrl.replaceAll('http://', 'https://');
}
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
bool isEmote = imgUrl.contains('/emote/');
bool isMall = imgUrl.contains('/mall/');
if (isMall) {
return const SizedBox();
}
// bool inTable =
// extensionContext.element!.previousElementSibling == null ||
// extensionContext.element!.nextElementSibling == null;
// imgUrl = Utils().imageUrl(imgUrl!);
// return Image.network(
// imgUrl,
// width: isEmote ? 22 : null,
// height: isEmote ? 22 : null,
// );
return NetworkImgLayer(
width: isEmote ? 22 : Get.size.width - 24,
height: isEmote ? 22 : 200,
src: imgUrl,
);
} catch (err) {
print(err);
return const SizedBox();
}
},
),
],
style: {
"html": Style(
fontSize: FontSize.medium,
lineHeight: LineHeight.percent(140),
),
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
"a": Style(
color: Theme.of(context).colorScheme.primary,
textDecoration: TextDecoration.none,
),
"p": Style(
margin: Margins.only(bottom: 10),
),
"span": Style(
fontSize: FontSize.medium,
height: Height(1.65),
),
"div": Style(height: Height.auto()),
"li > p": Style(
display: Display.inline,
),
"li": Style(
padding: HtmlPaddings.only(bottom: 4),
textAlign: TextAlign.justify,
),
"img": Style(margin: Margins.only(top: 4, bottom: 4)),
},
);
}
}

View File

@ -70,9 +70,17 @@ class NetworkImgLayer extends StatelessWidget {
Widget placeholder(context) {
return Container(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
width: width ?? double.infinity,
height: height ?? double.infinity,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface.withOpacity(0.4),
borderRadius: BorderRadius.circular(type == 'avatar'
? 50
: type == 'emote'
? 0
: StyleString.imgRadius.x),
),
child: Center(
child: Image.asset(
type == 'avatar'

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';
@ -15,16 +16,23 @@ import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
final dynamic videoItem;
final int crossAxisCount;
final Function()? longPress;
final Function()? longPressEnd;
const VideoCardV({
Key? key,
required this.videoItem,
required this.crossAxisCount,
this.longPress,
this.longPressEnd,
}) : super(key: key);
bool isStringNumeric(String str) {
RegExp numericRegex = RegExp(r'^\d+$');
return numericRegex.hasMatch(str);
}
void onPushDetail(heroTag) async {
String goto = videoItem.goto;
switch (goto) {
@ -60,6 +68,47 @@ class VideoCardV extends StatelessWidget {
'heroTag': heroTag,
});
break;
// 动态
case 'picture':
try {
String dynamicType = 'picture';
String uri = videoItem.uri;
String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554
dynamicType = 'read';
RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!;
String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
}
if (uri.startsWith('http')) {
String path = Uri.parse(uri).path;
if (isStringNumeric(path.split('/')[1])) {
// 请求接口
var res =
await DynamicsHttp.dynamicDetail(id: path.split('/')[1]);
if (res['status']) {
Get.toNamed('/dynamicDetail', arguments: {
'item': res['data'],
'floor': 1,
'action': 'detail'
});
}
return;
}
}
Get.toNamed('/htmlRender', parameters: {
'url': uri,
'title': videoItem.title,
'id': id,
'dynamicType': dynamicType
});
} catch (err) {
SmartDialog.showToast(err.toString());
}
break;
default:
SmartDialog.showToast(videoItem.goto);
Get.toNamed(
@ -77,7 +126,7 @@ class VideoCardV extends StatelessWidget {
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
return Card(
elevation: 1,
elevation: 0,
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.zero,
child: GestureDetector(
@ -100,17 +149,37 @@ class VideoCardV extends StatelessWidget {
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
if (videoItem.duration != null)
if (crossAxisCount == 1) ...[
PBadge(
bottom: 10,
right: 10,
text: videoItem.duration,
)
] else ...[
PBadge(
bottom: 6,
right: 7,
size: 'small',
type: 'gray',
text: videoItem.duration,
)
],
],
);
}),
),
VideoContent(videoItem: videoItem)
VideoContent(videoItem: videoItem, crossAxisCount: crossAxisCount)
],
),
),
@ -121,22 +190,53 @@ class VideoCardV extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
const VideoContent({Key? key, required this.videoItem}) : super(key: key);
final int crossAxisCount;
const VideoContent(
{Key? key, required this.videoItem, required this.crossAxisCount})
: super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Padding(
padding: const EdgeInsets.fromLTRB(9, 8, 9, 4),
padding: crossAxisCount == 1
? const EdgeInsets.fromLTRB(9, 9, 9, 4)
: const EdgeInsets.fromLTRB(5, 8, 5, 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
Row(
children: [
Expanded(
child: Text(
videoItem.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (videoItem.goto == 'av' && crossAxisCount == 1) ...[
const SizedBox(width: 10),
WatchLater(
size: 32,
iconSize: 18,
callFn: () async {
int aid = videoItem.param;
var res =
await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
),
],
],
),
if (crossAxisCount > 1) ...[
const SizedBox(height: 2),
VideoStat(
videoItem: videoItem,
),
],
if (crossAxisCount == 1) const SizedBox(height: 4),
Row(
children: [
if (videoItem.goto == 'bangumi') ...[
@ -167,6 +267,7 @@ class VideoContent extends StatelessWidget {
)
],
Expanded(
flex: crossAxisCount == 1 ? 0 : 1,
child: Text(
videoItem.owner.name,
maxLines: 1,
@ -177,95 +278,36 @@ class VideoContent extends StatelessWidget {
),
),
),
if (videoItem.goto == 'av')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
int aid = videoItem.param;
var res = await UserHttp.toViewLater(
bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
if (crossAxisCount == 1) ...[
Text(
'',
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
VideoStat(
videoItem: videoItem,
),
const Spacer(),
],
if (videoItem.goto == 'av' && crossAxisCount != 1) ...[
WatchLater(
size: 24,
iconSize: 14,
callFn: () async {
int aid = videoItem.param;
var res =
await UserHttp.toViewLater(bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
),
] else ...[
const SizedBox(height: 24)
]
],
),
// Row(
// children: [
// const SizedBox(width: 1),
// StatView(
// theme: 'gray',
// view: videoItem.stat.view,
// ),
// const SizedBox(width: 10),
// StatDanMu(
// theme: 'gray',
// danmu: videoItem.stat.danmaku,
// ),
// const Spacer(),
// SizedBox(
// width: 24,
// height: 24,
// child: PopupMenuButton<String>(
// padding: EdgeInsets.zero,
// tooltip: '稍后再看',
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// position: PopupMenuPosition.under,
// // constraints: const BoxConstraints(maxHeight: 35),
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) =>
// <PopupMenuEntry<String>>[
// PopupMenuItem<String>(
// onTap: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// value: 'pause',
// height: 35,
// child: const Row(
// children: [
// Icon(Icons.watch_later_outlined, size: 16),
// SizedBox(width: 6),
// Text('稍后再看', style: TextStyle(fontSize: 13))
// ],
// ),
// ),
// ],
// ),
// ),
// ],
// ),
],
),
),
@ -274,53 +316,72 @@ class VideoContent extends StatelessWidget {
}
class VideoStat extends StatelessWidget {
final int? view;
final int? danmaku;
final int? duration;
final dynamic videoItem;
const VideoStat(
{Key? key,
required this.view,
required this.danmaku,
required this.duration})
: super(key: key);
const VideoStat({
Key? key,
required this.videoItem,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 48,
padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black54,
],
tileMode: TileMode.mirror,
return RichText(
maxLines: 1,
text: TextSpan(
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
StatView(
theme: 'white',
view: view,
),
const SizedBox(width: 6),
StatDanMu(
theme: 'white',
danmu: danmaku,
),
],
),
Text(
Utils.timeFormat(duration!),
style: const TextStyle(fontSize: 11, color: Colors.white),
)
TextSpan(text: '${videoItem.stat.view}观看'),
TextSpan(text: '${videoItem.stat.danmu}弹幕'),
],
),
);
}
}
class WatchLater extends StatelessWidget {
final double? size;
final double? iconSize;
final Function? callFn;
const WatchLater({
Key? key,
required this.size,
required this.iconSize,
this.callFn,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: iconSize,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () => callFn!(),
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
),
);

View File

@ -97,6 +97,9 @@ class Api {
// 操作用户关系
static const String relationMod = '/x/relation/modify';
// 相互关系查询
static const String relationSearch = '/x/space/wbi/acc/relation';
// 评论列表
// https://api.bilibili.com/x/v2/reply/main?csrf=6e22efc1a47225ea25f901f922b5cfdd&mode=3&oid=254175381&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=0&type=11
static const String replyList = '/x/v2/reply';
@ -126,12 +129,14 @@ class Api {
static const String userFavFolder = '/x/v3/fav/folder/created/list';
/// 收藏夹 详情
/// media_id int 收藏夹id
/// media_id 当前收藏夹id 搜索全部时为默认收藏夹id
/// pn int 当前页
/// ps int pageSize
/// keyword String 搜索词
/// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿
/// tid int 分区id
/// platform web
/// type 0 当前收藏夹 1 全部收藏夹
// https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0
static const String userFavFolderDetail = '/x/v3/fav/resource/list';
@ -164,6 +169,12 @@ class Api {
// 清空历史记录
static const String clearHistory = '/x/v2/history/clear';
// 删除某条历史记录
static const String delHistory = '/x/v2/history/delete';
// 搜索历史记录
static const String searchHistory = '/x/web-goblin/history/search';
// 热搜
static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword';
@ -239,6 +250,9 @@ class Api {
// wts=1689767832
static const String memberArchive = '/x/space/wbi/arc/search';
// 用户动态搜索
static const String memberDynamicSearch = '/x/space/dynamic/search';
// 用户动态
static const String memberDynamic = '/x/polymer/web-dynamic/v1/feed/space';
@ -285,6 +299,9 @@ class Api {
// 黑名单
static const String blackLst = '/x/relation/blacks';
// 移除黑名单
static const String removeBlack = '/x/relation/modify';
// github 获取最新版
static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
@ -292,4 +309,67 @@ class Api {
// 多少人在看
// https://api.bilibili.com/x/player/online/total?aid=913663681&cid=1203559746&bvid=BV1MM4y1s7NZ&ts=56427838
static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so';
// up主分组
static const String followUpTag = '/x/relation/tags';
// 设置Up主分组
// 0 添加至默认分组 否则使用,分割tagid
static const String addUsers = '/x/relation/tags/addUsers';
// 获取指定分组下的up
static const String followUpGroup = '/x/relation/tag';
// 获取某个动态详情
// timezone_offset=-480
// id=849312409672744983
// features=itemOpusStyle
static const String dynamicDetail = '/x/polymer/web-dynamic/v1/detail';
// AI总结
/// https://api.bilibili.com/x/web-interface/view/conclusion/get?
/// bvid=BV1ju4y1s7kn&
/// cid=1296086601&
/// up_mid=4641697&
/// w_rid=1607c6c5a4a35a1297e31992220900ae&
/// wts=1697033079
static const String aiConclusion = '/x/web-interface/view/conclusion/get';
// captcha验证码
static const String getCaptcha =
'https://passport.bilibili.com/x/passport-login/captcha?source=main_web';
// web端短信验证码
static const String smsCode =
'https://passport.bilibili.com/x/passport-login/web/sms/send';
// web端验证码登录
// web端密码登录
// app端短信验证码
static const String appSmsCode =
'https://passport.bilibili.com/x/passport-login/sms/send';
// app端验证码登录
// 获取短信验证码
// static const String appSafeSmsCode =
// 'https://passport.bilibili.com/x/safecenter/common/sms/send';
/// app端密码登录
/// username
/// password
/// key
/// rhash
static const String loginInByPwdApi =
'https://passport.bilibili.com/x/passport-login/oauth2/login';
/// 密码加密密钥
/// disable_rcmd
/// local_id
static const getWebKey =
'https://passport.bilibili.com/x/passport-login/web/key';
}

View File

@ -23,4 +23,31 @@ class BlackHttp {
};
}
}
// 移除黑名单
static Future removeBlack({required int fid}) async {
var res = await Request().post(
Api.removeBlack,
queryParameters: {
'act': 6,
'csrf': await Request.getCsrf(),
'fid': fid,
'jsonp': 'jsonp',
're_src': 116,
},
);
if (res.data['code'] == 0) {
return {
'status': true,
'data': [],
'msg': '操作成功',
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -2,4 +2,37 @@ class HttpString {
static const String baseUrl = 'https://www.bilibili.com';
static const String baseApiUrl = 'https://api.bilibili.com';
static const String tUrl = 'https://api.vc.bilibili.com';
static const List<int> validateStatusCodes = [
302,
304,
307,
400,
401,
403,
404,
405,
409,
412,
500,
503,
504,
509,
616,
617,
625,
626,
628,
629,
632,
643,
650,
652,
658,
662,
688,
689,
701,
799,
8888
];
}

27
lib/http/danmaku.dart Normal file
View File

@ -0,0 +1,27 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/danmaku/dm.pb.dart';
import 'constants.dart';
class DanmakaHttp {
// 获取视频弹幕
static Future queryDanmaku({
required int cid,
required int segmentIndex,
}) async {
// 构建参数对象
Map<String, int> params = {
'type': 1,
'oid': cid,
'segment_index': segmentIndex,
};
var response = await Request().get(
Api.webDanmaku,
data: params,
extra: {'resType': ResponseType.bytes},
);
return DmSegMobileReply.fromBuffer(response.data);
}
}

View File

@ -28,6 +28,7 @@ class DynamicsHttp {
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} catch (err) {
print(err);
return {
'status': false,
'data': [],
@ -85,4 +86,35 @@ class DynamicsHttp {
};
}
}
//
static Future dynamicDetail({
String? id,
}) async {
var res = await Request().get(Api.dynamicDetail, data: {
'timezone_offset': -480,
'id': id,
'features': 'itemOpusStyle',
});
if (res.data['code'] == 0) {
try {
return {
'status': true,
'data': DynamicItemModel.fromJson(res.data['data']['item']),
};
} catch (err) {
return {
'status': false,
'data': [],
'msg': err.toString(),
};
}
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

103
lib/http/html.dart Normal file
View File

@ -0,0 +1,103 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:pilipala/http/index.dart';
class HtmlHttp {
// article
static Future reqHtml(id, dynamicType) async {
var response = await Request().get(
"https://www.bilibili.com/opus/$id",
extra: {'ua': 'pc'},
);
if (response.data.contains('Redirecting to')) {
RegExp regex = RegExp(r'//([\w\.]+)/(\w+)/(\w+)');
Match match = regex.firstMatch(response.data)!;
String matchedString = match.group(0)!;
response = await Request().get(
'https:$matchedString' + '/',
extra: {'ua': 'pc'},
);
}
try {
Document rootTree = parse(response.data);
// log(response.data.body.toString());
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
// 头像
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader
.querySelector('.fixed-author-header__author__name')!
.text;
// 动态详情
Element opusDetail = appDom.querySelector('.opus-detail')!;
// 发布时间
String updateTime =
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
//
String opusContent =
opusDetail.querySelector('.opus-module-content')!.innerHtml;
String test = opusDetail
.querySelector('.horizontal-scroll-album__pic__img')!
.innerHtml;
String commentId = opusDetail
.querySelector('.bili-comment-container')!
.className
.split(' ')[1]
.split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
'uname': uname,
'updateTime': updateTime,
'content': test + opusContent,
'commentId': int.parse(commentId)
};
} catch (err) {
print('err: $err');
}
}
// read
static Future reqReadHtml(id, dynamicType) async {
var response = await Request().get(
"https://www.bilibili.com/$dynamicType/$id/",
extra: {'ua': 'pc'},
);
Document rootTree = parse(response.data);
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.up-left')!;
// 头像
// String avatar =
// authorHeader.querySelector('.bili-avatar-img')!.attributes['data-src']!;
// print(avatar);
// avatar = 'https:${avatar.split('@')[0]}';
String uname = authorHeader.querySelector('.up-name')!.text.trim();
// 动态详情
Element opusDetail = appDom.querySelector('.article-content')!;
// 发布时间
// String updateTime =
// opusDetail.querySelector('.opus-module-author__pub__text')!.text;
// print(updateTime);
//
String opusContent =
opusDetail.querySelector('#read-article-holder')!.innerHtml;
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!;
return {
'status': true,
'avatar': '',
'uname': uname,
'updateTime': '',
'content': opusContent,
'commentId': int.parse(number)
};
}
}

View File

@ -4,12 +4,13 @@ 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';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/interceptor.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
@ -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 {
@ -42,6 +48,7 @@ class Request {
}
}
}
setOptionsHeaders(userInfo, userInfo != null && userInfo.mid != null);
if (cookie.isEmpty) {
try {
@ -59,9 +66,6 @@ class Request {
static Future<String> getCsrf() async {
var cookies = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseApiUrl));
// for (var i in cookies) {
// print(i);
// }
String token = '';
if (cookies.where((e) => e.name == 'bili_jct').isNotEmpty) {
token = cookies.firstWhere((e) => e.name == 'bili_jct').value;
@ -69,6 +73,17 @@ class Request {
return token;
}
static setOptionsHeaders(userInfo, status) {
if (status) {
dio.options.headers['x-bili-mid'] = userInfo.mid.toString();
}
dio.options.headers['env'] = 'prod';
dio.options.headers['app-key'] = 'android64';
dio.options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
dio.options.headers['x-bili-aurora-zone'] = 'sh001';
dio.options.headers['referer'] = 'https://www.bilibili.com/';
}
/*
* config it and create
*/
@ -82,29 +97,47 @@ class Request {
//响应流上前后两次接受到数据的间隔,单位为毫秒。
receiveTimeout: const Duration(milliseconds: 12000),
//Http请求头.
headers: {
// 'cookie': '',
},
headers: {},
);
Box userInfoCache = GStrorage.userInfo;
var userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null && userInfo.mid != null) {
options.headers['x-bili-mid'] = userInfo.mid.toString();
options.headers['env'] = 'prod';
options.headers['app-key'] = 'android64';
options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
options.headers['x-bili-aurora-zone'] = 'sh001';
options.headers['referer'] = 'https://www.bilibili.com/';
}
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代理问题冲突
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
)
/// 设置代理
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
// Config the client.
client.findProxy = (uri) {
if (enableSystemProxy) {
print('🌹:$systemProxyHost');
print('🌹:$systemProxyPort');
// return 'PROXY host:port';
return 'PROXY $systemProxyHost:$systemProxyPort';
} else {
// 不设置代理
return 'DIRECT';
}
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
},
);
//添加拦截器
@ -119,30 +152,26 @@ class Request {
dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) {
return status! >= 200 && status < 300 || status == 304 || status == 302;
return status! >= 200 && status < 300 ||
HttpString.validateStatusCodes.contains(status);
};
}
/*
* get请求
*/
get(url, {data, cacheOptions, options, cancelToken, extra}) async {
get(url, {data, options, cancelToken, extra}) async {
Response response;
Options options;
String ua = 'pc';
Options options = Options();
ResponseType resType = ResponseType.json;
if (extra != null) {
ua = extra!['ua'] ?? 'pc';
resType = extra!['resType'] ?? ResponseType.json;
if (extra['ua'] != null) {
options.headers = {'user-agent': headerUa(type: extra['ua'])};
}
}
if (cacheOptions != null) {
cacheOptions.headers = {'user-agent': headerUa(ua)};
options = cacheOptions;
} else {
options = Options();
options.headers = {'user-agent': headerUa(ua)};
options.responseType = resType;
}
options.responseType = resType;
try {
response = await dio.get(
url,
@ -209,15 +238,19 @@ class Request {
token.cancel("cancelled");
}
String headerUa(ua) {
String headerUa({type = 'mob'}) {
String headerUa = '';
if (ua == 'mob') {
headerUa = Platform.isIOS
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
: 'Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36';
if (type == 'mob') {
if (Platform.isIOS) {
headerUa =
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Mobile/15E148 Safari/604.1';
} else {
headerUa =
'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36';
}
} else {
headerUa =
'Mozilla/5.0 (MaciMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36';
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15';
}
return headerUa;
}

View File

@ -17,8 +17,6 @@ class ApiInterceptor extends Interceptor {
handler.next(options);
}
Box localCache = GStrorage.localCache;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
@ -29,8 +27,11 @@ class ApiInterceptor extends Interceptor {
final uri = Uri.parse(locations.first);
final accessKey = uri.queryParameters['access_key'];
final mid = uri.queryParameters['mid'];
localCache
.put(LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
try {
Box localCache = GStrorage.localCache;
localCache.put(
LocalCacheKey.accessKey, {'mid': mid, 'value': accessKey});
} catch (_) {}
}
}
}
@ -45,7 +46,10 @@ class ApiInterceptor extends Interceptor {
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 处理网络请求错误
// handler.next(err);
SmartDialog.showToast(await dioError(err));
SmartDialog.showToast(
await dioError(err),
displayType: SmartToastType.onlyRefresh,
);
super.onError(err, handler);
}

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,7 +1,9 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class MemberHttp {
@ -18,6 +20,7 @@ class MemberHttp {
var res = await Request().get(
Api.memberInfo,
data: params,
extra: {'ua': 'pc'},
);
if (res.data['code'] == 0) {
return {
@ -65,7 +68,7 @@ class MemberHttp {
int ps = 30,
int tid = 0,
int? pn,
String keyword = '',
String? keyword,
String order = 'pubdate',
bool orderAvoided = true,
}) async {
@ -74,7 +77,7 @@ class MemberHttp {
'ps': ps,
'tid': tid,
'pn': pn,
'keyword': keyword,
'keyword': keyword ?? '',
'order': order,
'platform': 'web',
'web_location': 1550101,
@ -83,6 +86,7 @@ class MemberHttp {
var res = await Request().get(
Api.memberArchive,
data: params,
extra: {'ua': 'pc'},
);
if (res.data['code'] == 0) {
return {
@ -119,4 +123,96 @@ class MemberHttp {
};
}
}
// 搜索用户动态
static Future memberDynamicSearch({int? pn, int? ps, int? mid}) async {
var res = await Request().get(Api.memberDynamic, data: {
'keyword': '海拔',
'mid': mid,
'pn': pn,
'ps': ps,
'platform': 'web'
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': DynamicsDataModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 查询分组
static Future followUpTags() async {
var res = await Request().get(Api.followUpTag);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 设置分组
static Future addUsers(int? fids, String? tagids) async {
var res = await Request().post(Api.addUsers, queryParameters: {
'fids': fids,
'tagids': tagids ?? '0',
'csrf': await Request.getCsrf(),
}, data: {
'cross_domain': true
});
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取某分组下的up
static Future followUpGroup(
int? mid,
int? tagid,
int? pn,
int? ps,
) async {
var res = await Request().get(Api.followUpGroup, data: {
'mid': mid,
'tagid': tagid,
'pn': pn,
'ps': ps,
});
if (res.data['code'] == 0) {
// FollowItemModel
return {
'status': true,
'data': res.data['data']
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -26,7 +26,7 @@ class ReplyHttp {
Map errMap = {
-400: '请求错误',
-404: '无此项',
12002: '当前页面评论功能已关闭"',
12002: '当前页面评论功能已关闭',
12009: '评论主体的type不合法',
12061: 'UP主已关闭评论区',
};

View File

@ -1,37 +1,63 @@
import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList);
if (res.data['code'] == 0) {
if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(resultMap),
};
}
} else if (res.data is Map<String, dynamic> && res.data['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(res.data),
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
// 获取搜索建议
static Future searchSuggest({required term}) async {
var res = await Request().get(Api.serachSuggest,
data: {'term': term, 'main_ver': 'v1', 'highlight': term});
if (res.data['code'] == 0) {
res.data['result']['term'] = term;
return {
'status': true,
'data': SearchSuggestModel.fromJson(res.data['result']),
};
if (res.data is String) {
Map<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
if (resultMap['result'] is Map) {
resultMap['result']['term'] = term;
}
return {
'status': true,
'data': resultMap['result'] is Map
? SearchSuggestModel.fromJson(resultMap['result'])
: [],
};
} else {
return {
'status': false,
'data': [],
'msg': '请求错误 🙅',
};
}
} else {
return {
'status': false,
@ -61,29 +87,44 @@ class SearchHttp {
var res = await Request().get(Api.searchByType, data: reqData);
if (res.data['code'] == 0 && res.data['data']['numPages'] > 0) {
Object data;
switch (searchType) {
case SearchType.video:
data = SearchVideoModel.fromJson(res.data['data']);
break;
case SearchType.live_room:
data = SearchLiveModel.fromJson(res.data['data']);
break;
case SearchType.bili_user:
data = SearchUserModel.fromJson(res.data['data']);
break;
case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']);
break;
try {
switch (searchType) {
case SearchType.video:
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['result']) {
// 屏蔽推广和拉黑用户
i['available'] = !blackMidsList.contains(i['mid']);
}
data = SearchVideoModel.fromJson(res.data['data']);
break;
case SearchType.live_room:
data = SearchLiveModel.fromJson(res.data['data']);
break;
case SearchType.bili_user:
data = SearchUserModel.fromJson(res.data['data']);
break;
case SearchType.media_bangumi:
data = SearchMBangumiModel.fromJson(res.data['data']);
break;
case SearchType.article:
data = SearchArticleModel.fromJson(res.data['data']);
break;
}
return {
'status': true,
'data': data,
};
} catch (err) {
print(err);
}
return {
'status': true,
'data': data,
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['data']['numPages'] == 0 ? '没有相关数据' : '请求错误 🙅',
'msg': res.data['data'] != null && res.data['data']['numPages'] == 0
? '没有相关数据'
: res.data['message'],
};
}
}

View File

@ -8,6 +8,7 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/user/history.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/wbi_sign.dart';
class UserHttp {
static Future<dynamic> userStat({required int mid}) async {
@ -70,14 +71,15 @@ class UserHttp {
required int pn,
required int ps,
String keyword = '',
String order = 'mtime'}) async {
String order = 'mtime',
int type = 0}) async {
var res = await Request().get(Api.userFavFolderDetail, data: {
'media_id': mediaId,
'pn': pn,
'ps': ps,
'keyword': keyword,
'order': order,
'type': 0,
'type': type,
'tid': 0,
'platform': 'web'
});
@ -179,14 +181,16 @@ class UserHttp {
}
// 移除已观看
static Future toViewDel() async {
static Future toViewDel({int? aid}) async {
final Map<String, dynamic> params = {
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
};
params[aid != null ? 'aid' : 'viewed'] = aid ?? true;
var res = await Request().post(
Api.toViewDel,
queryParameters: {
'jsonp': 'jsonp',
'viewed': true,
'csrf': await Request.getCsrf(),
},
queryParameters: params,
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': 'yeah成功移除'};
@ -229,4 +233,64 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// 删除历史记录
static Future delHistory(kid) async {
var res = await Request().post(
Api.delHistory,
queryParameters: {
'kid': kid,
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '已删除'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 相互关系查询
static Future relationSearch(int mid) async {
Map params = await WbiSign().makSign({
'mid': mid,
'token': '',
'platform': 'web',
'web_location': 1550101,
});
var res = await Request().get(
Api.relationSearch,
data: {
'mid': mid,
'w_rid': params['w_rid'],
'wts': params['wts'],
},
);
if (res.data['code'] == 0) {
// relation 主动状态
// 被动状态
return {'status': true, 'data': res.data['data']};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
// 搜索历史记录
static Future searchHistory(
{required int pn, required String keyword}) async {
var res = await Request().get(
Api.searchHistory,
data: {
'pn': pn,
'keyword': keyword,
'business': 'all',
},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': HistoryData.fromJson(res.data['data'])};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

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'] 为结果
@ -20,6 +22,9 @@ import 'package:pilipala/utils/storage.dart';
class VideoHttp {
static Box localCache = GStrorage.localCache;
static Box setting = GStrorage.setting;
static bool enableRcmdDynamic =
setting.get(SettingBoxKey.enableRcmdDynamic, defaultValue: true);
static Box userInfoCache = GStrorage.userInfo;
// 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
@ -73,6 +78,7 @@ class VideoHttp {
for (var i in res.data['data']['items']) {
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
(!enableRcmdDynamic ? i['card_goto'] != 'picture' : true) &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
list.add(RecVideoItemAppModel.fromJson(i));
@ -130,6 +136,11 @@ class VideoHttp {
// 'platform': '',
// 'high_quality': ''
};
// 免登录查看1080p
if (userInfoCache.get('userInfoCache') == null &&
setting.get(SettingBoxKey.p1080, defaultValue: true)) {
data['try_look'] = 1;
}
try {
var res = await Request().get(Api.videoUrl, data: data);
if (res.data['code'] == 0) {
@ -411,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

@ -1,4 +1,11 @@
<<<<<<< HEAD
import 'package:audio_service/audio_service.dart';
=======
import 'dart:io';
>>>>>>> main
import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@ -13,6 +20,8 @@ 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';
import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc.
@ -24,10 +33,23 @@ void main() async {
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown])
.then((_) async {
await GStrorage.init();
<<<<<<< HEAD
await AudioService.init<AudioHandler>(
builder: () => MyAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.guozhigq.pilipala.channel.audio',
androidNotificationChannelName: 'Music playback',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
androidNotificationIcon: 'drawable/audio_service_icon',
),
);
=======
await setupServiceLocator();
>>>>>>> main
runApp(const MyApp());
await Request.setCookie();
await Data.init();
await GStrorage.lazyInit();
// 小白条、导航栏沉浸
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
@ -35,6 +57,10 @@ void main() async {
systemNavigationBarDividerColor: Colors.transparent,
statusBarColor: Colors.transparent,
));
await Request.setCookie();
Data.init();
GStrorage.lazyInit();
PiliSchame.init();
});
}
@ -59,6 +85,23 @@ class MyApp extends StatelessWidget {
double textScale =
setting.get(SettingBoxKey.defaultTextScale, defaultValue: 1.0);
// 强制设置高帧率
if (Platform.isAndroid) {
try {
late List modes;
FlutterDisplayMode.supported.then((value) {
modes = value;
var storageDisplay = setting.get(SettingBoxKey.displayMode);
DisplayMode f = DisplayMode.auto;
if (storageDisplay != null) {
f = modes.firstWhere((e) => e.toString() == storageDisplay);
}
DisplayMode preferred = modes.toList().firstWhere((el) => el == f);
FlutterDisplayMode.setPreferredMode(preferred);
});
} catch (_) {}
}
return DynamicColorBuilder(
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme;
@ -133,3 +176,34 @@ class MyApp extends StatelessWidget {
);
}
}
class MyAudioHandler extends BaseAudioHandler
with
QueueHandler, // mix in default queue callback implementations
SeekHandler {
// mix in default seek callback implementations
// The most common callbacks:
@override
Future<void> play() async {
print('play');
// All 'play' requests from all origins route to here. Implement this
// callback to start playing audio appropriate to your app. e.g. music.
}
///
@override
Future<void> pause() async {}
///
@override
Future<void> stop() async {}
///
@override
Future<void> seek(Duration position) async {}
///
@override
Future<void> skipToQueueItem(int i) async {}
}

View File

@ -12,20 +12,20 @@ enum SearchType {
live_room,
// 主播live_user
// live_user,
// 专栏article
// article,
// 话题topic
// topic,
// 用户bili_user
bili_user,
// 专栏article
article,
// 相簿photo
// photo
}
extension SearchTypeExtension on SearchType {
String get type =>
['video', 'media_bangumi', 'live_room', 'bili_user'][index];
String get label => ['视频', '番剧', '直播间', '用户'][index];
['video', 'media_bangumi', 'live_room', 'bili_user', 'article'][index];
String get label => ['视频', '番剧', '直播间', '用户', '专栏'][index];
}
// 搜索类型为视频、专栏及相簿时

10194
lib/models/danmaku/dm.pb.dart Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,347 @@
///
// Generated code. Do not modify.
// source: dm.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name
// ignore_for_file: UNDEFINED_SHOWN_NAME
import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb;
class AvatarType extends $pb.ProtobufEnum {
static const AvatarType AvatarTypeNone = AvatarType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'AvatarTypeNone');
static const AvatarType AvatarTypeNFT = AvatarType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'AvatarTypeNFT');
static const $core.List<AvatarType> values = <AvatarType>[
AvatarTypeNone,
AvatarTypeNFT,
];
static final $core.Map<$core.int, AvatarType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static AvatarType? valueOf($core.int value) => _byValue[value];
const AvatarType._($core.int v, $core.String n) : super(v, n);
}
class BubbleType extends $pb.ProtobufEnum {
static const BubbleType BubbleTypeNone = BubbleType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'BubbleTypeNone');
static const BubbleType BubbleTypeClickButton = BubbleType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'BubbleTypeClickButton');
static const BubbleType BubbleTypeDmSettingPanel = BubbleType._(
2,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'BubbleTypeDmSettingPanel');
static const $core.List<BubbleType> values = <BubbleType>[
BubbleTypeNone,
BubbleTypeClickButton,
BubbleTypeDmSettingPanel,
];
static final $core.Map<$core.int, BubbleType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static BubbleType? valueOf($core.int value) => _byValue[value];
const BubbleType._($core.int v, $core.String n) : super(v, n);
}
class CheckboxType extends $pb.ProtobufEnum {
static const CheckboxType CheckboxTypeNone = CheckboxType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'CheckboxTypeNone');
static const CheckboxType CheckboxTypeEncourage = CheckboxType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'CheckboxTypeEncourage');
static const CheckboxType CheckboxTypeColorDM = CheckboxType._(
2,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'CheckboxTypeColorDM');
static const $core.List<CheckboxType> values = <CheckboxType>[
CheckboxTypeNone,
CheckboxTypeEncourage,
CheckboxTypeColorDM,
];
static final $core.Map<$core.int, CheckboxType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static CheckboxType? valueOf($core.int value) => _byValue[value];
const CheckboxType._($core.int v, $core.String n) : super(v, n);
}
class DMAttrBit extends $pb.ProtobufEnum {
static const DMAttrBit DMAttrBitProtect = DMAttrBit._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'DMAttrBitProtect');
static const DMAttrBit DMAttrBitFromLive = DMAttrBit._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'DMAttrBitFromLive');
static const DMAttrBit DMAttrHighLike = DMAttrBit._(
2,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'DMAttrHighLike');
static const $core.List<DMAttrBit> values = <DMAttrBit>[
DMAttrBitProtect,
DMAttrBitFromLive,
DMAttrHighLike,
];
static final $core.Map<$core.int, DMAttrBit> _byValue =
$pb.ProtobufEnum.initByValue(values);
static DMAttrBit? valueOf($core.int value) => _byValue[value];
const DMAttrBit._($core.int v, $core.String n) : super(v, n);
}
class ExposureType extends $pb.ProtobufEnum {
static const ExposureType ExposureTypeNone = ExposureType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'ExposureTypeNone');
static const ExposureType ExposureTypeDMSend = ExposureType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'ExposureTypeDMSend');
static const $core.List<ExposureType> values = <ExposureType>[
ExposureTypeNone,
ExposureTypeDMSend,
];
static final $core.Map<$core.int, ExposureType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static ExposureType? valueOf($core.int value) => _byValue[value];
const ExposureType._($core.int v, $core.String n) : super(v, n);
}
class PostPanelBizType extends $pb.ProtobufEnum {
static const PostPanelBizType PostPanelBizTypeNone = PostPanelBizType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostPanelBizTypeNone');
static const PostPanelBizType PostPanelBizTypeEncourage = PostPanelBizType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostPanelBizTypeEncourage');
static const PostPanelBizType PostPanelBizTypeColorDM = PostPanelBizType._(
2,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostPanelBizTypeColorDM');
static const PostPanelBizType PostPanelBizTypeNFTDM = PostPanelBizType._(
3,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostPanelBizTypeNFTDM');
static const PostPanelBizType PostPanelBizTypeFragClose = PostPanelBizType._(
4,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostPanelBizTypeFragClose');
static const PostPanelBizType PostPanelBizTypeRecommend = PostPanelBizType._(
5,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostPanelBizTypeRecommend');
static const $core.List<PostPanelBizType> values = <PostPanelBizType>[
PostPanelBizTypeNone,
PostPanelBizTypeEncourage,
PostPanelBizTypeColorDM,
PostPanelBizTypeNFTDM,
PostPanelBizTypeFragClose,
PostPanelBizTypeRecommend,
];
static final $core.Map<$core.int, PostPanelBizType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static PostPanelBizType? valueOf($core.int value) => _byValue[value];
const PostPanelBizType._($core.int v, $core.String n) : super(v, n);
}
class PostStatus extends $pb.ProtobufEnum {
static const PostStatus PostStatusNormal = PostStatus._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostStatusNormal');
static const PostStatus PostStatusClosed = PostStatus._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'PostStatusClosed');
static const $core.List<PostStatus> values = <PostStatus>[
PostStatusNormal,
PostStatusClosed,
];
static final $core.Map<$core.int, PostStatus> _byValue =
$pb.ProtobufEnum.initByValue(values);
static PostStatus? valueOf($core.int value) => _byValue[value];
const PostStatus._($core.int v, $core.String n) : super(v, n);
}
class RenderType extends $pb.ProtobufEnum {
static const RenderType RenderTypeNone = RenderType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'RenderTypeNone');
static const RenderType RenderTypeSingle = RenderType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'RenderTypeSingle');
static const RenderType RenderTypeRotation = RenderType._(
2,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'RenderTypeRotation');
static const $core.List<RenderType> values = <RenderType>[
RenderTypeNone,
RenderTypeSingle,
RenderTypeRotation,
];
static final $core.Map<$core.int, RenderType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static RenderType? valueOf($core.int value) => _byValue[value];
const RenderType._($core.int v, $core.String n) : super(v, n);
}
class SubtitleAiStatus extends $pb.ProtobufEnum {
static const SubtitleAiStatus None = SubtitleAiStatus._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'None');
static const SubtitleAiStatus Exposure = SubtitleAiStatus._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'Exposure');
static const SubtitleAiStatus Assist = SubtitleAiStatus._(
2,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'Assist');
static const $core.List<SubtitleAiStatus> values = <SubtitleAiStatus>[
None,
Exposure,
Assist,
];
static final $core.Map<$core.int, SubtitleAiStatus> _byValue =
$pb.ProtobufEnum.initByValue(values);
static SubtitleAiStatus? valueOf($core.int value) => _byValue[value];
const SubtitleAiStatus._($core.int v, $core.String n) : super(v, n);
}
class SubtitleAiType extends $pb.ProtobufEnum {
static const SubtitleAiType Normal = SubtitleAiType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'Normal');
static const SubtitleAiType Translate = SubtitleAiType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'Translate');
static const $core.List<SubtitleAiType> values = <SubtitleAiType>[
Normal,
Translate,
];
static final $core.Map<$core.int, SubtitleAiType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static SubtitleAiType? valueOf($core.int value) => _byValue[value];
const SubtitleAiType._($core.int v, $core.String n) : super(v, n);
}
class SubtitleType extends $pb.ProtobufEnum {
static const SubtitleType CC = SubtitleType._(0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'CC');
static const SubtitleType AI = SubtitleType._(1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'AI');
static const $core.List<SubtitleType> values = <SubtitleType>[
CC,
AI,
];
static final $core.Map<$core.int, SubtitleType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static SubtitleType? valueOf($core.int value) => _byValue[value];
const SubtitleType._($core.int v, $core.String n) : super(v, n);
}
class ToastFunctionType extends $pb.ProtobufEnum {
static const ToastFunctionType ToastFunctionTypeNone = ToastFunctionType._(
0,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'ToastFunctionTypeNone');
static const ToastFunctionType ToastFunctionTypePostPanel =
ToastFunctionType._(
1,
const $core.bool.fromEnvironment('protobuf.omit_enum_names')
? ''
: 'ToastFunctionTypePostPanel');
static const $core.List<ToastFunctionType> values = <ToastFunctionType>[
ToastFunctionTypeNone,
ToastFunctionTypePostPanel,
];
static final $core.Map<$core.int, ToastFunctionType> _byValue =
$pb.ProtobufEnum.initByValue(values);
static ToastFunctionType? valueOf($core.int value) => _byValue[value];
const ToastFunctionType._($core.int v, $core.String n) : super(v, n);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,74 @@
///
// Generated code. Do not modify.
// source: dm.proto
//
// @dart = 2.12
// ignore_for_file: annotate_overrides,camel_case_types,constant_identifier_names,deprecated_member_use_from_same_package,directives_ordering,library_prefixes,non_constant_identifier_names,prefer_final_fields,return_of_invalid_type,unnecessary_const,unnecessary_import,unnecessary_this,unused_import,unused_shown_name, avoid_renaming_method_parameters
import 'dart:async' as $async;
import 'package:protobuf/protobuf.dart' as $pb;
import 'dart:core' as $core;
import 'dm.pb.dart' as $0;
import 'dm.pbjson.dart';
export 'dm.pb.dart';
abstract class DMServiceBase extends $pb.GeneratedService {
$async.Future<$0.DmSegMobileReply> dmSegMobile(
$pb.ServerContext ctx, $0.DmSegMobileReq request);
$async.Future<$0.DmViewReply> dmView(
$pb.ServerContext ctx, $0.DmViewReq request);
$async.Future<$0.Response> dmPlayerConfig(
$pb.ServerContext ctx, $0.DmPlayerConfigReq request);
$async.Future<$0.DmSegOttReply> dmSegOtt(
$pb.ServerContext ctx, $0.DmSegOttReq request);
$async.Future<$0.DmSegSDKReply> dmSegSDK(
$pb.ServerContext ctx, $0.DmSegSDKReq request);
$async.Future<$0.DmExpoReportRes> dmExpoReport(
$pb.ServerContext ctx, $0.DmExpoReportReq request);
$pb.GeneratedMessage createRequest($core.String method) {
switch (method) {
case 'DmSegMobile':
return $0.DmSegMobileReq();
case 'DmView':
return $0.DmViewReq();
case 'DmPlayerConfig':
return $0.DmPlayerConfigReq();
case 'DmSegOtt':
return $0.DmSegOttReq();
case 'DmSegSDK':
return $0.DmSegSDKReq();
case 'DmExpoReport':
return $0.DmExpoReportReq();
default:
throw $core.ArgumentError('Unknown method: $method');
}
}
$async.Future<$pb.GeneratedMessage> handleCall($pb.ServerContext ctx,
$core.String method, $pb.GeneratedMessage request) {
switch (method) {
case 'DmSegMobile':
return this.dmSegMobile(ctx, request as $0.DmSegMobileReq);
case 'DmView':
return this.dmView(ctx, request as $0.DmViewReq);
case 'DmPlayerConfig':
return this.dmPlayerConfig(ctx, request as $0.DmPlayerConfigReq);
case 'DmSegOtt':
return this.dmSegOtt(ctx, request as $0.DmSegOttReq);
case 'DmSegSDK':
return this.dmSegSDK(ctx, request as $0.DmSegSDKReq);
case 'DmExpoReport':
return this.dmExpoReport(ctx, request as $0.DmExpoReportReq);
default:
throw $core.ArgumentError('Unknown method: $method');
}
}
$core.Map<$core.String, $core.dynamic> get $json => DMServiceBase$json;
$core.Map<$core.String, $core.Map<$core.String, $core.dynamic>>
get $messageJson => DMServiceBase$messageJson;
}

893
lib/models/danmaku/dm.proto Normal file
View File

@ -0,0 +1,893 @@
syntax = "proto3";
package bilibili.community.service.dm.v1;
// 说明文档
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/danmaku/danmaku_proto.md
//弹幕
service DM {
// 获取分段弹幕
rpc DmSegMobile (DmSegMobileReq) returns (DmSegMobileReply);
// 客户端弹幕元数据 字幕、分段、防挡蒙版等
rpc DmView(DmViewReq) returns (DmViewReply);
// 修改弹幕配置
rpc DmPlayerConfig (DmPlayerConfigReq) returns (Response);
// ott弹幕列表
rpc DmSegOtt(DmSegOttReq) returns(DmSegOttReply);
// SDK弹幕列表
rpc DmSegSDK(DmSegSDKReq) returns(DmSegSDKReply);
//
rpc DmExpoReport(DmExpoReportReq) returns (DmExpoReportRes);
}
//
message Avatar {
//
string id = 1;
//
string url = 2;
//
AvatarType avatar_type = 3;
}
//
enum AvatarType {
AvatarTypeNone = 0; //
AvatarTypeNFT = 1; //
}
//
message Bubble {
//
string text = 1;
//
string url = 2;
}
//
enum BubbleType {
BubbleTypeNone = 0; //
BubbleTypeClickButton = 1; //
BubbleTypeDmSettingPanel = 2; //
}
//
message BubbleV2 {
//
string text = 1;
//
string url = 2;
//
BubbleType bubble_type = 3;
//
bool exposure_once = 4;
//
ExposureType exposure_type = 5;
}
//
message Button {
//
string text = 1;
//
int32 action = 2;
}
//
message BuzzwordConfig {
//
repeated BuzzwordShowConfig keywords = 1;
}
//
message BuzzwordShowConfig {
//
string name = 1;
//
string schema = 2;
//
int32 source = 3;
//
int64 id = 4;
//
int64 buzzword_id = 5;
//
int32 schema_type = 6;
}
//
message CheckBox {
//
string text = 1;
//
CheckboxType type = 2;
//
bool default_value = 3;
//
bool show = 4;
}
//
enum CheckboxType {
CheckboxTypeNone = 0; //
CheckboxTypeEncourage = 1; //
CheckboxTypeColorDM = 2; //
}
//
message CheckBoxV2 {
//
string text = 1;
//
int32 type = 2;
//
bool default_value = 3;
}
//
message ClickButton {
//
repeated string portrait_text = 1;
//
repeated string landscape_text = 2;
//
repeated string portrait_text_focus = 3;
//
repeated string landscape_text_focus = 4;
//
RenderType render_type = 5;
//
bool show = 6;
//
Bubble bubble = 7;
}
//
message ClickButtonV2 {
//
repeated string portrait_text = 1;
//
repeated string landscape_text = 2;
//
repeated string portrait_text_focus = 3;
//
repeated string landscape_text_focus = 4;
//
int32 render_type = 5;
//
bool text_input_post = 6;
//
bool exposure_once = 7;
//
int32 exposure_type = 8;
}
// 互动弹幕条目信息
message CommandDm {
// 弹幕id
int64 id = 1;
// 对象视频cid
int64 oid = 2;
// 发送者mid
string mid = 3;
// 互动弹幕指令
string command = 4;
// 互动弹幕正文
string content = 5;
// 出现时间
int32 progress = 6;
// 创建时间
string ctime = 7;
// 发布时间
string mtime = 8;
// 扩展json数据
string extra = 9;
// 弹幕id str类型
string idStr = 10;
}
// 弹幕ai云屏蔽列表
message DanmakuAIFlag {
// 弹幕ai云屏蔽条目
repeated DanmakuFlag dm_flags = 1;
}
// 弹幕条目
message DanmakuElem {
// 弹幕dmid
int64 id = 1;
// 弹幕出现位置(单位ms)
int32 progress = 2;
// 弹幕类型 1 2 3:普通弹幕 4:底部弹幕 5:顶部弹幕 6:逆向弹幕 7:高级弹幕 8:代码弹幕 9:BAS弹幕(pool必须为2)
int32 mode = 3;
// 弹幕字号
int32 fontsize = 4;
// 弹幕颜色
uint32 color = 5;
// 发送者mid hash
string midHash = 6;
// 弹幕正文
string content = 7;
// 发送时间
int64 ctime = 8;
// 权重 用于屏蔽等级 区间:[1,10]
int32 weight = 9;
// 动作
string action = 10;
// 弹幕池 0:普通池 1:字幕池 2:特殊池(代码/BAS弹幕)
int32 pool = 11;
// 弹幕dmid str
string idStr = 12;
// 弹幕属性位(bin求AND)
// bit0:保护 bit1:直播 bit2:高赞
int32 attr = 13;
//
string animation = 22;
// 大会员专属颜色
DmColorfulType colorful = 24;
}
// 弹幕ai云屏蔽条目
message DanmakuFlag {
// 弹幕dmid
int64 dmid = 1;
// 评分
uint32 flag = 2;
}
// 云屏蔽配置信息
message DanmakuFlagConfig {
// 云屏蔽等级
int32 rec_flag = 1;
// 云屏蔽文案
string rec_text = 2;
// 云屏蔽开关
int32 rec_switch = 3;
}
// 弹幕默认配置
message DanmuDefaultPlayerConfig {
bool player_danmaku_use_default_config = 1; // 是否使用推荐弹幕设置
bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽
int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级
bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕
bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕
bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕
bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕
bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕
bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕
float player_danmaku_opacity = 12; // 弹幕不透明度
float player_danmaku_scalingfactor = 13; // 弹幕缩放比例
float player_danmaku_domain = 14; // 弹幕显示区域
int32 player_danmaku_speed = 15; // 弹幕速度
bool inline_player_danmaku_switch = 16; // 是否开启弹幕
int32 player_danmaku_senior_mode_switch = 17; //
int32 player_danmaku_ai_recommended_level_v2 = 18; //
map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 19; //
}
// 弹幕配置
message DanmuPlayerConfig {
bool player_danmaku_switch = 1; // 是否开启弹幕
bool player_danmaku_switch_save = 2; // 是否记录弹幕开关设置
bool player_danmaku_use_default_config = 3; // 是否使用推荐弹幕设置
bool player_danmaku_ai_recommended_switch = 4; // 是否开启智能云屏蔽
int32 player_danmaku_ai_recommended_level = 5; // 智能云屏蔽等级
bool player_danmaku_blocktop = 6; // 是否屏蔽顶端弹幕
bool player_danmaku_blockscroll = 7; // 是否屏蔽滚动弹幕
bool player_danmaku_blockbottom = 8; // 是否屏蔽底端弹幕
bool player_danmaku_blockcolorful = 9; // 是否屏蔽彩色弹幕
bool player_danmaku_blockrepeat = 10; // 是否屏蔽重复弹幕
bool player_danmaku_blockspecial = 11; // 是否屏蔽高级弹幕
float player_danmaku_opacity = 12; // 弹幕不透明度
float player_danmaku_scalingfactor = 13; // 弹幕缩放比例
float player_danmaku_domain = 14; // 弹幕显示区域
int32 player_danmaku_speed = 15; // 弹幕速度
bool player_danmaku_enableblocklist = 16; // 是否开启屏蔽列表
bool inline_player_danmaku_switch = 17; // 是否开启弹幕
int32 inline_player_danmaku_config = 18; //
int32 player_danmaku_ios_switch_save = 19; //
int32 player_danmaku_senior_mode_switch = 20; //
int32 player_danmaku_ai_recommended_level_v2 = 21; //
map<int32, int32> player_danmaku_ai_recommended_level_v2_map = 22; //
}
//
message DanmuPlayerConfigPanel {
//
string selection_text = 1;
}
// 弹幕显示区域自动配置
message DanmuPlayerDynamicConfig {
// 时间
int32 progress = 1;
// 弹幕显示区域
float player_danmaku_domain = 14;
}
// 弹幕配置信息
message DanmuPlayerViewConfig {
// 弹幕默认配置
DanmuDefaultPlayerConfig danmuku_default_player_config = 1;
// 弹幕用户配置
DanmuPlayerConfig danmuku_player_config = 2;
// 弹幕显示区域自动配置列表
repeated DanmuPlayerDynamicConfig danmuku_player_dynamic_config = 3;
//
DanmuPlayerConfigPanel danmuku_player_config_panel = 4;
}
// web端用户弹幕配置
message DanmuWebPlayerConfig {
bool dm_switch = 1; // 是否开启弹幕
bool ai_switch = 2; // 是否开启智能云屏蔽
int32 ai_level = 3; // 智能云屏蔽等级
bool blocktop = 4; // 是否屏蔽顶端弹幕
bool blockscroll = 5; // 是否屏蔽滚动弹幕
bool blockbottom = 6; // 是否屏蔽底端弹幕
bool blockcolor = 7; // 是否屏蔽彩色弹幕
bool blockspecial = 8; // 是否屏蔽重复弹幕
bool preventshade = 9; //
bool dmask = 10; //
float opacity = 11; //
int32 dmarea = 12; //
float speedplus = 13; //
float fontsize = 14; // 弹幕字号
bool screensync = 15; //
bool speedsync = 16; //
string fontfamily = 17; //
bool bold = 18; // 是否使用加粗
int32 fontborder = 19; //
string draw_type = 20; // 弹幕渲染类型
int32 senior_mode_switch = 21; //
int32 ai_level_v2 = 22; //
map<int32, int32> ai_level_v2_map = 23; //
}
// 弹幕属性位值
enum DMAttrBit {
DMAttrBitProtect = 0; // 保护弹幕
DMAttrBitFromLive = 1; // 直播弹幕
DMAttrHighLike = 2; // 高赞弹幕
}
message DmColorful {
DmColorfulType type = 1; // 颜色类型
string src = 2; //
}
enum DmColorfulType {
NoneType = 0; // 无
VipGradualColor = 60001; // 渐变色
}
//
message DmExpoReportReq {
//
string session_id = 1;
//
int64 oid = 2;
//
string spmid = 4;
}
//
message DmExpoReportRes {}
// 修改弹幕配置-请求
message DmPlayerConfigReq {
int64 ts = 1; //
PlayerDanmakuSwitch switch = 2; // 是否开启弹幕
PlayerDanmakuSwitchSave switch_save = 3; // 是否记录弹幕开关设置
PlayerDanmakuUseDefaultConfig use_default_config = 4; // 是否使用推荐弹幕设置
PlayerDanmakuAiRecommendedSwitch ai_recommended_switch = 5; // 是否开启智能云屏蔽
PlayerDanmakuAiRecommendedLevel ai_recommended_level = 6; // 智能云屏蔽等级
PlayerDanmakuBlocktop blocktop = 7; // 是否屏蔽顶端弹幕
PlayerDanmakuBlockscroll blockscroll = 8; // 是否屏蔽滚动弹幕
PlayerDanmakuBlockbottom blockbottom = 9; // 是否屏蔽底端弹幕
PlayerDanmakuBlockcolorful blockcolorful = 10; // 是否屏蔽彩色弹幕
PlayerDanmakuBlockrepeat blockrepeat = 11; // 是否屏蔽重复弹幕
PlayerDanmakuBlockspecial blockspecial = 12; // 是否屏蔽高级弹幕
PlayerDanmakuOpacity opacity = 13; // 弹幕不透明度
PlayerDanmakuScalingfactor scalingfactor = 14; // 弹幕缩放比例
PlayerDanmakuDomain domain = 15; // 弹幕显示区域
PlayerDanmakuSpeed speed = 16; // 弹幕速度
PlayerDanmakuEnableblocklist enableblocklist = 17; // 是否开启屏蔽列表
InlinePlayerDanmakuSwitch inlinePlayerDanmakuSwitch = 18; // 是否开启弹幕
PlayerDanmakuSeniorModeSwitch senior_mode_switch = 19; //
PlayerDanmakuAiRecommendedLevelV2 ai_recommended_level_v2 = 20; //
}
//
message DmSegConfig {
//
int64 page_size = 1;
//
int64 total = 2;
}
// 获取弹幕-响应
message DmSegMobileReply {
// 弹幕列表
repeated DanmakuElem elems = 1;
// 是否已关闭弹幕
// 0:未关闭 1:已关闭
int32 state = 2;
// 弹幕云屏蔽ai评分值
DanmakuAIFlag ai_flag = 3;
repeated DmColorful colorfulSrc = 5;
}
// 获取弹幕-请求
message DmSegMobileReq {
// 稿件avid/漫画epid
int64 pid = 1;
// 视频cid/漫画cid
int64 oid = 2;
// 弹幕类型
// 1:视频 2:漫画
int32 type = 3;
// 分段(6min)
int64 segment_index = 4;
// 是否青少年模式
int32 teenagers_mode = 5;
//
int64 ps = 6;
//
int64 pe = 7;
//
int32 pull_mode = 8;
//
int32 from_scene = 9;
}
// ott弹幕列表-响应
message DmSegOttReply {
// 是否已关闭弹幕
// 0:未关闭 1:已关闭
bool closed = 1;
// 弹幕列表
repeated DanmakuElem elems = 2;
}
// ott弹幕列表-请求
message DmSegOttReq {
// 稿件avid/漫画epid
int64 pid = 1;
// 视频cid/漫画cid
int64 oid = 2;
// 弹幕类型
// 1:视频 2:漫画
int32 type = 3;
// 分段(6min)
int64 segment_index = 4;
}
// 弹幕SDK-响应
message DmSegSDKReply {
// 是否已关闭弹幕
// 0:未关闭 1:已关闭
bool closed = 1;
// 弹幕列表
repeated DanmakuElem elems = 2;
}
// 弹幕SDK-请求
message DmSegSDKReq {
// 稿件avid/漫画epid
int64 pid = 1;
// 视频cid/漫画cid
int64 oid = 2;
// 弹幕类型
// 1:视频 2:漫画
int32 type = 3;
// 分段(6min)
int64 segment_index = 4;
}
// 客户端弹幕元数据-响应
message DmViewReply {
// 是否已关闭弹幕
// 0:未关闭 1:已关闭
bool closed = 1;
// 智能防挡弹幕蒙版信息
VideoMask mask = 2;
// 视频字幕
VideoSubtitle subtitle = 3;
// 高级弹幕专包url(bfs)
repeated string special_dms = 4;
// 云屏蔽配置信息
DanmakuFlagConfig ai_flag = 5;
// 弹幕配置信息
DanmuPlayerViewConfig player_config = 6;
// 弹幕发送框样式
int32 send_box_style = 7;
// 是否允许
bool allow = 8;
// check box 是否展示
string check_box = 9;
// check box 展示文本
string check_box_show_msg = 10;
// 展示文案
string text_placeholder = 11;
// 弹幕输入框文案
string input_placeholder = 12;
// 用户举报弹幕 cid维度屏蔽的正则规则
repeated string report_filter_content = 13;
//
ExpoReport expo_report = 14;
//
BuzzwordConfig buzzword_config = 15;
//
repeated Expressions expressions = 16;
//
repeated PostPanel post_panel = 17;
//
repeated string activity_meta = 18;
//
repeated PostPanelV2 post_panel2 = 19;
}
// 客户端弹幕元数据-请求
message DmViewReq {
// 稿件avid/漫画epid
int64 pid = 1;
// 视频cid/漫画cid
int64 oid = 2;
// 弹幕类型
// 1:视频 2:漫画
int32 type = 3;
// 页面spm
string spmid = 4;
// 是否冷启
int32 is_hard_boot = 5;
}
// web端弹幕元数据-响应
// https://api.bilibili.com/x/v2/dm/web/view
message DmWebViewReply {
// 是否已关闭弹幕
// 0:未关闭 1:已关闭
int32 state = 1;
//
string text = 2;
//
string text_side = 3;
// 分段弹幕配置
DmSegConfig dm_sge = 4;
// 云屏蔽配置信息
DanmakuFlagConfig flag = 5;
// 高级弹幕专包url(bfs)
repeated string special_dms = 6;
// check box 是否展示
bool check_box = 7;
// 弹幕数
int64 count = 8;
// 互动弹幕
repeated CommandDm commandDms = 9;
// 用户弹幕配置
DanmuWebPlayerConfig player_config = 10;
// 用户举报弹幕 cid维度屏蔽
repeated string report_filter_content = 11;
//
repeated Expressions expressions = 12;
//
repeated PostPanel post_panel = 13;
//
repeated string activity_meta = 14;
}
//
message ExpoReport {
//
bool should_report_at_end = 1;
}
//
enum ExposureType {
ExposureTypeNone = 0; //
ExposureTypeDMSend = 1; //
}
//
message Expression {
//
repeated string keyword = 1;
//
string url = 2;
//
repeated Period period = 3;
}
//
message Expressions {
//
repeated Expression data = 1;
}
// 是否开启弹幕
message InlinePlayerDanmakuSwitch {
//
bool value = 1;
}
//
message Label {
//
string title = 1;
//
repeated string content = 2;
}
//
message LabelV2 {
//
string title = 1;
//
repeated string content = 2;
//
bool exposure_once = 3;
//
int32 exposure_type = 4;
}
//
message Period {
//
int64 start = 1;
//
int64 end = 2;
}
message PlayerDanmakuAiRecommendedLevel {bool value = 1;} // 智能云屏蔽等级
message PlayerDanmakuAiRecommendedLevelV2 {int32 value = 1;} //
message PlayerDanmakuAiRecommendedSwitch {bool value = 1;} // 是否开启智能云屏蔽
message PlayerDanmakuBlockbottom {bool value = 1;} // 是否屏蔽底端弹幕
message PlayerDanmakuBlockcolorful {bool value = 1;} // 是否屏蔽彩色弹幕
message PlayerDanmakuBlockrepeat {bool value = 1;} // 是否屏蔽重复弹幕
message PlayerDanmakuBlockscroll {bool value = 1;} // 是否屏蔽滚动弹幕
message PlayerDanmakuBlockspecial {bool value = 1;} // 是否屏蔽高级弹幕
message PlayerDanmakuBlocktop {bool value = 1;} // 是否屏蔽顶端弹幕
message PlayerDanmakuDomain {float value = 1;} // 弹幕显示区域
message PlayerDanmakuEnableblocklist {bool value = 1;} // 是否开启屏蔽列表
message PlayerDanmakuOpacity {float value = 1;} // 弹幕不透明度
message PlayerDanmakuScalingfactor {float value = 1;} // 弹幕缩放比例
message PlayerDanmakuSeniorModeSwitch {int32 value = 1;} //
message PlayerDanmakuSpeed {int32 value = 1;} // 弹幕速度
message PlayerDanmakuSwitch {bool value = 1; bool can_ignore = 2;} // 是否开启弹幕
message PlayerDanmakuSwitchSave {bool value = 1;} // 是否记录弹幕开关设置
message PlayerDanmakuUseDefaultConfig {bool value = 1;} // 是否使用推荐弹幕设置
//
message PostPanel {
//
int64 start = 1;
//
int64 end = 2;
//
int64 priority = 3;
//
int64 biz_id = 4;
//
PostPanelBizType biz_type = 5;
//
ClickButton click_button = 6;
//
TextInput text_input = 7;
//
CheckBox check_box = 8;
//
Toast toast = 9;
}
//
enum PostPanelBizType {
PostPanelBizTypeNone = 0; //
PostPanelBizTypeEncourage = 1; //
PostPanelBizTypeColorDM = 2; //
PostPanelBizTypeNFTDM = 3; //
PostPanelBizTypeFragClose = 4; //
PostPanelBizTypeRecommend = 5; //
}
//
message PostPanelV2 {
//
int64 start = 1;
//
int64 end = 2;
//
int32 biz_type = 3;
//
ClickButtonV2 click_button = 4;
//
TextInputV2 text_input = 5;
//
CheckBoxV2 check_box = 6;
//
ToastV2 toast = 7;
//
BubbleV2 bubble = 8;
//
LabelV2 label = 9;
//
int32 post_status = 10;
}
//
enum PostStatus {
PostStatusNormal = 0; //
PostStatusClosed = 1; //
}
//
enum RenderType {
RenderTypeNone = 0; //
RenderTypeSingle = 1; //
RenderTypeRotation = 2; //
}
// 修改弹幕配置-响应
message Response {
//
int32 code = 1;
//
string message = 2;
}
//
enum SubtitleAiStatus {
None = 0; //
Exposure = 1; //
Assist = 2; //
}
//
enum SubtitleAiType {
Normal = 0; //
Translate = 1; //
}
// 单个字幕信息
message SubtitleItem {
// 字幕id
int64 id = 1;
// 字幕id str
string id_str = 2;
// 字幕语言代码
string lan = 3;
// 字幕语言
string lan_doc = 4;
// 字幕文件url
string subtitle_url = 5;
// 字幕作者信息
UserInfo author = 6;
// 字幕类型
SubtitleType type = 7;
//
string lan_doc_brief = 8;
//
SubtitleAiType ai_type = 9;
//
SubtitleAiStatus ai_status = 10;
}
enum SubtitleType {
CC = 0; // CC字幕
AI = 1; // AI生成字幕
}
//
message TextInput {
//
repeated string portrait_placeholder = 1;
//
repeated string landscape_placeholder = 2;
//
RenderType render_type = 3;
//
bool placeholder_post = 4;
//
bool show = 5;
//
repeated Avatar avatar = 6;
//
PostStatus post_status = 7;
//
Label label = 8;
}
//
message TextInputV2 {
//
repeated string portrait_placeholder = 1;
//
repeated string landscape_placeholder = 2;
//
RenderType render_type = 3;
//
bool placeholder_post = 4;
//
repeated Avatar avatar = 5;
//
int32 text_input_limit = 6;
}
//
message Toast {
//
string text = 1;
//
int32 duration = 2;
//
bool show = 3;
//
Button button = 4;
}
//
message ToastButtonV2 {
//
string text = 1;
//
int32 action = 2;
}
//
enum ToastFunctionType {
ToastFunctionTypeNone = 0; //
ToastFunctionTypePostPanel = 1; //
}
//
message ToastV2 {
//
string text = 1;
//
int32 duration = 2;
//
ToastButtonV2 toast_button_v2 = 3;
}
// 字幕作者信息
message UserInfo {
// 用户mid
int64 mid = 1;
// 用户昵称
string name = 2;
// 用户性别
string sex = 3;
// 用户头像url
string face = 4;
// 用户签名
string sign = 5;
// 用户等级
int32 rank = 6;
}
// 智能防挡弹幕蒙版信息
message VideoMask {
// 视频cid
int64 cid = 1;
// 平台
// 0:web端 1:客户端
int32 plat = 2;
// 帧率
int32 fps = 3;
// 间隔时间
int64 time = 4;
// 蒙版url
string mask_url = 5;
}
// 视频字幕信息
message VideoSubtitle {
// 视频原语言代码
string lan = 1;
// 视频原语言
string lanDoc = 2;
// 视频字幕列表
repeated SubtitleItem subtitles = 3;
}

View File

@ -244,7 +244,9 @@ class Vote {
choiceCnt = json['choice_cnt'];
share = json['share'];
defaultShare = json['default_share'];
endTime = json['end_time'];
endTime = json['end_time'] is int
? json['end_time']
: int.parse(json['end_time']);
joinNum = json['join_num'];
status = json['status'];
type = json['type'];

View File

@ -8,7 +8,7 @@ class FollowDataModel {
List<FollowItemModel>? list;
FollowDataModel.fromJson(Map<String, dynamic> json) {
total = json['total'];
total = json['total'] ?? 0;
list = json['list']
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList();
@ -19,7 +19,7 @@ class FollowItemModel {
FollowItemModel({
this.mid,
this.attribute,
this.mtime,
// this.mtime,
this.tag,
this.special,
this.uname,
@ -30,7 +30,7 @@ class FollowItemModel {
int? mid;
int? attribute;
int? mtime;
// int? mtime;
List? tag;
int? special;
String? uname;
@ -41,7 +41,7 @@ class FollowItemModel {
FollowItemModel.fromJson(Map<String, dynamic> json) {
mid = json['mid'];
attribute = json['attribute'];
mtime = json['mtime'];
// mtime = json['mtime'];
tag = json['tag'];
special = json['special'];
uname = json['uname'];

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,23 @@
class MemberTagItemModel {
MemberTagItemModel({
this.count,
this.name,
this.tagid,
this.tip,
this.checked,
});
int? count;
String? name;
int? tagid;
String? tip;
bool? checked;
MemberTagItemModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
name = json['name'];
tagid = json['tagid'];
tip = json['tip'];
checked = false;
}
}

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList();
}
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id,
this.cid,
// this.author,
// this.mid,
this.mid,
// this.typeid,
// this.typename,
this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id;
int? cid;
// String? author;
// String? mid;
int? mid;
// String? typeid;
// String? typename;
String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl'];
aid = json['aid'];
bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']);
description = json['description'];
@ -376,3 +378,75 @@ class SearchMBangumiItemModel {
indexShow = json['index_show'];
}
}
class SearchArticleModel {
SearchArticleModel({this.list});
List<SearchArticleItemModel>? list;
SearchArticleModel.fromJson(Map<String, dynamic> json) {
list = json['result'] != null
? json['result']
.map<SearchArticleItemModel>(
(e) => SearchArticleItemModel.fromJson(e))
.toList()
: [];
}
}
class SearchArticleItemModel {
SearchArticleItemModel({
this.pubTime,
this.like,
this.title,
this.subTitle,
this.rankOffset,
this.mid,
this.imageUrls,
this.id,
this.categoryId,
this.view,
this.reply,
this.desc,
this.rankScore,
this.type,
this.templateId,
this.categoryName,
});
int? pubTime;
int? like;
List? title;
String? subTitle;
int? rankOffset;
int? mid;
List? imageUrls;
int? id;
int? categoryId;
int? view;
int? reply;
String? desc;
int? rankScore;
String? type;
int? templateId;
String? categoryName;
SearchArticleItemModel.fromJson(Map<String, dynamic> json) {
pubTime = json['pub_time'];
like = json['like'];
title = Em.regTitle(json['title']);
subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), '');
rankOffset = json['rank_offset'];
mid = json['mid'];
imageUrls = json['image_urls'];
id = json['id'];
categoryId = json['category_id'];
view = json['view'];
reply = json['reply'];
desc = json['desc'];
rankScore = json['rank_score'];
type = json['type'];
templateId = json['templateId'];
categoryName = json['category_name'];
}
}

View File

@ -3,17 +3,23 @@ class HistoryData {
this.cursor,
this.tab,
this.list,
this.page,
});
Cursor? cursor;
List<HisTabItem>? tab;
List<HisListItem>? list;
Map? page;
HistoryData.fromJson(Map<String, dynamic> json) {
cursor = Cursor.fromJson(json['cursor']);
tab = json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList();
list =
json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList();
cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null;
tab = json['tab'] != null
? json['tab'].map<HisTabItem>((e) => HisTabItem.fromJson(e)).toList()
: [];
list = json['list'] != null
? json['list'].map<HisListItem>((e) => HisListItem.fromJson(e)).toList()
: [];
page = json['page'];
}
}
@ -79,6 +85,7 @@ class HisListItem {
this.kid,
this.tagName,
this.liveStatus,
this.checked,
});
String? title;
@ -105,6 +112,7 @@ class HisListItem {
int? kid;
String? tagName;
int? liveStatus;
bool? checked;
HisListItem.fromJson(Map<String, dynamic> json) {
title = json['title'];
@ -131,6 +139,7 @@ class HisListItem {
kid = json['kid'];
tagName = json['tag_name'];
liveStatus = json['live_status'];
checked = false;
}
}

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,3 +1,5 @@
import 'dart:developer';
import 'package:pilipala/models/video/play/quality.dart';
class PlayUrlModel {
@ -77,8 +79,8 @@ class Dash {
double? minBufferTime;
List<VideoItem>? video;
List<AudioItem>? audio;
Map? dolby;
Map? flac;
Dolby? dolby;
Flac? flac;
Dash.fromJson(Map<String, dynamic> json) {
duration = json['duration'];
@ -87,8 +89,8 @@ class Dash {
audio = json['audio'] != null
? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()
: [];
dolby = json['dolby'];
flac = json['flac'] ?? {};
dolby = json['dolby'] != null ? Dolby.fromJson(json['dolby']) : null;
flac = json['flac'] != null ? Flac.fromJson(json['flac']) : null;
}
}
@ -128,7 +130,8 @@ class VideoItem {
VideoItem.fromJson(Map<String, dynamic> json) {
id = json['id'];
baseUrl = json['baseUrl'];
backupUrl = json['backupUrl'].toList().first;
backupUrl =
json['backupUrl'] != null ? json['backupUrl'].toList().first : '';
bandWidth = json['bandWidth'];
mimeType = json['mime_type'];
codecs = json['codecs'];
@ -179,7 +182,8 @@ class AudioItem {
AudioItem.fromJson(Map<String, dynamic> json) {
id = json['id'];
baseUrl = json['baseUrl'];
backupUrl = json['backupUrl'].toList().first;
backupUrl =
json['backupUrl'] != null ? json['backupUrl'].toList().first : '';
bandWidth = json['bandWidth'];
mimeType = json['mime_type'];
codecs = json['codecs'];
@ -218,3 +222,33 @@ class FormatItem {
codecs = json['codecs'];
}
}
class Dolby {
Dolby({
this.type,
this.audio,
});
// 1普通杜比音效 2全景杜比音效
int? type;
List<AudioItem>? audio;
Dolby.fromJson(Map<String, dynamic> json) {
type = json['type'];
audio = json['audio'] != null
? json['audio'].map<AudioItem>((e) => AudioItem.fromJson(e)).toList()
: [];
}
}
class Flac {
Flac({this.display, this.audio});
bool? display;
AudioItem? audio;
Flac.fromJson(Map<String, dynamic> json) {
display = json['display'];
audio = json['audio'] != null ? AudioItem.fromJson(json['audio']) : null;
}
}

View File

@ -95,6 +95,17 @@ class _AboutPageState extends State<AboutPage> {
style: subTitleStyle,
),
),
ListTile(
onTap: () => _aboutController.panDownload(),
title: const Text('网盘下载'),
trailing: Text(
'提取码pili',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
),
),
),
ListTile(
onTap: () => _aboutController.feedback(),
title: const Text('问题反馈'),
@ -106,7 +117,7 @@ class _AboutPageState extends State<AboutPage> {
),
ListTile(
onTap: () => _aboutController.qqChanel(),
title: const Text('QQ频道'),
title: const Text('QQ'),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
@ -173,7 +184,7 @@ class AboutController extends GetxController {
// 获取远程版本
Future getRemoteApp() async {
var result = await Request().get(Api.latestApp);
var result = await Request().get(Api.latestApp, extra: {'ua': 'pc'});
data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data;
remoteVersion.value = data.tagName!;
@ -195,6 +206,14 @@ class AboutController extends GetxController {
);
}
// 从网盘下载
panDownload() {
launchUrl(
Uri.parse('https://www.123pan.com/s/9sVqVv-flu0A.html'),
mode: LaunchMode.externalApplication,
);
}
// 问题反馈
feedback() {
launchUrl(
@ -207,17 +226,9 @@ class AboutController extends GetxController {
// qq频道
qqChanel() {
Clipboard.setData(
const ClipboardData(text: 'https://pd.qq.com/s/css9rdwga'),
);
SmartDialog.showToast(
'已复制,即将在浏览器打开',
displayTime: const Duration(milliseconds: 500),
).then(
(value) => launchUrl(
Uri.parse('https://pd.qq.com/s/css9rdwga'),
mode: LaunchMode.externalApplication,
),
const ClipboardData(text: '489981949'),
);
SmartDialog.showToast('已复制QQ群号');
}
// tg频道

View File

View File

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

539
lib/pages/audio/view.dart Normal file
View File

@ -0,0 +1,539 @@
import 'dart:math';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
// import 'package:just_audio_background/just_audio_background.dart';
class AudioPlayerPage extends StatefulWidget {
const AudioPlayerPage({super.key});
@override
State<AudioPlayerPage> createState() => _AudioPlayerPageState();
}
class _AudioPlayerPageState extends State<AudioPlayerPage> {
static int _nextMediaId = 0;
late AudioPlayer _player;
final _playlist = ConcatenatingAudioSource(children: [
ClippingAudioSource(
start: const Duration(seconds: 0),
end: const Duration(seconds: 90),
child: AudioSource.uri(Uri.parse(
"https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3")),
tag: MediaItem(
id: '${_nextMediaId++}',
album: "Science Friday",
title: "A Salute To Head-Scratching Science (30 seconds)",
artUri: Uri.parse(
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
),
),
// AudioSource.uri(
// Uri.parse(
// "https://upos-sz-mirror08c.bilivideo.com/upgcxcode/05/52/1205825205/1205825205-1-16.mp4?e=ig8euxZM2rNcNbRVhwdVhwdlhWdVhwdVhoNvNC8BqJIzNbfq9rVEuxTEnE8L5F6VnEsSTx0vkX8fqJeYTj_lta53NCM=&uipk=5&nbs=1&deadline=1693821903&gen=playurlv2&os=08cbv&oi=1865700872&trid=bfc9c19f85c545dd8f4794ff97f4f57fh&mid=17340771&platform=html5&upsig=9bf98515091bb8a80e1950a03a2a0d68&uparams=e,uipk,nbs,deadline,gen,os,oi,trid,mid,platform&bvc=vod&nettype=0&f=h_0_0&bw=49663&logo=80000000"),
// headers: {
// 'user-agent':
// 'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.4 Safari/605.1.15',
// 'referer': 'https://www.bilibili.com'
// },
// tag: MediaItem(
// id: '${_nextMediaId++}',
// album: "Science Friday",
// title: "A Salute To Head-Scratching Science",
// artUri: Uri.parse(
// "https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
// ),
// ),
AudioSource.uri(
Uri.parse("https://s3.amazonaws.com/scifri-segments/scifri201711241.mp3"),
tag: MediaItem(
id: '${_nextMediaId++}',
album: "Science Friday",
title: "From Cat Rheology To Operatic Incompetence",
artUri: Uri.parse(
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
),
),
AudioSource.uri(
Uri.parse("asset:///audio/nature.mp3"),
tag: MediaItem(
id: '${_nextMediaId++}',
album: "Public Domain",
title: "Nature Sounds",
artUri: Uri.parse(
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg"),
),
),
]);
@override
void initState() {
super.initState();
_player = AudioPlayer();
_init();
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
Future<void> _init() async {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech());
// Listen to errors during playback.
_player.playbackEventStream.listen((event) {},
onError: (Object e, StackTrace stackTrace) {
print('A stream error occurred: $e');
});
try {
await _player.setAudioSource(_playlist);
} catch (e, stackTrace) {
// Catch load errors: 404, invalid url ...
print("Error loading playlist: $e");
print(stackTrace);
}
}
// Stream<PositionData> get _positionDataStream =>
// Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
// _player.positionStream,
// _player.bufferedPositionStream,
// _player.durationStream,
// (position, bufferedPosition, duration) => PositionData(
// position, bufferedPosition, duration ?? Duration.zero));
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: StreamBuilder<SequenceState?>(
stream: _player.sequenceStateStream,
builder: (context, snapshot) {
final state = snapshot.data;
if (state?.sequence.isEmpty ?? true) {
return const SizedBox();
}
final metadata = state!.currentSource!.tag as MediaItem;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Image.network(metadata.artUri.toString())),
),
),
Text(metadata.album!,
style: Theme.of(context).textTheme.titleLarge),
Text(metadata.title),
],
);
},
),
),
ControlButtons(_player),
// StreamBuilder<PositionData>(
// stream: _positionDataStream,
// builder: (context, snapshot) {
// final positionData = snapshot.data;
// return SeekBar(
// duration: positionData?.duration ?? Duration.zero,
// position: positionData?.position ?? Duration.zero,
// bufferedPosition:
// positionData?.bufferedPosition ?? Duration.zero,
// onChangeEnd: (newPosition) {
// _player.seek(newPosition);
// },
// );
// },
// ),
const SizedBox(height: 8.0),
Row(
children: [
StreamBuilder<LoopMode>(
stream: _player.loopModeStream,
builder: (context, snapshot) {
final loopMode = snapshot.data ?? LoopMode.off;
const icons = [
Icon(Icons.repeat, color: Colors.grey),
Icon(Icons.repeat, color: Colors.orange),
Icon(Icons.repeat_one, color: Colors.orange),
];
const cycleModes = [
LoopMode.off,
LoopMode.all,
LoopMode.one,
];
final index = cycleModes.indexOf(loopMode);
return IconButton(
icon: icons[index],
onPressed: () {
_player.setLoopMode(cycleModes[
(cycleModes.indexOf(loopMode) + 1) %
cycleModes.length]);
},
);
},
),
Expanded(
child: Text(
"Playlist",
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
StreamBuilder<bool>(
stream: _player.shuffleModeEnabledStream,
builder: (context, snapshot) {
final shuffleModeEnabled = snapshot.data ?? false;
return IconButton(
icon: shuffleModeEnabled
? const Icon(Icons.shuffle, color: Colors.orange)
: const Icon(Icons.shuffle, color: Colors.grey),
onPressed: () async {
final enable = !shuffleModeEnabled;
if (enable) {
await _player.shuffle();
}
await _player.setShuffleModeEnabled(enable);
},
);
},
),
],
),
SizedBox(
height: 240.0,
child: StreamBuilder<SequenceState?>(
stream: _player.sequenceStateStream,
builder: (context, snapshot) {
final state = snapshot.data;
final sequence = state?.sequence ?? [];
return ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
if (oldIndex < newIndex) newIndex--;
_playlist.move(oldIndex, newIndex);
},
children: [
for (var i = 0; i < sequence.length; i++)
Dismissible(
key: ValueKey(sequence[i]),
background: Container(
color: Colors.redAccent,
alignment: Alignment.centerRight,
child: const Padding(
padding: EdgeInsets.only(right: 8.0),
child: Icon(Icons.delete, color: Colors.white),
),
),
onDismissed: (dismissDirection) {
_playlist.removeAt(i);
},
child: Material(
color: i == state!.currentIndex
? Colors.grey.shade300
: null,
child: ListTile(
title: Text(sequence[i].tag.title as String),
onTap: () {
_player.seek(Duration.zero, index: i);
},
),
),
),
],
);
},
),
),
],
),
),
);
}
}
class ControlButtons extends StatelessWidget {
final AudioPlayer player;
const ControlButtons(this.player, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.volume_up),
onPressed: () {
showSliderDialog(
context: context,
title: "Adjust volume",
divisions: 10,
min: 0.0,
max: 1.0,
stream: player.volumeStream,
onChanged: player.setVolume,
);
},
),
StreamBuilder<SequenceState?>(
stream: player.sequenceStateStream,
builder: (context, snapshot) => IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: player.hasPrevious ? player.seekToPrevious : null,
),
),
StreamBuilder<PlayerState>(
stream: player.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState;
final playing = playerState?.playing;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return Container(
margin: const EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: const CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: const Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: const Icon(Icons.pause),
iconSize: 64.0,
onPressed: player.pause,
);
} else {
return IconButton(
icon: const Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => player.seek(Duration.zero,
index: player.effectiveIndices!.first),
);
}
},
),
StreamBuilder<SequenceState?>(
stream: player.sequenceStateStream,
builder: (context, snapshot) => IconButton(
icon: const Icon(Icons.skip_next),
onPressed: player.hasNext ? player.seekToNext : null,
),
),
StreamBuilder<double>(
stream: player.speedStream,
builder: (context, snapshot) => IconButton(
icon: Text("${snapshot.data?.toStringAsFixed(1)}x",
style: const TextStyle(fontWeight: FontWeight.bold)),
onPressed: () {
showSliderDialog(
context: context,
title: "Adjust speed",
divisions: 10,
min: 0.5,
max: 1.5,
stream: player.speedStream,
onChanged: player.setSpeed,
);
},
),
),
],
);
}
}
void showSliderDialog({
required BuildContext context,
required String title,
required int divisions,
required double min,
required double max,
String valueSuffix = '',
required Stream<double> stream,
required ValueChanged<double> onChanged,
}) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(title, textAlign: TextAlign.center),
content: StreamBuilder<double>(
stream: stream,
builder: (context, snapshot) => SizedBox(
height: 100.0,
child: Column(
children: [
Text('${snapshot.data?.toStringAsFixed(1)}$valueSuffix',
style: const TextStyle(
fontFamily: 'Fixed',
fontWeight: FontWeight.bold,
fontSize: 24.0)),
Slider(
divisions: divisions,
min: min,
max: max,
value: snapshot.data ?? 1.0,
onChanged: onChanged,
),
],
),
),
),
),
);
}
class PositionData {
final Duration position;
final Duration bufferedPosition;
final Duration duration;
PositionData(this.position, this.bufferedPosition, this.duration);
}
class SeekBar extends StatefulWidget {
final Duration duration;
final Duration position;
final Duration bufferedPosition;
final ValueChanged<Duration>? onChanged;
final ValueChanged<Duration>? onChangeEnd;
const SeekBar({
Key? key,
required this.duration,
required this.position,
required this.bufferedPosition,
this.onChanged,
this.onChangeEnd,
}) : super(key: key);
@override
SeekBarState createState() => SeekBarState();
}
class SeekBarState extends State<SeekBar> {
double? _dragValue;
late SliderThemeData _sliderThemeData;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_sliderThemeData = SliderTheme.of(context).copyWith(
trackHeight: 2.0,
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
SliderTheme(
data: _sliderThemeData.copyWith(
thumbShape: HiddenThumbComponentShape(),
activeTrackColor: Colors.blue.shade100,
inactiveTrackColor: Colors.grey.shade300,
),
child: ExcludeSemantics(
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(widget.bufferedPosition.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
),
SliderTheme(
data: _sliderThemeData.copyWith(
inactiveTrackColor: Colors.transparent,
),
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
Positioned(
right: 16.0,
bottom: 0.0,
child: Text(
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
.firstMatch("$_remaining")
?.group(1) ??
'$_remaining',
style: Theme.of(context).textTheme.bodySmall),
),
],
);
}
Duration get _remaining => widget.duration - widget.position;
}
class HiddenThumbComponentShape extends SliderComponentShape {
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {}
}

View File

@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
? int.parse(Get.parameters['seasonId']!)
: null;
var epId = Get.parameters['epId'] != null
? int.parse(Get.parameters['epId']!)
? int.tryParse(Get.parameters['epId']!)
: null;
// 是否预渲染 骨架屏
@ -257,7 +258,8 @@ class BangumiIntroController extends GetxController {
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid;
videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl();
// 重新请求评论
try {
@ -291,4 +293,31 @@ class BangumiIntroController extends GetxController {
}
return result;
}
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
if (bangumiDetail.value.episodes != null) {
episodes = bangumiDetail.value.episodes!;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
int currentIndex =
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
int nextIndex = currentIndex + 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (platRepeat == PlayRepeat.listCycle) {
if (nextIndex == episodes.length - 1) {
nextIndex = 0;
}
}
if (nextIndex <= episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {}
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
}
}

View File

@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
class _BangumiIntroPanelState extends State<BangumiIntroPanel>
with AutomaticKeepAliveClientMixin {
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
late BangumiIntroController bangumiIntroController;
late VideoDetailController videoDetailCtr;
BangumiInfoModel? bangumiDetail;
late Future _futureBuilderFuture;
late int cid;
late String heroTag;
// 添加页面缓存
@override
@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
@override
void initState() {
super.initState();
heroTag = Get.arguments['heroTag'];
cid = widget.cid!;
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value;
});
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
videoDetailCtr.cid.listen((p0) {
print('🐶🐶$p0');
cid = p0;
setState(() {});
});
}
@override
@ -61,22 +72,25 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
return BangumiInfo(
loadingStatus: false,
bangumiDetail: bangumiDetail,
cid: cid,
);
} else {
// 请求错误
return HttpError(
errMsg: snapshot.data['msg'],
fn: () => Get.back(),
);
// return HttpError(
// errMsg: snapshot.data['msg'],
// fn: () => Get.back(),
// );
return SizedBox();
}
} else {
return BangumiInfo(
loadingStatus: true,
bangumiDetail: bangumiDetail,
cid: widget.cid,
cid: cid,
);
}
},
@ -117,6 +131,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!;
print('cid: $cid');
videoDetailCtr.cid.listen((p0) {
cid = p0;
print('cid: $cid');
setState(() {});
});
}
// 收藏
@ -260,9 +280,15 @@ class _BangumiInfoState extends State<BangumiInfo> {
children: [
Text(
!widget.loadingStatus
? widget.bangumiDetail!.areas!
.first['name']
: bangumiItem!.areas!.first['name'],
? (widget.bangumiDetail!.areas!
.isNotEmpty
? widget.bangumiDetail!.areas!
.first['name']
: '')
: (bangumiItem!.areas!.isNotEmpty
? bangumiItem!
.areas!.first['name']
: ''),
style: TextStyle(
fontSize: 12,
color: t.colorScheme.outline,

View File

@ -113,6 +113,9 @@ class _BangumiPageState extends State<BangumiPage>
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
if (snapshot.data == null) {
return const SizedBox();
}
Map data = snapshot.data as Map;
List list = _bangumidController.bangumiFollowList;
if (data['status']) {
@ -198,7 +201,7 @@ class _BangumiPageState extends State<BangumiPage>
},
),
),
const LoadingMore()
LoadingMore()
],
),
);

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart';
class BangumiPanel extends StatefulWidget {
@ -30,16 +32,28 @@ class _BangumiPanelState extends State<BangumiPanel> {
dynamic userInfo;
// 默认未开通
int vipStatus = 0;
late int cid;
String heroTag = Get.arguments['heroTag'];
late final VideoDetailController videoDetailCtr;
@override
void initState() {
super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
cid = widget.cid!;
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex();
userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) {
vipStatus = userInfo.vipStatus;
}
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex();
});
}
@override

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -60,7 +61,7 @@ class _BlackListPageState extends State<BlackListPage> {
centerTitle: false,
title: Obx(
() => Text(
'黑名单管理 ${_blackListController.blackList.length} / 5000',
'黑名单管理 - ${_blackListController.total.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
@ -104,10 +105,11 @@ class _BlackListPageState extends State<BlackListPage> {
overflow: TextOverflow.ellipsis,
),
dense: true,
// trailing: TextButton(
// onPressed: () {},
// child: const Text('移除'),
// ),
trailing: TextButton(
onPressed: () => _blackListController
.removeBlack(list[index].mid),
child: const Text('移除'),
),
);
},
),
@ -136,6 +138,7 @@ class _BlackListPageState extends State<BlackListPage> {
class BlackListController extends GetxController {
int currentPage = 1;
int pageSize = 50;
RxInt total = 0.obs;
RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async {
@ -146,6 +149,7 @@ class BlackListController extends GetxController {
if (result['status']) {
if (type == 'init') {
blackList.value = result['data'].list;
total.value = result['data'].total;
} else {
blackList.addAll(result['data'].list);
}
@ -154,4 +158,13 @@ class BlackListController extends GetxController {
}
return result;
}
Future removeBlack(mid) async {
var result = await BlackHttp.removeBlack(fid: mid);
if (result['status']) {
blackList.removeWhere((e) => e.mid == mid);
total.value = total.value - 1;
SmartDialog.showToast(result['msg']);
}
}
}

View File

@ -0,0 +1,76 @@
import 'package:pilipala/http/danmaku.dart';
import 'package:pilipala/models/danmaku/dm.pb.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
class PlDanmakuController {
PlDanmakuController(this.cid, this.playerController);
final int cid;
final PlPlayerController playerController;
late Duration videoDuration;
// 按 6min 分段
int segCount = 0;
List<DmSegMobileReply> dmSegList = [];
// 已请求的段落标记
List<int> hasrequestSeg = [];
int currentSegIndex = 1;
int currentDmIndex = 0;
void calcSegment() {
dmSegList.clear();
// 视频分段数
segCount = (videoDuration.inSeconds / (60 * 6)).ceil();
dmSegList = List<DmSegMobileReply>.generate(
segCount < 1 ? 1 : segCount, (index) => DmSegMobileReply());
// 当前分段
try {
currentSegIndex =
(playerController.position.value.inSeconds / (60 * 6)).ceil();
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
} catch (_) {}
}
Future<List<DmSegMobileReply>> queryDanmaku() async {
// dmSegList.clear();
DmSegMobileReply result =
await DanmakaHttp.queryDanmaku(cid: cid, segmentIndex: currentSegIndex);
if (result.elems.isNotEmpty) {
result.elems.sort((a, b) => (a.progress).compareTo(b.progress));
// dmSegList.add(result);
currentSegIndex = currentSegIndex < 1 ? 1 : currentSegIndex;
dmSegList[currentSegIndex - 1] = result;
}
if (dmSegList.isNotEmpty) {
findClosestPositionIndex(playerController.position.value.inMilliseconds);
}
return dmSegList;
}
/// 查询当前最接近的弹幕
void findClosestPositionIndex(int position) {
int segIndex = (position / (6 * 60 * 1000)).ceil() - 1;
if (segIndex < 0) segIndex = 0;
List elems = dmSegList[segIndex].elems;
if (segIndex < dmSegList.length) {
int left = 0;
int right = elems.length;
while (left < right) {
int mid = (right + left) ~/ 2;
var midPosition = elems[mid].progress;
if (midPosition >= position) {
right = mid;
} else {
left = mid + 1;
}
}
currentSegIndex = segIndex;
currentDmIndex = right;
} else {
currentSegIndex = segIndex;
currentDmIndex = 0;
}
}
}

View File

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

180
lib/pages/danmaku/view.dart Normal file
View File

@ -0,0 +1,180 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/pages/danmaku/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/danmaku.dart';
import 'package:pilipala/utils/storage.dart';
/// 传入播放器控制器,监听播放进度,加载对应弹幕
class PlDanmaku extends StatefulWidget {
final int cid;
final PlPlayerController playerController;
const PlDanmaku({
super.key,
required this.cid,
required this.playerController,
});
@override
State<PlDanmaku> createState() => _PlDanmakuState();
}
class _PlDanmakuState extends State<PlDanmaku> {
late PlPlayerController playerController;
late PlDanmakuController _plDanmakuController;
DanmakuController? _controller;
bool danmuPlayStatus = true;
Box setting = GStrorage.setting;
late bool enableShowDanmaku;
late List blockTypes;
late double showArea;
late double opacityVal;
late double fontSizeVal;
late double danmakuSpeedVal;
@override
void initState() {
super.initState();
enableShowDanmaku =
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false);
_plDanmakuController =
PlDanmakuController(widget.cid, widget.playerController);
if (mounted) {
playerController = widget.playerController;
_plDanmakuController.videoDuration = playerController.duration.value;
if (enableShowDanmaku || playerController.isOpenDanmu.value) {
_plDanmakuController
..calcSegment()
..queryDanmaku();
}
playerController
..addStatusLister(playerListener)
..addPositionListener(videoPositionListen);
}
playerController.isOpenDanmu.listen((p0) {
if (p0) {
if (_plDanmakuController.dmSegList.isEmpty) {
_plDanmakuController
..calcSegment()
..queryDanmaku();
}
}
});
blockTypes = playerController.blockTypes;
showArea = playerController.showArea;
opacityVal = playerController.opacityVal;
fontSizeVal = playerController.fontSizeVal;
danmakuSpeedVal = playerController.danmakuSpeedVal;
}
// 播放器状态监听
void playerListener(PlayerStatus? status) {
if (status == PlayerStatus.paused) {
_controller!.pause();
}
if (status == PlayerStatus.playing) {
_controller!.onResume();
}
}
void videoPositionListen(Duration position) {
if (!danmuPlayStatus) {
_controller!.onResume();
danmuPlayStatus = true;
}
if (!playerController.isOpenDanmu.value) {
return;
}
PlDanmakuController ctr = _plDanmakuController;
int currentPosition = position.inMilliseconds;
blockTypes = playerController.blockTypes;
// 根据position判断是否有已缓存弹幕。没有则请求对应段
int segIndex = (currentPosition / (6 * 60 * 1000)).ceil();
segIndex = segIndex < 1 ? 1 : segIndex;
if (ctr.dmSegList[segIndex - 1].elems.isEmpty &&
!ctr.hasrequestSeg.contains(segIndex - 1)) {
ctr.hasrequestSeg.add(segIndex - 1);
ctr.currentSegIndex = segIndex;
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
ctr.queryDanmaku();
});
}
// 超出分段数返回
if (ctr.currentSegIndex >= ctr.dmSegList.length) {
return;
}
if (ctr.dmSegList.isEmpty ||
ctr.dmSegList[ctr.currentSegIndex].elems.isEmpty) {
return;
}
// 超出当前分段的弹幕总数返回
if (ctr.currentDmIndex >= ctr.dmSegList[ctr.currentSegIndex].elems.length) {
ctr.currentDmIndex = 0;
ctr.currentSegIndex++;
return;
}
var element = ctr.dmSegList[ctr.currentSegIndex].elems[ctr.currentDmIndex];
var delta = currentPosition - element.progress;
if (delta >= 0 && delta < 200) {
// 屏蔽彩色弹幕
if (blockTypes.contains(6) ? element.color == 16777215 : true) {
_controller!.addItems([
DanmakuItem(
element.content,
color: DmUtils.decimalToColor(element.color),
time: element.progress,
type: DmUtils.getPosition(element.mode),
)
]);
}
ctr.currentDmIndex++;
} else {
if (!playerController.isOpenDanmu.value) {
_controller!.pause();
danmuPlayStatus = false;
return;
}
ctr.findClosestPositionIndex(position.inMilliseconds);
}
}
@override
void dispose() {
playerController.removePositionListener(videoPositionListen);
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, box) {
double initDuration = box.maxWidth / 12;
return Obx(
() => AnimatedOpacity(
opacity: playerController.isOpenDanmu.value ? 1 : 0,
duration: const Duration(milliseconds: 100),
child: DanmakuView(
createdController: (DanmakuController e) async {
widget.playerController.danmakuController = _controller = e;
},
option: DanmakuOption(
fontSize: 15 * fontSizeVal,
area: showArea,
opacity: opacityVal,
hideTop: blockTypes.contains(5),
hideScroll: blockTypes.contains(2),
hideBottom: blockTypes.contains(4),
duration: initDuration /
(danmakuSpeedVal * widget.playerController.playbackSpeed),
),
statusChanged: (isPlaying) {},
),
),
);
});
}
}

View File

@ -149,10 +149,30 @@ class DynamicsController extends GetxController {
case 'DYNAMIC_TYPE_ARTICLE':
String title = item.modules.moduleDynamic.major.opus.title;
String url = item.modules.moduleDynamic.major.opus.jumpUrl;
Get.toNamed(
'/webview',
parameters: {'url': 'https:$url', 'type': 'note', 'pageTitle': title},
);
if (url.contains('opus') || url.contains('read')) {
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(url);
String number = matches.first.group(0)!;
if (url.contains('read')) {
number = 'cv$number';
}
Get.toNamed('/htmlRender', parameters: {
'url': url.startsWith('//') ? url.split('//').last : url,
'title': title,
'id': number,
'dynamicType': url.split('//').last.split('/')[1]
});
} else {
Get.toNamed(
'/webview',
parameters: {
'url': 'https:$url',
'type': 'note',
'pageTitle': title
},
);
}
break;
case 'DYNAMIC_TYPE_PGC':
print('番剧');

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/reply.dart';
@ -17,6 +18,7 @@ class DynamicDetailController extends GetxController {
RxString noMore = ''.obs;
RxList<ReplyItemModel> replyList = [ReplyItemModel()].obs;
RxInt acount = 0.obs;
final ScrollController scrollController = ScrollController();
ReplySortType _sortType = ReplySortType.time;
RxString sortTypeTitle = ReplySortType.time.titles.obs;

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/video_reply.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -9,7 +10,10 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/pages/dynamics/deatil/index.dart';
import 'package:pilipala/pages/dynamics/widgets/author_panel.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/pages/video/detail/replyNew/index.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import '../widgets/dynamic_panel.dart';
@ -21,15 +25,18 @@ class DynamicDetailPage extends StatefulWidget {
State<DynamicDetailPage> createState() => _DynamicDetailPageState();
}
class _DynamicDetailPageState extends State<DynamicDetailPage> {
late DynamicDetailController? _dynamicDetailController;
class _DynamicDetailPageState extends State<DynamicDetailPage>
with TickerProviderStateMixin {
late DynamicDetailController _dynamicDetailController;
late AnimationController fabAnimationCtr;
Future? _futureBuilderFuture;
late StreamController<bool> titleStreamC; // appBar title
final ScrollController scrollController = ScrollController();
late ScrollController scrollController;
bool _visibleTitle = false;
String? action;
// 回复类型
late int type;
bool _isFabVisible = true;
@override
void initState() {
@ -38,39 +45,42 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
// floor 1原创 2转发
if (Get.arguments['floor'] == 1) {
oid = int.parse(Get.arguments['item'].basic!['comment_id_str']);
print(oid);
} else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
try {
String type = Get.arguments['item'].modules.moduleDynamic.major.type;
/// TODO
if (type == 'MAJOR_TYPE_OPUS') {
} else {
oid = Get.arguments['item'].modules.moduleDynamic.major.draw.id;
}
} catch (_) {}
}
int commentType = Get.arguments['item'].basic!['comment_type'] ?? 11;
int commentType = 11;
try {
commentType = Get.arguments['item'].basic!['comment_type'];
} catch (_) {}
type = (commentType == 0) ? 11 : commentType;
action =
Get.arguments.containsKey('action') ? Get.arguments['action'] : null;
_dynamicDetailController = Get.put(DynamicDetailController(oid, type));
_futureBuilderFuture = _dynamicDetailController!.queryReplyList();
_dynamicDetailController =
Get.put(DynamicDetailController(oid, type), tag: oid.toString());
_futureBuilderFuture = _dynamicDetailController.queryReplyList();
titleStreamC = StreamController<bool>();
scrollController.addListener(_listen);
if (action == 'comment') {
_visibleTitle = true;
titleStreamC.add(true);
}
}
void _listen() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController!.queryReplyList(reqType: 'onLoad');
});
}
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
fabAnimationCtr = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
fabAnimationCtr.forward();
// 滚动事件监听
scrollListener();
}
void replyReply(replyItem) {
@ -97,9 +107,58 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
);
}
void scrollListener() {
scrollController = _dynamicDetailController.scrollController;
scrollController.addListener(
() {
// 分页加载
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 300) {
EasyThrottle.throttle('replylist', const Duration(seconds: 2), () {
_dynamicDetailController.queryReplyList(reqType: 'onLoad');
});
}
// 标题
if (scrollController.offset > 55 && !_visibleTitle) {
_visibleTitle = true;
titleStreamC.add(true);
} else if (scrollController.offset <= 55 && _visibleTitle) {
_visibleTitle = false;
titleStreamC.add(false);
}
// fab按钮
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
_showFab();
} else if (direction == ScrollDirection.reverse) {
_hideFab();
}
},
);
}
void _showFab() {
if (!_isFabVisible) {
_isFabVisible = true;
fabAnimationCtr.forward();
}
}
void _hideFab() {
if (_isFabVisible) {
_isFabVisible = false;
fabAnimationCtr.reverse();
}
}
@override
void dispose() {
scrollController.removeListener(() {});
fabAnimationCtr.dispose();
scrollController.dispose();
super.dispose();
}
@ -118,7 +177,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: author(_dynamicDetailController!.item, context),
child: AuthorPanel(item: _dynamicDetailController.item),
);
},
),
@ -126,155 +185,206 @@ class _DynamicDetailPageState extends State<DynamicDetailPage> {
),
body: RefreshIndicator(
onRefresh: () async {
await _dynamicDetailController!.queryReplyList();
await _dynamicDetailController.queryReplyList();
},
child: CustomScrollView(
controller: scrollController,
slivers: [
if (action != 'comment')
SliverToBoxAdapter(
child: DynamicPanel(
item: _dynamicDetailController!.item,
source: 'detail',
),
),
SliverPersistentHeader(
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
width: 0.6,
color: Theme.of(context).dividerColor.withOpacity(0.05),
),
child: Stack(
children: [
CustomScrollView(
controller: scrollController,
slivers: [
if (action != 'comment')
SliverToBoxAdapter(
child: DynamicPanel(
item: _dynamicDetailController.item,
source: 'detail',
),
),
height: 45,
padding: const EdgeInsets.only(left: 12, right: 6),
child: Row(
children: [
Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'${_dynamicDetailController!.acount.value}',
key: ValueKey<int>(
_dynamicDetailController!.acount.value),
SliverPersistentHeader(
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
width: 0.6,
color: Theme.of(context)
.dividerColor
.withOpacity(0.05),
),
),
),
const Text('条回复'),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () =>
_dynamicDetailController!.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_dynamicDetailController!.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
),
)
],
),
),
),
pinned: true,
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => _dynamicDetailController!.replyList.isEmpty &&
_dynamicDetailController!.isLoadingMore
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index ==
_dynamicDetailController!
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
_dynamicDetailController!
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _dynamicDetailController!
.replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController!
.replyList[index].replies!
.add(replyItem);
},
);
}
},
childCount:
_dynamicDetailController!.replyList.length +
1,
height: 45,
padding: const EdgeInsets.only(left: 12, right: 6),
child: Row(
children: [
Obx(
() => AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation, child: child);
},
child: Text(
'${_dynamicDetailController.acount.value}',
key: ValueKey<int>(
_dynamicDetailController.acount.value),
),
),
),
const Text('条回复'),
const Spacer(),
SizedBox(
height: 35,
child: TextButton.icon(
onPressed: () =>
_dynamicDetailController.queryBySort(),
icon: const Icon(Icons.sort, size: 16),
label: Obx(() => Text(
_dynamicDetailController
.sortTypeLabel.value,
style: const TextStyle(fontSize: 13),
)),
),
)
],
),
),
),
pinned: true,
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (snapshot.data['status']) {
// 请求成功
return Obx(
() => _dynamicDetailController.replyList.isEmpty &&
_dynamicDetailController.isLoadingMore
? SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index ==
_dynamicDetailController
.replyList.length) {
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context)
.padding
.bottom),
height: MediaQuery.of(context)
.padding
.bottom +
100,
child: Center(
child: Obx(
() => Text(
_dynamicDetailController
.noMore.value,
style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
);
} else {
return ReplyItem(
replyItem: _dynamicDetailController
.replyList[index],
showReplyRow: true,
replyLevel: '1',
replyReply: (replyItem) =>
replyReply(replyItem),
replyType: ReplyType.values[type],
addReply: (replyItem) {
_dynamicDetailController
.replyList[index].replies!
.add(replyItem);
},
);
}
},
childCount: _dynamicDetailController
.replyList.length +
1,
),
),
);
} else {
// 请求错误
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
],
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 2),
end: const Offset(0, 0),
).animate(CurvedAnimation(
parent: fabAnimationCtr,
curve: Curves.easeInOut,
)),
child: FloatingActionButton(
heroTag: null,
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (BuildContext context) {
return VideoReplyNewDialog(
oid: _dynamicDetailController.oid ??
IdUtils.bv2av(Get.parameters['bvid']!),
root: 0,
parent: 0,
replyType: ReplyType.values[type],
);
},
).then(
(value) => {
// 完成评论,数据添加
if (value != null && value['data'] != null)
{
_dynamicDetailController.replyList
.add(value['data']),
_dynamicDetailController.acount.value++
}
},
);
} else {
// 请求错误
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoReplySkeleton();
}, childCount: 8),
);
}
},
)
},
tooltip: '评论动态',
child: const Icon(Icons.reply),
),
),
),
],
),
),

View File

@ -11,7 +11,6 @@ import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
@ -32,7 +31,6 @@ class _DynamicsPageState extends State<DynamicsPage>
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
Box userInfoCache = GStrorage.userInfo;
EventBus eventBus = EventBus();
late ScrollController scrollController;
@override
@ -66,12 +64,13 @@ class _DynamicsPageState extends State<DynamicsPage>
},
);
eventBus.on(EventName.loginEvent, (args) {
_dynamicsController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
});
_dynamicsController.userLogin.listen((status) {
if (mounted) {
setState(() {
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
});
}
});
}
@ -213,6 +212,9 @@ class _DynamicsPageState extends State<DynamicsPage>
future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value));
@ -233,6 +235,9 @@ class _DynamicsPageState extends State<DynamicsPage>
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data == null) {
return const SliverToBoxAdapter(child: SizedBox());
}
Map data = snapshot.data;
if (data['status']) {
List<DynamicItemModel> list =

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
/// TODO 点击跳转
Widget addWidget(item, context, type, {floor = 1}) {
@ -19,8 +22,27 @@ Widget addWidget(item, context, type, {floor = 1}) {
: Theme.of(context).colorScheme.background;
switch (type) {
case 'ADDITIONAL_TYPE_UGC':
// 转发的投稿
return InkWell(
onTap: () {},
onTap: () async {
String text = dynamicProperty[type].jumpUrl;
RegExp bvRegex = RegExp(r'BV[0-9A-Za-z]{10}', caseSensitive: false);
Iterable<Match> matches = bvRegex.allMatches(text);
if (matches.isNotEmpty) {
Match match = matches.first;
String bvid = match.group(0)!;
String cover = dynamicProperty[type].cover;
try {
int cid = await SearchHttp.ab2c(bvid: bvid);
Get.toNamed('/video?bvid=$bvid&cid=$cid',
arguments: {'pic': cover, 'heroTag': bvid});
} catch (err) {
SmartDialog.showToast(err.toString());
}
} else {
print("No match found.");
}
},
child: Container(
padding:
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
@ -61,101 +83,111 @@ Widget addWidget(item, context, type, {floor = 1}) {
);
case 'ADDITIONAL_TYPE_RESERVE':
return dynamicProperty[type].state != -1
? Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 12, top: 10, right: 12, bottom: 10),
color: bgColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dynamicProperty[type].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
? dynamicProperty[type].title != null
? Padding(
padding: const EdgeInsets.only(top: 8),
child: InkWell(
onTap: () {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.only(
left: 12, top: 10, right: 12, bottom: 10),
color: bgColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
dynamicProperty[type].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
children: [
if (dynamicProperty[type].desc1 != null)
TextSpan(
text:
dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
if (dynamicProperty[type].desc2 != null)
TextSpan(
text:
dynamicProperty[type].desc2['text']),
],
),
)
],
),
const SizedBox(height: 1),
Text.rich(
TextSpan(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
children: [
TextSpan(text: dynamicProperty[type].desc1['text']),
const TextSpan(text: ' '),
TextSpan(text: dynamicProperty[type].desc2['text']),
],
),
)
],
),
// TextButton(onPressed: () {}, child: Text('123'))
),
),
)
: const SizedBox();
case 'ADDITIONAL_TYPE_GOODS':
return Padding(
padding: const EdgeInsets.only(top: 6),
child: InkWell(
onTap: () {},
child: Container(
padding:
const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
decoration: BoxDecoration(
color: bgColor,
borderRadius: const BorderRadius.all(Radius.circular(6)),
),
child: Row(
children: [
NetworkImgLayer(
width: 75,
height: 75,
src: dynamicProperty[type].items.first.cover,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
dynamicProperty[type].items.first.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
dynamicProperty[type].items.first.brief,
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize,
),
),
const SizedBox(height: 2),
Text(
dynamicProperty[type].items.first.price,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
),
],
// TextButton(onPressed: () {}, child: Text('123'))
),
),
],
),
),
));
)
: const SizedBox()
: const SizedBox();
case 'ADDITIONAL_TYPE_GOODS':
// 商品
return const SizedBox();
// return Padding(
// padding: const EdgeInsets.only(top: 6),
// child: InkWell(
// onTap: () {},
// child: Container(
// padding:
// const EdgeInsets.only(left: 12, top: 8, right: 12, bottom: 8),
// decoration: BoxDecoration(
// color: bgColor,
// borderRadius: const BorderRadius.all(Radius.circular(6)),
// ),
// child: Row(
// children: [
// NetworkImgLayer(
// width: 75,
// height: 75,
// src: dynamicProperty[type].items.first.cover,
// ),
// const SizedBox(width: 10),
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.start,
// children: [
// Text(
// dynamicProperty[type].items.first.name,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
// ),
// Text(
// dynamicProperty[type].items.first.brief,
// maxLines: 1,
// style: TextStyle(
// color: Theme.of(context).colorScheme.outline,
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize,
// ),
// ),
// const SizedBox(height: 2),
// Text(
// dynamicProperty[type].items.first.price,
// style: TextStyle(
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// ),);
case 'ADDITIONAL_TYPE_MATCH':
return const SizedBox();
case 'ADDITIONAL_TYPE_COMMON':

View File

@ -42,13 +42,17 @@ Widget articlePanel(item, context, {floor = 1}) {
.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 2),
if (item.modules.moduleDynamic.major.opus.summary.text != 'undefined')
if (item.modules.moduleDynamic.major.opus.summary.text !=
'undefined') ...[
Text(
item.modules.moduleDynamic.major.opus.summary.richTextNodes.first
.text,
maxLines: 4,
style: const TextStyle(height: 1.55),
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
],
picWidget(item, context)
],
),

View File

@ -1,65 +1,163 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart';
Widget author(item, context) {
String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
return Row(
children: [
GestureDetector(
onTap: () {
feedBack();
Get.toNamed(
'/member?mid=${item.modules.moduleAuthor.mid}',
arguments: {
'face': item.modules.moduleAuthor.face,
'heroTag': heroTag
},
);
},
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: item.modules.moduleAuthor.face,
class AuthorPanel extends StatelessWidget {
final dynamic item;
const AuthorPanel({super.key, required this.item});
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
return Row(
children: [
GestureDetector(
onTap: () {
// 番剧
if (item.modules.moduleAuthor.type == 'AUTHOR_TYPE_PGC') {
return;
}
feedBack();
Get.toNamed(
'/member?mid=${item.modules.moduleAuthor.mid}',
arguments: {
'face': item.modules.moduleAuthor.face,
'heroTag': heroTag
},
);
},
child: Hero(
tag: heroTag,
child: NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: item.modules.moduleAuthor.face,
),
),
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.modules.moduleAuthor.name,
style: TextStyle(
color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.modules.moduleAuthor.name,
style: TextStyle(
color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
),
),
DefaultTextStyle.merge(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
),
child: Row(
children: [
Text(item.modules.moduleAuthor.pubTime),
if (item.modules.moduleAuthor.pubTime != '' &&
item.modules.moduleAuthor.pubAction != '')
const Text(' '),
Text(item.modules.moduleAuthor.pubAction),
],
),
)
],
),
const Spacer(),
if (item.type == 'DYNAMIC_TYPE_AV')
SizedBox(
width: 32,
height: 32,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: item);
},
);
},
icon: const Icon(Icons.more_vert_outlined, size: 18),
),
),
DefaultTextStyle.merge(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
],
);
}
}
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
child: Row(
children: [
Text(item.modules.moduleAuthor.pubTime),
if (item.modules.moduleAuthor.pubTime != '' &&
item.modules.moduleAuthor.pubAction != '')
const Text(' '),
Text(item.modules.moduleAuthor.pubAction),
],
),
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleSmall,
),
)
),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
],
),
],
);
);
}
}

View File

@ -1,40 +1,183 @@
// 内容
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'rich_node_panel.dart';
Widget content(item, context, source) {
TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary);
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.modules.moduleDynamic.topic != null) ...[
GestureDetector(
child: Text(
'#${item.modules.moduleDynamic.topic.name}',
style: authorStyle,
),
),
],
IgnorePointer(
// 禁用SelectableRegion的触摸交互功能
ignoring: source == 'detail' ? false : true,
child: SelectableRegion(
magnifierConfiguration: const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text.rich(
richNode(item, context),
maxLines: source == 'detail' ? 999 : 3,
overflow: TextOverflow.ellipsis,
),
// ignore: must_be_immutable
class Content extends StatefulWidget {
dynamic item;
String? source;
Content({
super.key,
this.item,
this.source,
});
@override
State<Content> createState() => _ContentState();
}
class _ContentState extends State<Content> {
late bool hasPics;
List<OpusPicsModel> pics = [];
@override
void initState() {
super.initState();
hasPics = widget.item.modules.moduleDynamic.major != null &&
widget.item.modules.moduleDynamic.major.opus != null &&
widget.item.modules.moduleDynamic.major.opus.pics.isNotEmpty;
if (hasPics) {
pics = widget.item.modules.moduleDynamic.major.opus.pics;
}
}
InlineSpan picsNodes() {
List<InlineSpan> spanChilds = [];
int len = pics.length;
List<String> picList = [];
if (len == 1) {
OpusPicsModel pictureItem = pics.first;
picList.add(pictureItem.url!);
spanChilds.add(const TextSpan(text: '\n'));
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: 0, imgList: picList);
},
);
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: NetworkImgLayer(
src: pictureItem.url,
width: box.maxWidth / 2,
height: box.maxWidth *
0.5 *
(pictureItem.height != null && pictureItem.width != null
? pictureItem.height! / pictureItem.width!
: 1),
),
),
);
},
),
),
],
),
);
);
}
if (len > 1) {
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pics[i].url!);
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
child: NetworkImgLayer(
src: pics[i].url,
width: box.maxWidth,
height: box.maxWidth,
),
);
},
),
);
}
spanChilds.add(
WidgetSpan(
child: LayoutBuilder(
builder: (context, BoxConstraints box) {
double maxWidth = box.maxWidth;
double crossCount = len < 3 ? 2 : 3;
double height = maxWidth /
crossCount *
(len % crossCount == 0
? len ~/ crossCount
: len ~/ crossCount + 1) +
6;
return Container(
padding: const EdgeInsets.only(top: 6),
height: height,
child: GridView.count(
padding: EdgeInsets.zero,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: crossCount.toInt(),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
childAspectRatio: 1,
children: list,
),
);
},
),
),
);
}
return TextSpan(
children: spanChilds,
);
}
@override
Widget build(BuildContext context) {
TextStyle authorStyle =
TextStyle(color: Theme.of(context).colorScheme.primary);
return Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(12, 0, 12, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.item.modules.moduleDynamic.topic != null) ...[
GestureDetector(
child: Text(
'#${widget.item.modules.moduleDynamic.topic.name}',
style: authorStyle,
),
),
],
IgnorePointer(
// 禁用SelectableRegion的触摸交互功能
ignoring: widget.source == 'detail' ? false : true,
child: SelectableRegion(
magnifierConfiguration: const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text.rich(
/// fix 默认20px高度
style: const TextStyle(height: 0),
richNode(widget.item, context),
maxLines: widget.source == 'detail' ? 999 : 3,
overflow: TextOverflow.ellipsis,
),
),
),
if (hasPics) ...[
Text.rich(picsNodes()),
]
],
),
);
}
}

View File

@ -39,10 +39,11 @@ class DynamicPanel extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: author(item, context),
child: AuthorPanel(item: item),
),
if (item!.modules!.moduleDynamic!.desc != null)
content(item, context, source),
if (item!.modules!.moduleDynamic!.desc != null ||
item!.modules!.moduleDynamic!.major != null)
Content(item: item, source: source),
forWard(item, context, _dynamicsController, source),
const SizedBox(height: 2),
if (source == null) ActionPanel(item: item),

View File

@ -44,19 +44,21 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
],
),
const SizedBox(height: 2),
if (item.modules.moduleDynamic.topic != null) ...[
Padding(
padding: floor == 2
? EdgeInsets.zero
: const EdgeInsets.only(left: 12, right: 12),
child: GestureDetector(
child: Text(
'#${item.modules.moduleDynamic.topic.name}',
style: authorStyle,
),
),
),
],
/// fix #话题跟content重复
// if (item.modules.moduleDynamic.topic != null) ...[
// Padding(
// padding: floor == 2
// ? EdgeInsets.zero
// : const EdgeInsets.only(left: 12, right: 12),
// child: GestureDetector(
// child: Text(
// '#${item.modules.moduleDynamic.topic.name}',
// style: authorStyle,
// ),
// ),
// ),
// ],
Text.rich(
richNode(item, context),
// 被转发状态(floor=2) 隐藏
@ -71,6 +73,8 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
: const EdgeInsets.only(left: 12, right: 12),
child: picWidget(item, context),
),
/// 附加内容 商品信息、直播预约等等
if (item.modules.moduleDynamic.additional != null)
addWidget(
item,
@ -133,7 +137,12 @@ Widget forWard(item, context, ctr, source, {floor = 1}) {
],
),
const SizedBox(height: 8),
Text(item.modules.moduleDynamic.desc.text)
Text.rich(
richNode(item, context),
// 被转发状态(floor=2) 隐藏
maxLines: source == 'detail' && floor != 2 ? 999 : 4,
overflow: TextOverflow.ellipsis,
),
],
)
: item.modules.moduleDynamic.additional != null

View File

@ -1,20 +1,22 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/preview/index.dart';
Widget picWidget(item, context) {
String type = item.modules.moduleDynamic.major.type;
List pictures = [];
if (type == 'MAJOR_TYPE_OPUS') {
pictures = item.modules.moduleDynamic.major.opus.pics;
/// fix 图片跟rich_node_panel重复
// pictures = item.modules.moduleDynamic.major.opus.pics;
return const SizedBox();
}
if (type == 'MAJOR_TYPE_DRAW') {
pictures = item.modules.moduleDynamic.major.draw.items;
}
int len = pictures.length;
List picList = [];
List<String> picList = [];
List<Widget> list = [];
for (var i = 0; i < len; i++) {
picList.add(pictures[i].src ?? pictures[i].url);
@ -23,11 +25,14 @@ Widget picWidget(item, context) {
builder: (context, BoxConstraints box) {
return GestureDetector(
onTap: () {
Get.toNamed('/preview',
arguments: {'initialPage': i, 'imgList': picList});
showDialog(
useSafeArea: false,
context: context,
builder: (context) {
return ImagePreview(initialPage: i, imgList: picList);
},
);
},
// child: Hero(
// tag: pictures[i].src ?? pictures[i].url,
child: NetworkImgLayer(
src: pictures[i].src ?? pictures[i].url,
width: box.maxWidth,
@ -44,27 +49,25 @@ Widget picWidget(item, context) {
double maxWidth = box.maxWidth;
double aspectRatio = 1.0;
double origAspectRatio = 0.0;
double crossCount = len == 1
? 1
: len < 3
? 2
: 3;
double crossCount = 3;
double height = 0.0;
if (len == 1) {
origAspectRatio =
aspectRatio = pictures.first.width / pictures.first.height;
try {
origAspectRatio =
aspectRatio = pictures.first.width / pictures.first.height;
} catch (_) {}
if (aspectRatio < 0.4) {
aspectRatio = 0.4;
}
height = pictures.first.height * maxWidth / pictures.first.width;
if (origAspectRatio < 0.5 || pictures.first.width < 1920) {
crossCount = 2;
height = maxWidth / 2 / aspectRatio;
}
} else {
aspectRatio = 1;
height = maxWidth / crossCount * ((len / crossCount).ceil()) + 6;
height =
maxWidth / crossCount * ((len + crossCount - 1) ~/ crossCount) + 6;
}
return Container(
padding: const EdgeInsets.only(top: 4),

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