Merge branch 'preview-features' into preview

This commit is contained in:
the1812 2023-01-01 21:53:10 +08:00
commit 3397e56dc0
91 changed files with 1736 additions and 834 deletions

View File

@ -50,7 +50,7 @@ body:
attributes:
label: 播放器版本
description: |
在 b 站播放器上右键可以看到播放器版本号, 大于 3 的别反馈视频相关功能了, 去 [#3187](https://github.com/the1812/Bilibili-Evolved/issues/3187) 里有. 如果是 2, 还要确认下是不是新版播放页, 需要返回旧版才能使用脚本功能. (辨认方法可参考 [#3384](https://github.com/the1812/Bilibili-Evolved/discussions/3384))
在 b 站播放器上右键可以看到播放器版本号
placeholder: 请输入
validations:
required: true
@ -71,8 +71,8 @@ body:
attributes:
label: 错误信息
description: |
脚本直接报告的错误信息, 或者浏览器开发者工具 (F12 或 Ctrl+Shift+I 召唤) 里 Console / 控制台 一栏的输出, 在过滤里填入 `bilibili evolved`, 剩下的信息就可以粘贴过来, 太长的话可以截图放下面. <br><img width="400" src="https://user-images.githubusercontent.com/26504152/173189611-f1e9e46a-a31d-4d21-a95a-5771e16372dd.png">
render: shell
脚本直接报告的错误信息, 或者浏览器开发者工具 (F12 或 Ctrl+Shift+I 召唤) 里 Console / 控制台 一栏的输出, 太长的话可以截图放下面. <br><img width="400" src="https://user-images.githubusercontent.com/26504152/173189611-f1e9e46a-a31d-4d21-a95a-5771e16372dd.png">
render: Shell
placeholder: 请输入
validations:
required: true

View File

@ -1,6 +1,47 @@
# 更新日志
## v2.6.0-preview
`2023-01-01`
包含 v2.6.0 的所有更新内容.
✨新增
- 新增功能 `批量导入合集`. (PR #3884 by [swhoro](https://github.com/swhoro))
> 在合集页面制作一个批量导入按钮,可以新增所有合集内视频至同名收藏夹
- `扩展倍速` 添加 `隐藏移除图标``隐藏新增图标` 选项. (PR #3864 by [Tinhone](https://github.com/Tinhone))
- 新增插件 `快捷键扩展 - 开关 CC 字幕`, 默认为 <kbd>Shift</kbd> + <kbd>C</kbd>. (#3407)
🐛修复
- 修复新版播放页下 `简化评论区` 的样式. (#3872)
- `StreamSaver` 下载补充三方 cookie 说明. (#3824)
☕开发者相关
- 完成 define API + SwitchOptions API 的改造 (受影响组件为 `简化首页``简化直播间`), 感谢 [timongh](https://github.com/timongh) 的长期贡献.
- 重新拆分了一下 PlayerAgent 的文件.
## v2.6.0
`2023-01-01`
✨新增
- `快速收起评论区` 添加了平滑滚动效果. (PR #3857 by [Mike Lei](https://github.com/mikelei8291))
- `删除广告` 新增对搜索页广告的的屏蔽. (#3863)
- `显示视频投稿时间` 适配新版播放页. (#3860, PR #3880 by [wisokey](https://github.com/wisokey))
- `网址参数清理` 新增 `buvid`, `mid`, `up_id` 的清理. (#3896)
- `选集区域优化` 支持折叠合集下的分组. (#3764)
🐛修复
- 重新调整了 `自定义顶栏` 的历史弹窗过滤器, 改为只能选择 `全部` 或者单一类型 (#2729), 解决专栏加载不出来的问题 (#3676).
- 修复 `下载字幕` 在 3.x 播放器检测不到语言选项的问题. (#3873)
- 修复旧版播放页中高能进度条未跟随主题颜色. (#3830)
- 修复 `选集区域优化` 临时收起时, 多行标题出现布局问题. (#3841)
- 修复 `禁用特殊弹幕样式` 失效. (#3886)
- 修复 `删除视频弹窗` 失效. (#3853)
- 修复 `自定义顶栏` 开启后原版顶栏高度异常. (#3833) (应该还需要再优化下, 现在还会有点小空隙, 不过没之前那么离谱了)
- `隐藏视频标题层` 补充 `视频` 标签. (#3839)
## v2.5.2-preview
`2022-11-19`
包含 v2.5.2 的所有更新内容.

View File

@ -30,7 +30,7 @@ pnpm install
// @description Bilibili Evolved (本地)
// @version 300.0
// @author Grant Howard, Coulomb-G
// @copyright 2022, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)
// @copyright 2023, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)
// @license MIT
// @match *://*.bilibili.com/*
// @exclude *://*.bilibili.com/*/mobile.html

21
LICENCE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Grant Howard
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
LICENCE.md Normal file
View File

@ -0,0 +1 @@
见 [开源许可](./README.md#开源许可).

View File

@ -29,8 +29,10 @@
**注意事项**
- **⚠ 使用正式版 (GitHub 源) 和预览版须翻墙.**
- 默认不带任何具体功能, 需要在按[设置](#设置)中的说明添加感兴趣的功能.
- 可能不能使用弹幕点赞和举报
- 全景视频不能用鼠标拖拽视角(只能用键盘操作)
- 这个是一个综合性的脚本, 如果只是想下载视频建议去 [GreasyFork](https://greasyfork.org/zh-CN/scripts/by-site/bilibili.com) 找个更专业的.
- 可能不能使用弹幕点赞和举报.
- 全景视频不能用鼠标拖拽视角(只能用键盘操作).
- 默认屏蔽双击全屏, 如有需要可以安装 `双击全屏` 组件来恢复.
- 对性能有较大影响.
- 默认不对未登录的状态做适配.
- 新版本一旦正式发布, 就不再对旧版本做任何技术支持.
@ -41,6 +43,8 @@
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [安装](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master-cdn/dist/bilibili-evolved.user.js) | [安装](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/dist/bilibili-evolved.user.js) | [安装](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/dist/bilibili-evolved.preview.user.js) |
> 如果处于某种原因只能停在旧版, 可以参考[旧版本回退指南](./doc/rollback.md).
# 设置
脚本启用后, 在网页左侧中央会有功能面板和设置面板的入口. 功能面板中包含适用于当前页面的一些功能入口, 设置面板中可以管理组件的开启/关闭, 修改组件选项, 以及安装/卸载组件和插件等.
@ -115,6 +119,16 @@
但是, 任何组件/插件都对用户数据有着完全的访问能力, 对于其他来源(非本仓库提供)的组件/插件, 请自行甄别其安全性.
# 开源许可
源代码基于 [MIT 许可](https://zh.wikipedia.org/zh-cn/MIT%E8%A8%B1%E5%8F%AF%E8%AD%89) 公开.
但存在一些限制:
- 如果需要在其他地方发布此脚本<sup>1</sup>, 则必须满足以下条件之一:
- 保留此 README 文档, 且安装入口只存在于此文档的 ["安装"](#安装) 章节中, 不得额外提供跳过 README 文档阅读即可安装的方式.
- 自行处理相关联的技术支持, 不得直接导向本仓库. 也就是必须 Fork 并修改所有指向本仓库的反馈渠道, 包括 Issues 和 Discussions. 本仓库不对来自其他来源安装的脚本提供技术支持.
> <sup>1</sup> 指发布脚本的完整内容, 例如直接将安装链接 (以 `.user.js` 结尾) 发布, 或者在 GreasyFork 等脚本托管站点导入. 如果只是指向本仓库的页面链接 (例如 `https://github.com/the1812/Bilibili-Evolved`) 是没关系的.
# 第三方开源组件
👍感谢这些组件帮助我们极大地提升了开发效率.

View File

@ -28,6 +28,15 @@ https://afdian.net/@the1812?tab=sponsor
| 时间 | 用户名 | 单号后4位 | 金额 |
| ------------------- | ---------------- | --------- | ------- |
| 2022.12.31 16:59:32 | *梦 | 6802 | ¥5.00 |
| 2022.12.26 22:36:34 | b*8 | 3849 | ¥2.50 |
| 2022.12.16 12:27:57 | *訪 | 6143 | ¥3.79 |
| 2022.12.16 11:10:46 | U* | 8902 | ¥1.00 |
| 2022.12.13 21:43:21 | Y*l | 3706 | ¥5.00 |
| 2022.12.12 22:44:19 | 匿名 | 8546 | ¥10.00 |
| 2022.12.05 15:52:54 | 匿名 | 9553 | ¥5.00 |
| 2022.11.27 15:27:00 | gary8177 | 0627 | ¥10.00 |
| 2022.11.26 12:47:00 | 匿名 | 3570 | ¥10.00 |
| 2022.11.18 18:53:22 | 匿名 | 4381 | ¥3.00 |
| 2022.11.15 17:47:37 | *歌 | 4853 | ¥1.00 |
| 2022.11.08 09:13:48 | 匿名 | 7879 | ¥5.00 |

View File

@ -447,6 +447,14 @@
"fullRelativePath": "../../registry/dist/components/utils/image-resolution.js",
"fullAbsolutePath": "registry/dist/components/utils/image-resolution.js"
},
{
"type": "component",
"name": "importSeries",
"displayName": "批量导入合集",
"description": "by [@aiden](https://github.com/swhoro)\n\n在合集页面制作一个批量导入按钮可以新增所有合集内视频至同名收藏夹",
"fullRelativePath": "../../registry/dist/components/utils/import-series.js",
"fullAbsolutePath": "registry/dist/components/utils/import-series.js"
},
{
"type": "component",
"name": "keymap",
@ -627,7 +635,7 @@
"type": "component",
"name": "extendVideoSpeed",
"displayName": "扩展倍速",
"description": "by [@JLoeve](https://github.com/LonelySteve)\n\n\n\n> 扩展视频播放器的倍速菜单项,可用于突破原有播放倍数的上限或下限.\n\n### 🔧 **选项**\n\n- `隐藏滚动条`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.\n\n### **新增倍速**\n\n开启组件后在默认情况下播放器的倍速菜单就会新增 2.5x 和 3.0x 两个倍速选项.\n\n如果需要添加更多倍速只需将鼠标指针移到菜单顶部的新增图标上图标将变成一个输入框根据需要键入新的倍速值或通过滚轮增减数值或直接使用推荐的数值回车确认即可.\n\n新增倍速的范围要求在 0.0625 到 16 之间,数量则不受限制.\n\n**不推荐设置超高倍速(>3.0x**:原生播放器内部没有针对超高倍速进行优化,可能导致音画不同步、播放卡顿、声音嘈杂/消失等一系列问题.\n\n### **删除倍速**\n\n将鼠标指针移到**自定义**的倍速菜单项上,其右侧将会显示一个移除图标,单击即可删除相应的倍速.\n\n",
"description": "by [@JLoeve](https://github.com/LonelySteve)\n\n\n\n> 扩展视频播放器的倍速菜单项,可用于突破原有播放倍数的上限或下限.\n\n### 🔧 **选项**\n\n- `隐藏滚动条`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.\n\n- `隐藏移除图标`:如果认为倍速右侧的移除倍速图标有些突兀,可以开启此选项隐藏.\n\n- `隐藏新增图标`:如果认为顶部的新增倍速图标有些突兀,可以开启此选项隐藏.\n\n### **新增倍速**\n\n开启组件后在默认情况下播放器的倍速菜单就会新增 2.5x 和 3.0x 两个倍速选项.\n\n如果需要添加更多倍速只需将鼠标指针移到菜单顶部的新增图标上图标将变成一个输入框根据需要键入新的倍速值或通过滚轮增减数值或直接使用推荐的数值回车确认即可.\n\n新增倍速的范围要求在 0.0625 到 16 之间,数量则不受限制.\n\n**不推荐设置超高倍速(>3.0x**:原生播放器内部没有针对超高倍速进行优化,可能导致音画不同步、播放卡顿、声音嘈杂/消失等一系列问题.\n\n### **删除倍速**\n\n将鼠标指针移到**自定义**的倍速菜单项上,其右侧将会显示一个移除图标,单击即可删除相应的倍速.\n\n",
"fullRelativePath": "../../registry/dist/components/video/player/extend-speed.js",
"fullAbsolutePath": "registry/dist/components/video/player/extend-speed.js"
},
@ -675,7 +683,7 @@
"type": "component",
"name": "removePlayerPopup",
"displayName": "删除视频弹窗",
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.",
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.\r\n",
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
"fullAbsolutePath": "registry/dist/components/video/player/remove-popup.js"
},
@ -808,6 +816,14 @@
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-danmaku-list.js",
"fullAbsolutePath": "registry/dist/plugins/utils/keymap-toggle-danmaku-list.js"
},
{
"type": "plugin",
"name": "keymap.actions.toggleSubtitle",
"displayName": "快捷键扩展 - 开关 CC 字幕",
"description": "在快捷键的动作列表里添加一个 \"开关 CC 字幕\".",
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-subtitle.js",
"fullAbsolutePath": "registry/dist/plugins/utils/keymap-toggle-subtitle.js"
},
{
"type": "plugin",
"name": "vLoading.reimu",

View File

@ -565,6 +565,17 @@ by [@snowraincloud](https://github.com/snowraincloud)
根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)
### [批量导入合集](../../registry/dist/components/utils/import-series.js)
`importSeries`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/import-series.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/utils/import-series.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/import-series.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/import-series.js)
by [@aiden](https://github.com/swhoro)
在合集页面制作一个批量导入按钮,可以新增所有合集内视频至同名收藏夹
### [快捷键扩展](../../registry/dist/components/utils/keymap.js)
`keymap`
@ -809,6 +820,10 @@ by [@JLoeve](https://github.com/LonelySteve)
- `隐藏滚动条`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.
- `隐藏移除图标`:如果认为倍速右侧的移除倍速图标有些突兀,可以开启此选项隐藏.
- `隐藏新增图标`:如果认为顶部的新增倍速图标有些突兀,可以开启此选项隐藏.
### **新增倍速**
开启组件后,在默认情况下,播放器的倍速菜单就会新增 2.5x 和 3.0x 两个倍速选项.
@ -1054,6 +1069,15 @@ by FoundTheWOUT
在快捷键的动作列表里添加一个 "开关弹幕列表".
### [快捷键扩展 - 开关 CC 字幕](../../registry/dist/plugins/utils/keymap-toggle-subtitle.js)
`keymap.actions.toggleSubtitle`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/utils/keymap-toggle-subtitle.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/utils/keymap-toggle-subtitle.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/utils/keymap-toggle-subtitle.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/utils/keymap-toggle-subtitle.js)
在快捷键的动作列表里添加一个 "开关 CC 字幕".
### [加载提示 - 灵梦油库里](../../registry/dist/plugins/v-loading/reimu.js)
`vLoading.reimu`

View File

@ -37,7 +37,7 @@
"type": "component",
"name": "removePlayerPopup",
"displayName": "删除视频弹窗",
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.",
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.\r\n",
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
"fullAbsolutePath": "registry/dist/components/video/player/remove-popup.js"
},

View File

@ -5,7 +5,7 @@ Issues 用于记录 Bug 等发生异常问题的情况, 如果您希望讨论新
> 请注意 Issues 和 Discussions (讨论区) 是两个独立板块, 搜索结果是相互独立的, Issues 搜索不到的内容可能能在 Discussions 里搜到.
>
> 讨论区于 2020-12-10 启用, 在此之前的新功能建议 / 提问 / 答疑的讨论仍然可以在 Issues 这边搜索到.
>
>
> 讨论区使用指南见 https://github.com/the1812/Bilibili-Evolved/discussions/1297
发之前记得看下置顶 (Pinned) 的 issue (如果有的话), 当遇到一些我认为会影响绝大部分用户的情况时, 我就会把这个 issue 置顶.
@ -18,8 +18,6 @@ Issues 用于记录 Bug 等发生异常问题的情况, 如果您希望讨论新
Bug 反馈不仅仅是描述您遇到了什么样的 bug, 更重要的是总结一套步骤, 按照这个步骤来就能稳定遇到 bug. (对应模板中的`问题描述`) 其次是一些环境信息, 模板中也已经给出, 通常包括`脚本版本`, `浏览器版本`, `错误信息`等. 有时候我可能还会需要您导出脚本设置.
> 提示: 最好先在预览离线版(Preview Offline)中测试一下, 有些问题可能已经修复了但还未正式发布.
我们对此的处理方式如下:
1. 对于给出了所有必要信息的 bug 反馈, 我们会尽量尝试重现并修复这个 bug, 优先级高于功能请求.
2. 信息不全或者无法重现的, 我们有空的话会追问更多信息, 争取转变为上一条的情况; 没空的话可能就没有回复了, 但会留个心眼, 如果我们之后遇到了会再找过来解决的, 当然您主动提供更多信息自然更好.
@ -37,10 +35,10 @@ Bug 反馈不一定会做出回复, 特别是内容写得非常清晰时, 我就
附部分标签含义解释:
> 讨论区的功能对应的标签这里不再列出.
- `bug`: 确认为Bug, 将会处理
- `maybe-bug`: 可能是Bug, 待测试验证
- `adaptation`: b站的变化导致脚本未跟进适配
- `bad-issue`: 不符合以上规范的issue
- `bug`: 确认为 Bug, 将会处理
- `maybe-bug`: 可能是 Bug, 待测试验证
- `adaptation`: b 站的变化导致脚本未跟进适配
- `bad-issue`: 不符合以上规范的 issue
- `compatibility`: 兼容性问题
- `duplicate`: 重复
- `help-wanted`: 没啥解决思路

21
doc/rollback.md Normal file
View File

@ -0,0 +1,21 @@
# 旧版本回退指南
> ⚠警告: 使用旧版本产生的不兼容 / 功能失效等问题需要自行承担, 本仓库只受理最新版脚本的反馈.
在 [Release](https://github.com/the1812/Bilibili-Evolved/releases) 中找到要回退的版本, 并记住发布的时间.
前往 https://github.com/the1812/Bilibili-Evolved/commits/master/dist/bilibili-evolved.user.js, 找到相同日期中的第一个 `CI Build`, 并点击查看详情.
![image](https://user-images.githubusercontent.com/26504152/206855808-27c0d501-0c98-4f9c-be30-53ad0db715ab.png)
在详情页中右键 `Raw`, 选择 `Save link as...` (链接另存为), 会下载脚本的内容.
![image](https://user-images.githubusercontent.com/26504152/206855884-4a704c4c-4020-47fc-aa04-0f855d73b19b.png)
将脚本内容粘贴覆盖到油猴中, 并删掉这两行检查更新用的配置.
![image](https://user-images.githubusercontent.com/26504152/206856196-fbde3ef9-eddf-4b3f-a12f-12547f2f0494.png)
保存后, 进入 b 站, 从脚本设置中关闭 `自动更新器`.
![image](https://user-images.githubusercontent.com/26504152/206856445-ce5ebeb1-7d58-4602-8c3b-c506efdd5f32.png)

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { FeedsCard } from '@/components/feeds/api'
import { feedsUrls } from '@/core/utils/urls'
@ -17,7 +17,7 @@ const entry = async () => {
added: addCopyLinkButton,
})
}
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'copyFeedsLink',
displayName: '复制动态链接',
description: {
@ -26,4 +26,4 @@ export const component: ComponentMetadata = {
entry,
urlInclude: feedsUrls,
tags: [componentsTags.feeds, componentsTags.utils],
}
})

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { FeedsCard, feedsCardTypes } from '@/components/feeds/api'
import { feedsUrls } from '@/core/utils/urls'
@ -86,7 +86,7 @@ const entry = async () => {
added: disableDetails,
})
}
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'disableFeedsDetails',
displayName: '禁止跳转动态详情',
tags: [componentsTags.feeds],
@ -103,4 +103,4 @@ export const component: ComponentMetadata = {
addStyle()
enabled = true
},
}
})

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { styledComponentEntry } from '@/components/styled-component'
interface LiveInfo {
@ -69,7 +69,7 @@ const entry = async () => {
console.log(presented, extend)
}
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'extendFeedsLive',
displayName: '直播信息扩充',
description: {
@ -78,4 +78,4 @@ export const component: ComponentMetadata = {
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
tags: [componentsTags.feeds, componentsTags.live],
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
}
})

View File

@ -1,5 +1,9 @@
import {
defineOptionsMetadata,
defineComponentMetadata,
OptionsOfMetadata,
} from '@/components/define'
import { feedsCardsManager } from '@/components/feeds/api'
import { ComponentMetadata, ComponentOptions } from '@/components/types'
import { feedsFilterPlugin } from './plugin'
const entry = async () => {
@ -21,36 +25,34 @@ const entry = async () => {
const { mountVueComponent } = await import('@/core/utils')
leftPanel.insertAdjacentElement('afterbegin', mountVueComponent(FeedsFilterCard).$el)
}
const options: ComponentOptions = {
const options = defineOptionsMetadata({
types: {
defaultValue: [],
defaultValue: [] as number[],
displayName: '过滤动态类型',
hidden: true,
},
patterns: {
defaultValue: [],
defaultValue: [] as string[],
displayName: '过滤关键词',
hidden: true,
},
sideCards: {
defaultValue: [],
/** FIXME: 虽然代码里按照`number[]`, 但其实存进去的是`string[]`? */
defaultValue: [] as number[],
displayName: '过滤侧边栏',
hidden: true,
},
specialTypes: {
defaultValue: [],
defaultValue: [] as number[],
displayName: '过滤特殊动态类型',
hidden: true,
},
}
export type FeedsFilterOptions = {
types: number[]
patterns: string[]
/** FIXME: 虽然代码里按照`number[]`, 但其实存进去的是`string[]`? */
sideCards: number[]
specialTypes: number[]
}
export const component: ComponentMetadata = {
})
export type FeedsFilterOptions = OptionsOfMetadata<typeof options>
export const component = defineComponentMetadata({
name: 'feedsFilter',
displayName: '动态过滤器',
description: {
@ -66,4 +68,4 @@ export const component: ComponentMetadata = {
/^https:\/\/t\.bilibili\.com\/$/,
],
plugin: feedsFilterPlugin,
}
})

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
const id = 'fixed-sidebars-style'
const entry = async () => {
@ -6,7 +6,7 @@ const entry = async () => {
disableProfilePopup()
}
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'fixedFeedsSidebars',
instantStyles: [
{
@ -22,4 +22,4 @@ export const component: ComponentMetadata = {
tags: [componentsTags.feeds],
entry,
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
}
})

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { styledComponentEntry } from '@/components/styled-component'
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
import { feedsCardsManager } from '@/components/feeds/api'
@ -24,7 +24,7 @@ const entry = async () => {
button.innerHTML = '收起评论'
button.addEventListener('click', () => {
clickHandler()
card.scrollIntoView()
card.scrollIntoView({ behavior: 'smooth' })
})
commentBox.insertAdjacentElement('beforeend', button)
}
@ -76,7 +76,7 @@ const entry = async () => {
})
}
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'foldComments',
displayName: '快速收起评论',
description: {
@ -85,4 +85,4 @@ export const component: ComponentMetadata = {
urlInclude: feedsUrlsWithoutDetail,
tags: [componentsTags.feeds],
entry: styledComponentEntry(() => import('./fold-comment.scss'), entry),
}
})

View File

@ -1,8 +1,8 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { toggleStyle } from '@/components/styled-component'
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
...toggleStyle('fullFeedsContent', () => import('./full-content.scss')),
displayName: '展开动态内容',
description: {
@ -10,4 +10,4 @@ export const component: ComponentMetadata = {
},
tags: [componentsTags.style, componentsTags.feeds],
urlInclude: feedsUrlsWithoutDetail,
}
})

View File

@ -1,11 +1,11 @@
import { defineComponentMetadata } from '@/components/define'
import { toggleStyle } from '@/components/styled-component'
import { ComponentMetadata } from '@/components/types'
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
...toggleStyle('fullFeedsTitle', () => import('./full-feeds-title.scss')),
displayName: '展开动态标题',
description: {
'zh-CN': '在顶栏的视频动态中, 无论标题多长总是完全展开.',
},
tags: [componentsTags.feeds, componentsTags.style],
}
})

View File

@ -1,8 +1,8 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { select } from '@/core/spin-query'
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'unfoldFeeds',
displayName: '动态反折叠',
tags: [componentsTags.feeds],
@ -28,4 +28,4 @@ export const component: ComponentMetadata = {
},
})
},
}
})

View File

@ -168,6 +168,8 @@ export default Vue.extend({
</script>
<style lang="scss">
@import 'common';
.custom-navbar-item {
color: inherit;
position: relative;
@ -229,7 +231,7 @@ export default Vue.extend({
}
&.active .main-content {
font-weight: bold;
@include semi-bold();
font-size: 11pt;
}

View File

@ -409,7 +409,7 @@ export default Vue.extend({
.title {
grid-area: title;
font-size: 13px;
font-weight: bold;
@include semi-bold();
@include max-line(2);
-webkit-box-align: start;
margin: 0;

View File

@ -25,15 +25,15 @@
<div class="row-title">过滤:</div>
<div class="type-filters">
<div v-for="t of types" :key="t.name" class="type-filter">
<VButton
round
<RadioButton
:title="(t.checked ? '不显示' : '显示') + t.displayName"
:class="{ checked: t.checked }"
@click="toggleTypeFilter(t)"
:checked="t.checked"
:disabled="loading"
@change="toggleTypeFilter(t)"
>
<VIcon :icon="t.icon" :size="18"></VIcon>
{{ t.displayName }}
</VButton>
</RadioButton>
</div>
</div>
</div>
@ -108,14 +108,24 @@
import { bilibiliApi, getJsonWithCredentials, postTextWithCredentials } from '@/core/ajax'
import { formData, getCsrf } from '@/core/utils'
import { descendingSort } from '@/core/utils/sort'
import { VButton, VIcon, TextBox, VLoading, VEmpty, ScrollTrigger, DpiImage } from '@/ui'
import {
VButton,
VIcon,
RadioButton,
TextBox,
VLoading,
VEmpty,
ScrollTrigger,
DpiImage,
} from '@/ui'
import { popperMixin } from '../mixins'
import { types, TypeFilter, HistoryItem, getHistoryItems, group } from './types'
import { types, TypeFilter, HistoryItem, getHistoryItems, group, HistoryType } from './types'
export default Vue.extend({
components: {
VButton,
VIcon,
RadioButton,
TextBox,
VLoading,
VEmpty,
@ -137,17 +147,12 @@ export default Vue.extend({
},
computed: {
canNextPage() {
return (
this.search === '' &&
!this.loading &&
this.hasMorePage &&
this.types.every((t: TypeFilter) => t.checked)
)
return this.search === '' && !this.loading && this.hasMorePage
},
},
watch: {
search: lodash.debounce(function search() {
this.updateGroups()
this.reloadHistoryItems()
}, 200),
},
async created() {
@ -159,11 +164,22 @@ export default Vue.extend({
},
methods: {
toggleTypeFilter(typeFilter: TypeFilter) {
typeFilter.checked = !typeFilter.checked
this.updateGroups()
types.forEach(t => (t.checked = t.name === typeFilter.name))
this.reloadHistoryItems()
},
async reloadHistoryItems() {
this.viewTime = 0
this.hasMorePage = true
this.loading = true
try {
await this.nextPage()
} finally {
this.loading = false
}
},
filterFunc(item: HistoryItem) {
if (types.some(t => t.name === item.type && !t.checked)) {
const isAllType = types.find(it => it.name === HistoryType.All).checked
if (!isAllType && types.some(t => t.name === item.type && !t.checked)) {
return false
}
if (
@ -178,7 +194,10 @@ export default Vue.extend({
this.groups = group(this.cards.filter(this.filterFunc))
},
async nextPage() {
const items = await getHistoryItems(this.viewTime)
const items = await getHistoryItems(
this.viewTime,
types.find(t => t.checked),
)
const cards: HistoryItem[] = lodash.uniqBy(
this.cards.concat(items).sort(descendingSort((item: HistoryItem) => item.viewAt)),
item => item.id,
@ -188,7 +207,10 @@ export default Vue.extend({
if (cards.length > 0) {
this.viewTime = lodash.last(cards).viewAt
}
this.hasMorePage = cards.length !== 0
this.hasMorePage = items.length !== 0
if (this.hasMorePage && this.groups.length === 0) {
await this.nextPage()
}
},
async updateHistoryPauseState() {
const result = await bilibiliApi(
@ -223,7 +245,7 @@ export default Vue.extend({
@import '../popup';
.custom-navbar-history-list {
width: 350px;
width: 400px;
@include navbar-popup-height();
font-size: 12px;
padding: 0;
@ -251,23 +273,22 @@ export default Vue.extend({
margin: 16px 12px 4px 12px;
.header-row {
@include h-stretch(8px);
justify-content: space-between;
.row-title {
@include h-center();
}
}
.type-filters {
@include h-center(8px);
@include h-center(6px);
.type-filter {
.be-button {
padding: 4px 8px 4px 6px;
color: #8888;
.be-icon {
margin-right: 6px;
}
&.checked {
color: inherit;
}
// color: #8888;
// .be-icon {
// margin-right: 6px;
// }
// &.checked {
// color: inherit;
// }
}
}
}
@ -402,8 +423,8 @@ export default Vue.extend({
}
}
.title {
@include semi-bold();
grid-area: title;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -11,7 +11,7 @@ export const history: CustomNavbarItemInit = {
active: document.URL.replace(/\?.*$/, '') === href,
loginRequired: true,
boundingWidth: 350,
boundingWidth: 400,
noPopupPadding: true,
popupContent: () => import('./NavbarHistory.vue').then(m => m.default),
}

View File

@ -4,6 +4,7 @@ import { formatDuration } from '@/core/utils/formatters'
/** 历史项目类型, 值为 API 中的 `history.business` */
export enum HistoryType {
All = 'all',
Video = 'archive',
Live = 'live',
Article = 'article',
@ -48,32 +49,44 @@ export interface TypeFilter {
displayName: string
icon: string
checked: boolean
apiType: string
}
/** 所有的历史记录类型 */
export const types = [
{
name: HistoryType.All,
displayName: '全部',
icon: '',
checked: true,
apiType: '',
},
{
name: HistoryType.Video,
displayName: '视频',
icon: 'mdi-play-circle-outline',
checked: true,
checked: false,
apiType: 'archive',
},
{
name: HistoryType.Bangumi,
displayName: '番剧',
icon: 'mdi-television-classic',
checked: true,
checked: false,
apiType: 'archive',
},
{
name: HistoryType.Live,
displayName: '直播',
icon: 'mdi-video-wireless-outline',
checked: true,
checked: false,
apiType: 'live',
},
{
name: HistoryType.Article,
displayName: '专栏',
icon: 'mdi-newspaper-variant-outline',
checked: true,
checked: false,
apiType: 'article',
},
] as TypeFilter[]
@ -188,12 +201,17 @@ const parseHistoryItem = (item: any): HistoryItem => {
* ,
* @param viewTime
*/
export const getHistoryItems = async (viewTime?: number) => {
let api = 'https://api.bilibili.com/x/web-interface/history/cursor'
export const getHistoryItems = async (viewTime?: number, type?: TypeFilter) => {
const api = 'https://api.bilibili.com/x/web-interface/history/cursor'
const params = new URLSearchParams()
if (viewTime) {
api += `?view_at=${Math.round(viewTime / 1000)}`
params.set('view_at', Math.round(viewTime / 1000).toString())
}
const { list } = await bilibiliApi(getJsonWithCredentials(api), '获取历史记录失败')
params.set('type', type?.apiType ?? '')
const { list } = await bilibiliApi(
getJsonWithCredentials(`${api}?${params.toString()}`),
'获取历史记录失败',
)
if (!Array.isArray(list)) {
return []
}

View File

@ -186,7 +186,7 @@ export default Vue.extend({
.title {
margin-left: 6px;
font-size: 18px;
font-weight: bold;
@include semi-bold();
}
.grow {
flex: 1;

View File

@ -233,7 +233,7 @@ export default Vue.extend({
}
.title {
font-size: 14px;
font-weight: bold;
@include semi-bold();
padding-top: 4px;
color: inherit;
white-space: nowrap;

View File

@ -325,6 +325,8 @@ export default Vue.extend({
</script>
<style lang="scss">
@import 'common';
.user-info-panel {
border-radius: 8px;
overflow: hidden;
@ -441,7 +443,7 @@ export default Vue.extend({
.welcome,
.name {
font-size: 16px;
font-weight: bold;
@include semi-bold();
margin: 46px 0 16px 0;
text-align: center;
color: inherit;
@ -495,7 +497,7 @@ export default Vue.extend({
color: var(--theme-color) !important;
}
.stats-number {
font-weight: bold;
@include semi-bold();
margin-bottom: 4px;
font-size: 14px;
transition: none;

View File

@ -326,7 +326,7 @@ export default Vue.extend({
.title {
grid-area: title;
font-size: 13px;
font-weight: bold;
@include semi-bold();
margin: 0;
margin-top: 8px;
padding: 0 10px;

View File

@ -891,3 +891,6 @@
.right-area .tip-wrap .icon path[fill="#00AEEC"] {
@include theme-fill();
}
.bpx-player-pbp-videoshot rect[clip-path] {
@include theme-fill();
}

View File

@ -1,8 +1,8 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { LifeCycleEventTypes } from '@/core/life-cycle'
import { darkExcludes } from '../dark-urls'
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'darkModeFollowSystem',
displayName: '夜间模式跟随系统',
entry: () => {
@ -30,4 +30,4 @@ export const component: ComponentMetadata = {
> ( \`Microsoft Edge\`) 中,夜间模式仅会同步浏览器的亮 / 暗主题.
`.trim(),
},
}
})

View File

@ -1,4 +1,8 @@
import { ComponentMetadata } from '@/components/types'
import {
defineComponentMetadata,
defineOptionsMetadata,
OptionsOfMetadata,
} from '@/components/define'
import { fullyLoaded } from '@/core/life-cycle'
import { ComponentSettings, getComponentSettings } from '@/core/settings'
import { Range } from '@/ui/range'
@ -87,7 +91,33 @@ class ScheduleTime {
return result
}
}
const checkTime = (settings: ComponentSettings) => {
const options = defineOptionsMetadata({
range: {
defaultValue: {
start: '18:00',
end: '6:00',
},
displayName: '时间段',
validator: (range: Range<string>) => {
const { start, end } = range
const regex = /^(\d{1,2}):(\d{1,2})$/
if (!regex.test(start) || !regex.test(end)) {
return null
}
const startTime = new ScheduleTime(range.start)
const endTime = new ScheduleTime(range.end)
return {
start: startTime.toString(),
end: endTime.toString(),
}
},
},
})
type Options = OptionsOfMetadata<typeof options>
const checkTime = (settings: ComponentSettings<Options>) => {
const start = new ScheduleTime(settings.options.range.start)
const end = new ScheduleTime(settings.options.range.end)
const now = new ScheduleTime()
@ -106,7 +136,8 @@ const checkTime = (settings: ComponentSettings) => {
setTimeout(() => checkTime(settings), timeout)
}
}
export const component: ComponentMetadata = {
export const component = defineComponentMetadata({
name: 'darkModeSchedule',
displayName: '夜间模式计划时段',
description:
@ -114,26 +145,5 @@ export const component: ComponentMetadata = {
tags: [componentsTags.style, componentsTags.general],
entry: ({ settings }) => fullyLoaded(() => checkTime(settings)),
urlExclude: darkExcludes,
options: {
range: {
defaultValue: {
start: '18:00',
end: '6:00',
},
displayName: '时间段',
validator: (range: Range<string>) => {
const { start, end } = range
const regex = /^(\d{1,2}):(\d{1,2})$/
if (!regex.test(start) || !regex.test(end)) {
return null
}
const startTime = new ScheduleTime(range.start)
const endTime = new ScheduleTime(range.end)
return {
start: startTime.toString(),
end: endTime.toString(),
}
},
},
},
}
options,
})

View File

@ -27,11 +27,11 @@ $prefix: 'simplifyComments-switch';
margin-bottom: 4px !important;
max-height: 19.5px !important;
}
.reply-content {
.reply-content-container {
display: block !important;
flex-basis: 100% !important;
}
.root-reply:not(.reply-content) {
.root-reply:not(.reply-content-container) {
padding: 0 !important;
display: flex !important;
align-items: center !important;

View File

@ -1,5 +1,5 @@
import { createSwitchOptions, SwitchOptions } from '@/components/switch-options'
import { ComponentMetadata } from '@/components/types'
import { newSwitchComponentWrapper, defineSwitchMetadata } from '@/components/switch-options'
import { addComponentListener, getComponentSettings } from '@/core/settings'
import { sq } from '@/core/spin-query'
import { addStyle } from '@/core/style'
@ -7,7 +7,7 @@ import { getCookieValue, matchUrlPattern } from '@/core/utils'
import { useScopedConsole } from '@/core/utils/log'
import { mainSiteUrls } from '@/core/utils/urls'
const switchOptions: SwitchOptions = {
const switchMetadata = defineSwitchMetadata({
name: 'simplifyOptions',
dimAt: 'checked',
switchProps: {
@ -44,9 +44,10 @@ const switchOptions: SwitchOptions = {
displayName: '右侧分区导航(旧)',
},
},
}
})
const console = useScopedConsole('简化首页')
const metadata: ComponentMetadata = {
export const component = newSwitchComponentWrapper(switchMetadata)({
name: 'simplifyHome',
displayName: '简化首页',
description: '隐藏原版首页不需要的元素 / 分区.',
@ -58,7 +59,7 @@ const metadata: ComponentMetadata = {
],
urlInclude: mainSiteUrls,
tags: [componentsTags.style],
entry: async () => {
entry: async ({ metadata }) => {
// 正好是首页时提供首页分区的简化选项
const isHome = matchUrlPattern('https://www.bilibili.com/')
if (!isHome) {
@ -144,10 +145,10 @@ const metadata: ComponentMetadata = {
},
true,
)
switchOptions.switches[key] = option
switchMetadata.switches[key] = option
generatedSwitches[key] = option
})
options.simplifyOptions.switches = generatedSwitches
;(options.simplifyOptions as any).switches = generatedSwitches
const generatedStyles = Object.keys(generatedOptions)
.map(name =>
`
@ -160,6 +161,4 @@ const metadata: ComponentMetadata = {
.join('\n')
addStyle(generatedStyles, 'simplify-home-generated')
},
}
export const component = createSwitchOptions(switchOptions)(metadata)
})

View File

@ -1,8 +1,8 @@
import { createSwitchOptions } from '@/components/switch-options'
import { newSwitchComponentWrapper } from '@/components/switch-options'
import { styledComponentEntry } from '@/components/styled-component'
import { liveUrls } from '@/core/utils/urls'
export const component = createSwitchOptions({
export const component = newSwitchComponentWrapper({
name: 'simplifyOptions',
dimAt: 'checked',
switchProps: {

View File

@ -4,7 +4,6 @@ import {
OptionsOfMetadata,
} from '@/components/define'
import { ComponentEntry } from '@/components/types'
import { toggleStyle } from '@/components/styled-component'
import { playerUrls } from '@/core/utils/urls'
import { addComponentListener } from '@/core/settings'
@ -33,10 +32,18 @@ const entry: ComponentEntry<Options> = ({ metadata, settings }) => {
})
}
const name = 'disableSpecialDanmaku'
export const component = defineComponentMetadata({
name,
entry,
displayName: '禁用特殊弹幕样式',
tags: [componentsTags.style],
...toggleStyle('disableSpecialDanmaku', () => import('./special-danmaku.scss'), entry),
instantStyles: [
{
name,
style: () => import('./special-danmaku.scss'),
},
],
urlInclude: playerUrls,
description: {
'zh-CN': '移除高赞弹幕或 UP 主弹幕的特殊样式, 弹幕内容不会移除.',

View File

@ -1,21 +1,30 @@
body.disable-highlight-danmaku-style .b-danmaku-high {
display: block !important;
padding: 0 !important;
line-height: 1.125 !important;
.b-danmaku-high-icon {
display: none !important;
}
.b-danmaku-high-text {
margin: 0 !important;
text-shadow: inherit;
body.disable-highlight-danmaku-style {
.bili-dm.bili-high,
.b-danmaku-high {
display: block !important;
padding: 0 !important;
line-height: 1.125 !important;
.bili-high-icon,
.b-danmaku-high-icon {
display: none !important;
}
.bili-high-text,
.b-danmaku-high-text {
margin: 0 !important;
text-shadow: inherit;
}
}
}
body.disable-up-danmaku-style .b-danmaku-up {
padding: 0 !important;
line-height: 1.125 !important;
background-color: transparent !important;
border-radius: 0 !important;
.b-danmaku-up-tip {
display: none !important;
body.disable-up-danmaku-style {
.bili-dm.bili-up,
.b-danmaku-up {
padding: 0 !important;
line-height: 1.125 !important;
background-color: transparent !important;
border-radius: 0 !important;
.bili-up-tip,
.b-danmaku-up-tip {
display: none !important;
}
}
}

View File

@ -72,7 +72,7 @@ export const enableTouchMove = (element: HTMLElement, options?: TouchMoveOptions
options,
'minMoveDistance',
getComponentSettings('touchMiniPlayer').options.touchMoveDistance,
)
) as number
// const scroll = lodash.get(options, 'scroll', false)
const touchstart = (e: TouchEvent) => {
if (e.touches.length < 1) {

View File

@ -39,7 +39,7 @@ const createPosition = (e: TouchEvent, element: HTMLElement) => {
* - `cancel`:
*/
export class SwipeAction extends EventTarget {
minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance
minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance as number
startPosition: Position = null
lastAction: {
type: 'brightness' | 'volume' | 'progress'

View File

@ -6,8 +6,11 @@ import { Toast } from '@/core/toast'
import { matchUrlPattern, retrieveImageUrl } from '@/core/utils'
import { formatTitle } from '@/core/utils/title'
import { feedsUrls } from '@/core/utils/urls'
import { Options } from '.'
export const setupFeedImageExporter: ComponentEntry = async ({ settings: { options } }) => {
export const setupFeedImageExporter: ComponentEntry<Options> = async ({
settings: { options },
}) => {
if (!feedsUrls.some(url => matchUrlPattern(url))) {
return
}

View File

@ -1,8 +1,25 @@
import { defineComponentMetadata } from '@/components/define'
import {
defineComponentMetadata,
defineOptionsMetadata,
OptionsOfMetadata,
} from '@/components/define'
import { matchUrlPattern } from '@/core/utils'
import { columnUrls, feedsUrls } from '@/core/utils/urls'
import { setupFeedImageExporter } from './feed'
const options = defineOptionsMetadata({
columnFormat: {
defaultValue: '[title][ - n]',
displayName: '专栏图片命名格式',
},
feedFormat: {
defaultValue: '[user][ - id][ - n]',
displayName: '动态图片命名格式',
},
})
export type Options = OptionsOfMetadata<typeof options>
export const component = defineComponentMetadata({
name: 'imageExporter',
displayName: '图片批量导出',
@ -15,14 +32,5 @@ export const component = defineComponentMetadata({
component: () => import('./Widget.vue').then(m => m.default),
},
urlInclude: [...feedsUrls, ...columnUrls],
options: {
columnFormat: {
defaultValue: '[title][ - n]',
displayName: '专栏图片命名格式',
},
feedFormat: {
defaultValue: '[user][ - id][ - n]',
displayName: '动态图片命名格式',
},
},
options,
})

View File

@ -1,6 +1,20 @@
import { defineComponentMetadata } from '@/components/define'
import {
defineComponentMetadata,
defineOptionsMetadata,
OptionsOfMetadata,
} from '@/components/define'
import { startResolution } from './resolution'
const options = defineOptionsMetadata({
scale: {
displayName: '缩放级别',
defaultValue: 'auto',
hidden: true,
},
})
export type Options = OptionsOfMetadata<typeof options>
export const component = defineComponentMetadata({
name: 'imageResolution',
displayName: '高分辨率图片',
@ -11,11 +25,5 @@ export const component = defineComponentMetadata({
'zh-CN':
'根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)',
},
options: {
scale: {
displayName: '缩放级别',
defaultValue: 'auto',
hidden: true,
},
},
options,
})

View File

@ -1,4 +1,5 @@
import { styledComponentEntry } from '@/components/styled-component'
import { Options } from '.'
const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/
const excludeSelectors = ['#certify-img1', '#certify-img2']
@ -62,7 +63,7 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
)
})
}
export const startResolution = styledComponentEntry(
export const startResolution = styledComponentEntry<Options>(
() => import('./fix.scss'),
async ({ settings }) => {
const { allMutations } = await import('@/core/observer')

View File

@ -0,0 +1,17 @@
import { defineComponentMetadata } from '@/components/define'
import { realEntry } from './logic'
export const component = defineComponentMetadata({
name: 'importSeries',
author: {
name: 'aiden',
link: 'https://github.com/swhoro',
},
displayName: '批量导入合集',
tags: [componentsTags.utils],
entry: realEntry,
description: {
'zh-CN': '在合集页面制作一个批量导入按钮,可以新增所有合集内视频至同名收藏夹',
},
urlInclude: [/^https:\/\/space\.bilibili\.com\//],
})

View File

@ -0,0 +1,151 @@
import { getCsrf, delay } from '@/core/utils'
import { sq } from '@/core/spin-query'
import { urlChange } from '@/core/observer'
import { Toast } from '@/core/toast'
const importSeries = async (sid, uid, csrf) => {
// 获取合集所有视频
const seriesVideos = (
await fetch(
`https://api.bilibili.com/x/series/archives?mid=${uid}&series_id=${sid}&only_normal=true&sort=desc&pn=1&ps=99999`,
).then(r => r.json())
).data.archives
// 合集名
const seriesName = await sq(
() => document.getElementsByClassName('item cur')[0].innerHTML,
t => t !== '',
)
// 创建收藏夹获取新收藏夹id
let favId = 0
while (true) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `title=${seriesName}&csrf=${csrf}&privacy=0`,
}).then(r => r.json())
if (response.code === 0) {
favId = response.data.id
break
} else {
await delay(2000)
}
}
const prompt = ' 过久未动即触发风控,等待一段时间自动继续'
const toast = Toast.info(`0 / ${seriesVideos.length}${prompt}`, '收藏系列')
// 添加每一个视频至收藏夹
for (let i = 0; i < seriesVideos.length; i++) {
// 做个延迟,防止太快而遭服务器拒绝
await delay(500)
while (true) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `rid=${seriesVideos[i].aid}&add_media_ids=${favId}&type=2&csrf=${csrf}`,
}).then(r => r.json())
if (response.code === 0) {
// 如果请求成功更新toast
toast.message = `${i + 1} / ${seriesVideos.length}${prompt}`
break
} else {
// 如果请求失败,等待若干秒后重试
const delayTime = Math.random() * 2000 + 2000
console.log(`请求失败,等待${(delayTime / 1000).toFixed(1)}s后重试`)
await delay(delayTime)
}
}
}
// 添加收藏夹任务完成
toast.duration = 1000
Toast.success('完成', '收藏系列', 2000)
}
const importCollection = async (sid, csrf) => {
while (true) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `platform=web&season_id=${sid}&csrf=${csrf}`,
}).then(r => r.json())
if (response.code === 0) {
Toast.success('成功', '导入合集', 2000)
break
} else {
await delay(2000)
}
}
}
const addButton = () => {
enum PageType {
Collection,
Series,
}
const url = new URL(window.location.href)
let pageType: PageType
// series 或 collection 的 id
let sid
if (url.pathname.includes('seriesdetail')) {
// series id
sid = url.searchParams.get('sid')
pageType = PageType.Series
} else if (url.pathname.includes('collectiondetail')) {
// collection id
sid = url.searchParams.get('sid')
pageType = PageType.Collection
} else {
return
}
// 合集作者
const uid = url.pathname.split('/')[1]
// 当前登录用户 我 的验证信息
const csrf = getCsrf()
// add a button
const pageHead = document.getElementsByClassName('page-head')[0]
const rel = pageHead.children[1]
const button = document.createElement('a')
// 什么离谱class名play打错了可还行
button.className = 'paly-all-btn'
button.textContent = '一键收藏'
button.style.cssText = `
right: 6rem;
display: block;
text-align: center;
width: 65px;
`
pageHead.insertBefore(button, rel)
button.onclick = async () => {
if (pageType === PageType.Series) {
await importSeries(sid, uid, csrf)
} else {
await importCollection(sid, csrf)
}
}
}
export const realEntry = async () => {
urlChange(() => {
addButton()
})
}

View File

@ -1,6 +1,7 @@
import { playerAgent } from '@/components/video/player-agent'
import { getComponentSettings } from '@/core/settings'
import { registerAndGetData } from '@/plugins/data'
import { Options } from '.'
import { KeyBindingAction, KeyBindingActionContext } from './bindings'
export const clickElement = (target: string | HTMLElement, context: KeyBindingActionContext) => {
@ -181,11 +182,13 @@ export const builtInActions: Record<string, KeyBindingAction> = {
},
longJumpBackward: {
displayName: '长倒退',
run: () => playerAgent.changeTime(-getComponentSettings('keymap').options.longJumpSeconds),
run: () =>
playerAgent.changeTime(-getComponentSettings<Options>('keymap').options.longJumpSeconds),
},
longJumpForward: {
displayName: '长前进',
run: () => playerAgent.changeTime(getComponentSettings('keymap').options.longJumpSeconds),
run: () =>
playerAgent.changeTime(getComponentSettings<Options>('keymap').options.longJumpSeconds),
},
jumpBackward: {
displayName: '倒退',

View File

@ -13,7 +13,7 @@ export interface KeyBindingActionContext {
}
export interface KeyBindingAction {
displayName: string
run: (context: KeyBindingActionContext) => any
run: (context: KeyBindingActionContext) => unknown
prevent?: boolean
ignoreTyping?: boolean
}

View File

@ -1,22 +1,41 @@
import { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import { styledComponentEntry } from '@/components/styled-component'
import { ComponentEntry } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import {
defineComponentMetadata,
defineOptionsMetadata,
OptionsOfMetadata,
} from '@/components/define'
import { addComponentListener } from '@/core/settings'
import { actions } from './actions'
import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings'
import { presetBase, presets } from './presets'
const options = defineOptionsMetadata({
longJumpSeconds: {
defaultValue: 85,
displayName: '长跳跃秒数',
},
customKeyBindings: {
defaultValue: {} as Record<string, string>,
displayName: '自定义键位',
hidden: true,
},
preset: {
defaultValue: 'Default',
displayName: '预设',
hidden: true,
},
})
export type Options = OptionsOfMetadata<typeof options>
let config: KeyBindingConfig = null
const parseBindings = (bindings: Record<string, string>) =>
Object.entries(bindings).map(([actionName, keyString]) => {
const parseBindings = (bindings: Record<string, string>): KeyBinding[] => {
const parseBinding = (actionName: string, keyString: string) => {
const keys = keyString.split(' ').filter(it => it !== '')
return {
keys,
action: actions[actionName] || none,
} as KeyBinding
})
const entry: ComponentEntry = styledComponentEntry(
return { keys, action: actions[actionName] }
}
return Object.entries(bindings).map(([n, k]) => parseBinding(n, k))
}
const entry = styledComponentEntry<Options>(
() => import('./playback-tip.scss'),
async ({ settings }) => {
const update = () => {
@ -58,22 +77,7 @@ export const component = defineComponentMetadata({
'zh-CN': '为脚本的功能和 b 站的功能启用键盘快捷键支持, 快捷键列表可在`快捷键设置`中查看和配置.',
},
extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default),
options: {
longJumpSeconds: {
defaultValue: 85,
displayName: '长跳跃秒数',
},
customKeyBindings: {
defaultValue: {},
displayName: '自定义键位',
hidden: true,
},
preset: {
defaultValue: 'Default',
displayName: '预设',
hidden: true,
},
},
options,
plugin: {
displayName: '快捷键扩展 - 搜索支持',
setup: ({ addData }) => {

View File

@ -74,6 +74,13 @@ const entry: ComponentEntry = async ({ settings, metadata }) => {
},
true,
)
addComponentListener(
`${metadata.name}.showPlaceholder`,
(value: boolean) => {
document.body.classList.toggle('promotion-show-placeholder', value)
},
true,
)
}
export const component = defineComponentMetadata({
name: 'removePromotions',

View File

@ -1,3 +1,5 @@
@import 'common';
#slide_ad,
.v-wrap .vcd,
.ad-report,
@ -80,3 +82,25 @@ body.dark .blocked-ads.new {
body:not(.preserve-feed-goods) .bili-dyn-content__orig__additional:has(.dyn-goods) {
margin: 0 !important;
}
.bili-video-card__wrap {
a[href*="cm.bilibili.com"] {
> * {
visibility: hidden !important;
}
& ~ .bili-video-card__info {
display: none !important;
}
body.promotion-show-placeholder &::before {
content: "🚫已屏蔽广告";
border-radius: 6px;
@include h-center();
justify-content: center;
position: absolute;
z-index: 2;
font-size: 16px;
width: 100%;
height: 100%;
background-color: #8882;
}
}
}

View File

@ -48,6 +48,7 @@ const entry = async () => {
'-Arouter',
'vd_source',
'is_story_h5',
'buvid',
]
const [blockParams] = registerAndGetData('urlParamsClean.params', builtInBlockParams)
const builtInSiteSpecifiedParams = [
@ -63,6 +64,14 @@ const entry = async () => {
match: /\/\/www\.bilibili\.com\/bangumi\//,
param: 'theme',
},
{
match: /\/\/www\.bilibili\.com\/video\//,
param: 'mid',
},
{
match: /\/\/www\.bilibili\.com\/video\//,
param: 'up_id',
},
]
const [siteSpecifiedParams] = registerAndGetData(
'urlParamsClean.siteSpecifiedParams',

View File

@ -63,9 +63,8 @@
<div
v-if="selectedOutput && selectedOutput.description"
class="download-video-config-description"
>
{{ selectedOutput.description }}
</div>
v-html="selectedOutput.description"
></div>
<component
:is="selectedOutput.component"
v-if="selectedOutput && selectedOutput.component"
@ -417,8 +416,11 @@ export default Vue.extend({
align-self: stretch;
}
.download-video-config-description {
opacity: 0.5;
color: #888d;
margin-top: 4px;
a {
color: var(--theme-color-70);
}
}
&-footer {
@include h-center();

View File

@ -2,10 +2,12 @@ import { StreamSaverLibrary } from '@/core/runtime-library'
import { Toast } from '@/core/toast'
import { DownloadVideoOutput } from '../types'
const serviceWorkerMessage =
'需要浏览器允许来自 jimmywarting.github.io (StreamSaver 的网站) 的第三方 cookie, 详细原因见 <a href="https://github.com/jimmywarting/StreamSaver.js?#how-does-it-work" target="blank">How does it work</a>'
export const streamSaverOutput: DownloadVideoOutput = {
name: 'steamSaver',
displayName: 'StreamSaver',
description: '使用 StreamSaver 输出到本地文件, 下载过程中请勿关闭页面.',
description: `使用 StreamSaver 输出到本地文件, 下载过程中请勿关闭页面. 注意: ${serviceWorkerMessage}, 否则弹出的链接点击后会没有反应.`,
runAction: async action => {
const streamSaver = await StreamSaverLibrary
const fragments = action.infos.flatMap(it => it.titledFragments)

View File

@ -1,5 +1,5 @@
body {
&.full-episode-title {
&.full-episode-title:not(.disable-full-episode-list) {
.base-video-sections-v1 {
.video-episode-card {
height: auto !important;
@ -43,7 +43,7 @@ body {
.video-sections-content-list {
max-height: unset !important;
height: auto !important;
.video-section-list {
.video-section-list:not([style*="height: 0px"]) {
height: auto !important;
}
}

View File

@ -1,4 +1,9 @@
import { ComponentEntry, ComponentMetadata, ComponentOption } from '@/components/types'
import {
ComponentEntry,
ComponentMetadata,
OptionMetadata,
UnknownOptions,
} from '@/components/types'
import { CoreApis } from '@/core/core-apis'
import { addComponentListener, ComponentSettings } from '@/core/settings'
import { logError } from '@/core/utils/log'
@ -25,10 +30,12 @@ export type EntryContext = Parameters<ComponentEntry>[0]
export type OptionSubjects<O> = O & { [K in keyof O as `${Exclude<K, symbol>}$`]: Subject<O[K]> }
export class EntrySpeedComponent<O = Record<string, unknown>> implements EntryContext {
export class EntrySpeedComponent<O extends UnknownOptions = UnknownOptions>
implements EntryContext
{
static create: <OO extends Record<string, any> = unknown>(
metadata: Omit<ComponentMetadata, 'entry' | 'reload' | 'unload' | 'options'> & {
options?: { [K in keyof OO]: ComponentOption }
options?: { [K in keyof OO]: OptionMetadata }
},
) => ComponentMetadata

View File

@ -34,11 +34,15 @@ export const ERROR_MESSAGE_DURATION = 5000
/** 计算菜单项 order */
export const calcOrder = (value: number) => ((MAX_BROWSER_SPEED_VALUE - value) * 10000).toString()
export interface Options {
export type Options = {
/** 最大菜单高度 */
maxMenuHeight: boolean
/** 隐藏进度条 */
hideScrollbar: boolean
/** 隐藏移除图标 */
hideRemoveBtn: boolean
/** 隐藏新增图标 */
hideAddBtn: boolean
/** 扩展倍速列表 */
extendSpeedList: number[]
}
@ -381,6 +385,33 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
}),
)
this.options.hideRemoveBtn$.subscribe(
loadStyle({
name: 'extend-video-speed-no-remove-btn-style',
style: hideRemoveBtn =>
hideRemoveBtn &&
`
.${EXTEND_SPEED_ITEM_CLASS_NAME} [data-ref="close-btn"] {
display: none !important;
}
:is(${PLAYER_AGENT.custom.speedMenuItem.selector}):not(${PLAYER_AGENT.custom.active.selector}):hover [data-ref="close-btn"] {
display: none !important;
}`,
}),
)
this.options.hideAddBtn$.subscribe(
loadStyle({
name: 'extend-video-speed-no-add-btn-style',
style: hideAddBtn =>
hideAddBtn &&
`
.${EXTEND_SPEED_INPUT_CLASS_NAME} {
display: none !important;
}`,
}),
)
let nativeSpeedValue = 1
playbackRate$

View File

@ -18,6 +18,10 @@ export const component = ExtendSpeedComponent.create<Options>({
- \`隐藏滚动条\`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.
- \`隐藏移除图标\`:如果认为倍速右侧的移除倍速图标有些突兀,可以开启此选项隐藏.
- \`隐藏新增图标\`:如果认为顶部的新增倍速图标有些突兀,可以开启此选项隐藏.
### ****
2.5x 3.0x .
@ -47,6 +51,14 @@ export const component = ExtendSpeedComponent.create<Options>({
displayName: '隐藏滚动条',
defaultValue: false,
},
hideRemoveBtn: {
displayName: '隐藏移除图标',
defaultValue: false,
},
hideAddBtn: {
displayName: '隐藏新增图标',
defaultValue: false,
},
extendSpeedList: {
displayName: '扩展倍速列表',
defaultValue: [2.5, 3],

View File

@ -3,7 +3,7 @@ import { EntrySpeedComponent, VideoIdObject } from '../common/speed'
import { NoSuchSpeedMenuItemElementError, SpeedContext } from '../common/speed/context'
import { formatSpeedText } from '../common/speed/utils'
export interface Options {
export type Options = {
/** 全局倍速 */
globalSpeed: number
/** 固定全局倍速 */

View File

@ -0,0 +1 @@
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.

View File

@ -29,9 +29,6 @@ export const component = defineComponentMetadata({
],
displayName: '删除视频弹窗',
tags: [componentsTags.video, componentsTags.style],
description: {
'zh-CN': '删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.',
},
urlInclude: playerUrls,
options: {
votes: {

View File

@ -1,6 +1,8 @@
body {
&.remove-player-popup {
// https://www.bilibili.com/video/BV1V54y1b7yW/
&-combo-likes {
.bili-guide,
.bilibili-player-video-popup-three,
.bilibili-player-video-popup-three-animate,
.bilibili-player-video-popup-follow,
@ -12,14 +14,18 @@ body {
display: none !important;
}
}
// https://www.bilibili.com/video/BV11h411k72c/
&-related-videos {
.bili-link,
.bilibili-player-video-link,
.bilibili-player-link,
.bpx-player-link {
display: none !important;
}
}
// https://www.bilibili.com/video/BV12Y4y1L7SR/
&-votes {
.bili-vote,
.bilibili-player-video-popup-vote,
.bpx-player-popup-dm-close,
.bpx-player-popup-dm-shrink,
@ -27,7 +33,9 @@ body {
display: none !important;
}
}
// https://www.bilibili.com/video/BV1V54y1b7yW/
&-rates {
.bili-score,
.bilibili-player-score,
.bpx-player-popup-dm-close,
.bpx-player-score-summary-wrap,
@ -35,7 +43,9 @@ body {
display: none !important;
}
}
// https://www.bilibili.com/video/BV1fe4y1b7NE/
&-reservations {
.bili-reserve,
.bpx-player-reserve {
display: none !important;
}

View File

@ -4,6 +4,7 @@ import { playerReady } from '@/core/utils'
import { videoUrls } from '@/core/utils/urls'
import { getVueData } from '@/components/feeds/api'
import { VideoInfo } from '@/components/video/video-info'
import { useScopedConsole } from '@/core/utils/log'
interface RecommendList extends Vue {
isOpen: boolean
@ -15,35 +16,41 @@ interface RecommendList extends Vue {
name: string
}
}[]
$children: VideoPageCard[]
$children: (RecommendList & VideoPageCard)[]
}
interface VideoPageCard extends Vue {
name: string
title: string
// 组件添加元素非b站自有元素
mark: boolean
item: {
aid: string
ctime: number
owner: {
// 组件添加元素非b站自有元素
mark: boolean
name: string
}
}
}
const displayName = '显示视频投稿时间'
const console = useScopedConsole(displayName)
export const component = defineComponentMetadata({
author: { name: 'wisokey', link: 'https://github.com/wisokey' },
name: 'showUploadTime',
displayName: '显示视频投稿时间',
displayName,
description: '为视频播放页面的推荐列表中的视频添加显示视频投稿时间.',
tags: [componentsTags.video],
urlInclude: videoUrls,
entry: async () => {
const showUploadTime = (relist: VideoPageCard[]) => {
relist.forEach(async video => {
// 确认存放推荐视频列表的List中的元素是否被更新
if (!video.item.owner.mark) {
video.item.owner.mark = true
// 确认推荐视频卡片是否被更新
if (!video.mark) {
video.mark = true
let createTime: Date
@ -58,22 +65,39 @@ export const component = defineComponentMetadata({
createTime.getMonth() + 1
}-${createTime.getDate()}`
}
// 保存生成后的name
video.item.owner.name = video.name
}
})
}
const getRecoList = () => {
const reco_list = dq('#reco_list')
let recoList: RecommendList = getVueData(reco_list)
if (recoList.isOpen === undefined) {
recoList = recoList.$children[0]
if (recoList.isOpen === undefined) {
console.log('结构获取失败')
console.log(document.URL)
console.log(recoList)
}
}
return recoList
}
urlChange(async () => {
await playerReady()
const relist: VideoPageCard[] = getVueData(dq('#reco_list')).$children.filter(
const recoList: RecommendList = getRecoList()
const relist: VideoPageCard[] = recoList.$children.filter(
video => video.$el.className.indexOf('special') === -1,
)
showUploadTime(relist)
})
await playerReady()
// 监视推荐列表是否打开,如果打开则更新
childList(dq('#reco_list .rec-list'), async () => {
const recoList: RecommendList = getVueData(dq('#reco_list'))
const recoList: RecommendList = getRecoList()
if (recoList.isOpen) {
const relist: VideoPageCard[] = recoList.$children.filter(
video => video.$el.className.indexOf('special') === -1,

View File

@ -9,7 +9,10 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
'../subtitle-converter'
)
const { playerAgent } = await import('@/components/video/player-agent')
const playerSettingsText = localStorage.getItem('bilibili_player_settings')
const isBpxPlayer = dq('.bpx-player-video-wrap')
const playerSettingsText = isBpxPlayer
? localStorage.getItem('bpx_player_profile')
: localStorage.getItem('bilibili_player_settings')
if (!playerSettingsText) {
return [SubtitleConverter.defaultConfig, '']
}
@ -24,8 +27,11 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
1.6: SubtitleSize.VeryLarge,
}
const size = fontSizeMapping[subtitleSettings.fontsize]
const color = subtitleSettings.color.toString(16)
const opacity = subtitleSettings.backgroundopacity
const color =
typeof subtitleSettings.color === 'number'
? subtitleSettings.color.toString(16)
: parseInt(subtitleSettings.color).toString(16)
const opacity = subtitleSettings.backgroundopacity ?? subtitleSettings.opacity
const positions = {
bc: SubtitleLocation.BottomCenter,
@ -34,6 +40,12 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
tc: SubtitleLocation.TopCenter,
tl: SubtitleLocation.TopLeft,
tr: SubtitleLocation.TopRight,
'bottom-center': SubtitleLocation.BottomCenter,
'bottom-left': SubtitleLocation.BottomLeft,
'bottom-right': SubtitleLocation.BottomRight,
'top-center': SubtitleLocation.TopCenter,
'top-left': SubtitleLocation.TopLeft,
'top-right': SubtitleLocation.TopRight,
}
const subtitleLocation = positions[subtitleSettings.position]
@ -76,7 +88,7 @@ export const getBlobByType = async (
return null
}
const [config, language] = await getSubtitleConfig()
const subtitle = subtitles.find(s => s.language === language) || subtitles[0]
const subtitle = subtitles.find(s => s.languageCode === language) || subtitles[0]
const json = await getJson(subtitle.url)
const rawData = json.body
switch (type) {

View File

@ -1,4 +1,4 @@
import { ComponentMetadata } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { cdnRoots } from '@/core/cdn-types'
import { branches } from '@/core/meta'
import { getComponentsDoc } from './components-doc'
@ -92,7 +92,7 @@ ${getDocText(pluginsDoc.title, pluginsDoc.items)}
}
}
}
export const doc: ComponentMetadata = {
export const doc = defineComponentMetadata({
name: 'featureDocsGenerator',
displayName: '功能文档生成器',
entry,
@ -101,4 +101,4 @@ export const doc: ComponentMetadata = {
delete unsafeWindow.generateDocs
},
tags: [componentsTags.utils],
}
})

View File

@ -1,3 +1,4 @@
import { CustomNavbarOptions } from 'registry/lib/components/style/custom-navbar'
import { PluginMetadata } from '@/plugins/plugin'
import { getComponentSettings } from '@/core/settings'
import type { CustomNavbarItemInit } from '../../../components/style/custom-navbar/custom-navbar-item'
@ -11,7 +12,7 @@ export const plugin: PluginMetadata = {
const defaultLink = 'https://www.bilibili.com/v/channel/'
const name = 'channel'
const isOpenInNewTab = () => {
const { options } = getComponentSettings('customNavbar')
const { options } = getComponentSettings('customNavbar') as { options: CustomNavbarOptions }
if (name in options.openInNewTabOverrides) {
return options.openInNewTabOverrides[name]
}

View File

@ -1,6 +1,5 @@
import { KeyBindingAction } from 'registry/lib/components/utils/keymap/bindings'
import { PluginMetadata } from '@/plugins/plugin'
import { select } from '@/core/spin-query'
export const plugin: PluginMetadata = {
name: 'keymap.actions.toggleDanmakuList',
@ -10,10 +9,10 @@ export const plugin: PluginMetadata = {
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
actions.toggleDanmakuList = {
displayName: '开关弹幕列表',
prevent: true,
run: async () => {
const button = (await select('.bui-collapse-header')) as HTMLDivElement
const button = dq('.bui-collapse-header') as HTMLDivElement
button?.click()
return button
},
}
})

View File

@ -0,0 +1,24 @@
import { KeyBindingAction } from 'registry/lib/components/utils/keymap/bindings'
import { PluginMetadata } from '@/plugins/plugin'
import { playerAgent } from '@/components/video/player-agent'
export const plugin: PluginMetadata = {
name: 'keymap.actions.toggleSubtitle',
displayName: '快捷键扩展 - 开关 CC 字幕',
description: '在快捷键的动作列表里添加一个 "开关 CC 字幕".',
setup: ({ addData }) => {
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
actions.toggleSubtitle = {
displayName: '开关 CC 字幕',
run: async () => {
const button = playerAgent.query.control.buttons.subtitle.sync()
button?.click()
return button
},
}
})
addData('keymap.presets', (presetBase: Record<string, string>) => {
presetBase.toggleSubtitle = 'shift c'
})
},
}

View File

@ -1,5 +1,5 @@
{
"version": "2.5.2",
"version": "2.6.0",
"author": "Grant Howard, Coulomb-G",
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
"license": "MIT",

View File

@ -13,6 +13,7 @@ import {
UpdateCheckItem,
CheckSingleTypeUpdateConfig,
} from './utils'
import { Options } from '.'
export const checkUpdate = async (config: CheckUpdateConfig) => {
const {
@ -137,7 +138,7 @@ export const checkAllUpdate = async (config: CheckSingleTypeUpdateConfig) => {
}
export const silentCheckUpdate = () =>
checkAllUpdate({
maxCount: getComponentSettings(name).options.maxUpdateCount,
maxCount: getComponentSettings<Options>(name).options.maxUpdateCount,
})
export const silentCheckUpdateAndReload = reload(silentCheckUpdate)

View File

@ -58,7 +58,7 @@ const optionsMetadata = defineOptionsMetadata({
},
})
type Options = OptionsOfMetadata<typeof optionsMetadata>
export type Options = OptionsOfMetadata<typeof optionsMetadata>
const entry: ComponentEntry<Options> = async ({ settings: { options: opt } }) => {
if (isIframe()) {

View File

@ -1,7 +1,7 @@
import { addData } from '@/plugins/data'
import { getComponentSettings } from '@/core/settings'
import { languageNameToCode } from '@/core/utils/i18n'
import { Translation, GeneralTranslation, RegexTranslation } from './types'
import { Translation, GeneralTranslation, RegexTranslation, Options as I18nOptions } from './types'
/**
* `plugin.setup`使
@ -22,6 +22,6 @@ export const addI18nData = (
})
}
export const getSelectedLanguage = () => {
const settings = getComponentSettings('i18n')
const settings = getComponentSettings<I18nOptions>('i18n')
return languageNameToCode(settings.options.language)
}

View File

@ -4,6 +4,7 @@ import { getComponentSettings } from '@/core/settings'
import { languageNameToCode } from '@/core/utils/i18n'
import { registerAndGetData } from '@/plugins/data'
import { formData } from '@/core/utils'
import { Options as I18nOptions } from '../types'
export abstract class MachineTranslateProvider {
abstract translate(text: string): Promise<string>
@ -11,7 +12,7 @@ export abstract class MachineTranslateProvider {
abstract link: string
abstract defaultLanguage: string
protected getTargetLanguage() {
const i18n = getComponentSettings('i18n')
const i18n = getComponentSettings<I18nOptions>('i18n')
if (i18n.enabled) {
return languageNameToCode(i18n.options.language)
}
@ -111,7 +112,7 @@ export const translateProviderNames = Object.keys(translateProviders)
export const getTranslator = (): MachineTranslateProvider => {
const {
options: { translator },
} = getComponentSettings('i18n')
} = getComponentSettings<I18nOptions>('i18n')
const provider = translateProviders[translator] || translateProviders.GoogleCN
return provider
}

View File

@ -10,3 +10,7 @@ export type LanguagePack = {
map?: [string, Translation][]
regex?: RegexTranslation
}
export type Options = {
language: string
translator: string
}

View File

@ -1,8 +1,30 @@
import { childList } from '@/core/observer'
import { select } from '@/core/spin-query'
import { matchUrlPattern } from '@/core/utils'
import { matchUrlPattern, raiseEvent } from '@/core/utils'
import { useScopedConsole } from '@/core/utils/log'
import { liveUrls } from '@/core/utils/urls'
/**
* , . ()
*/
export const withControlBar = async (
callback: (controlBar: HTMLElement) => void | Promise<void>,
) => {
const livePlayer = (await select('.live-player-mounter')) as HTMLElement
const console = useScopedConsole('withControlBar')
if (!livePlayer) {
console.warn('livePlayer not found')
return
}
raiseEvent(livePlayer, 'mousemove')
const controlBar = dq(livePlayer, '.web-player-controller-wrap .control-area') as HTMLElement
if (!controlBar) {
console.warn('controlBar not found')
return
}
await callback(controlBar)
raiseEvent(livePlayer, 'mouseleave')
}
/**
* , ,
* (//DOM, )

View File

@ -90,7 +90,7 @@
<script lang="ts">
import { VButton, VIcon, SwitchBox, MiniToast } from '@/ui'
import { visible } from '@/core/observer'
import { ComponentOptions } from '../component'
import { OptionsMetadata } from '../component'
import ComponentDescription from './ComponentDescription.vue'
import ComponentOption from './ComponentOption.vue'
import { componentSettingsMixin } from './mixins'
@ -126,7 +126,7 @@ export default Vue.extend({
},
computed: {
generatedOptions() {
return Object.entries((this.componentData.options ?? {}) as ComponentOptions).filter(
return Object.entries((this.componentData.options ?? {}) as OptionsMetadata).filter(
([, option]) => !option.hidden,
)
},

View File

@ -56,7 +56,7 @@
<script lang="ts">
import { TextBox, SwitchBox, ColorPicker, RangeInput, VDropdown, ImagePicker, VSlider } from '@/ui'
import { getComponentSettings, ComponentSettings } from '@/core/settings'
import { ComponentOption } from '../component'
import { OptionMetadata } from '../component'
import { getDropdownItems } from './dropdown'
import SwitchOptions from '../SwitchOptions.vue'
@ -104,7 +104,7 @@ export default {
},
computed: {
type() {
const option = this.option as ComponentOption
const option = this.option as OptionMetadata
const { defaultValue } = option
// console.log(option)
switch (typeof defaultValue) {

View File

@ -1,5 +1,5 @@
import { none } from '@/core/utils'
import { ComponentEntry, ComponentMetadata } from './component'
import { ComponentEntry, ComponentMetadata, UnknownOptions } from './component'
/**
*
@ -7,7 +7,10 @@ import { ComponentEntry, ComponentMetadata } from './component'
* @param entry
*/
export const styledComponentEntry =
(styleImport: () => Promise<{ default: string }>, entry: ComponentEntry): ComponentEntry =>
<O extends UnknownOptions>(
styleImport: () => Promise<{ default: string }>,
entry: ComponentEntry<O>,
): ComponentEntry<O> =>
async context => {
const { default: style } = await styleImport()
const { addStyle } = await import('@/core/style')
@ -20,11 +23,11 @@ export const styledComponentEntry =
* @param styleImport
* @param entry
*/
export const toggleStyle = (
export const toggleStyle = <O extends UnknownOptions>(
name: string,
styleImport: () => Promise<{ default: string }>,
entry: ComponentEntry = none,
): Pick<ComponentMetadata, 'name' | 'entry' | 'reload' | 'unload'> => {
entry: ComponentEntry<O> = none,
): Pick<ComponentMetadata<O>, 'name' | 'entry' | 'reload' | 'unload'> => {
let styleElement: HTMLStyleElement = null
const styleEntry = async () => {
if (styleElement) {

View File

@ -0,0 +1,74 @@
/*
* Switch Options API, ,
*/
import { getComponentSettings, addComponentListener } from '@/core/settings'
import { ComponentMetadata, OptionsMetadata } from './component'
type Switches = {
[key: string]: {
displayName: string
defaultValue: boolean
}
}
export interface SwitchOptions {
name: string
switches: Switches
radio?: boolean
dimAt?: 'checked' | 'notChecked'
switchProps?: {
checkedIcon?: string
notCheckedIcon?: string
iconPosition?: 'left' | 'right'
}
}
export const createSwitchOptions = (options: SwitchOptions) => {
if (options.radio === undefined) {
options.radio = false
}
const { name: optionName, switches } = options
const extendComponentOptions: OptionsMetadata = {}
Object.entries(switches).forEach(([key, { displayName, defaultValue }]) => {
extendComponentOptions[`switch-${key}`] = {
defaultValue,
displayName,
hidden: true,
}
})
return (component: ComponentMetadata) => {
const optionDisplayName = `${component.displayName}选项`
const selfOption = {
componentName: component.name,
optionDisplayName,
}
Object.assign(options, selfOption)
extendComponentOptions[optionName] = {
defaultValue: options,
displayName: optionDisplayName,
}
component.options = { ...component.options, ...extendComponentOptions }
if (!component.widget) {
component.widget = {
component: () => import('./SwitchOptions.vue').then(m => m.default),
options,
}
}
const originalEntry = component.entry
component.entry = async (...args) => {
originalEntry?.(...args)
const { name } = component
const componentOptions = getComponentSettings(name).options
Object.keys(componentOptions).forEach(key => {
if (key.startsWith('switch-')) {
addComponentListener(
`${name}.${key}`,
(value: boolean) => {
document.body.classList.toggle(`${name}-${key}`, value)
},
true,
)
}
})
}
return component
}
}

View File

@ -1,71 +1,319 @@
import { getComponentSettings, addComponentListener } from '@/core/settings'
import { ComponentMetadata, ComponentOptions } from './component'
/**
* Switch Options API
* @module src/components/switch-options
*
* ComponentMetadata
* Widget Widget
*
* API {@link newSwitchComponentWrapper}
*/
type Switches = {
[key: string]: {
displayName: string
defaultValue: boolean
}
import { getComponentSettings, addComponentListener } from '@/core/settings'
import {
ComponentEntry,
ComponentMetadata,
OptionsMetadata,
OptionsOfMetadata,
UnknownOptions,
} from './component'
import { Widget } from './widget'
/**
*
*/
export type SwitchItemMetadata = {
/** 开关的显示名称 */
displayName: string
/** 开关的默认开启状态 */
defaultValue: boolean
}
export interface SwitchOptions {
name: string
switches: Switches
radio?: boolean
dimAt?: 'checked' | 'notChecked'
/**
*
*
* option
* option 'switch-' `boolean`
*/
export type SwitchItemsMetadata<S extends string> = {
[key in S]: SwitchItemMetadata
}
/**
* SwitchItemsMetadata
*/
export const defineSwitchItemsMetadata = <S extends string>(
c: SwitchItemsMetadata<S>,
): SwitchItemsMetadata<S> => c
/**
* API 使 {@link defineSwitchMetadata}
*/
export interface SwitchMetadata<N extends string, S extends string> {
/**
* `options`
*
* @see {@link SwitchOptions}
*/
name: N
/** 每个开关的单独配置。可用 {@link defineSwitchItemsMetadata} 单独定义 */
switches: SwitchItemsMetadata<S>
/**
* `undefined`
*
* @default false
*/
radio?: undefined | boolean
/**
*
*
* `undefined`:
* `'checked'`:
* `'notChecked'`:
*
* @default undefined
*/
dimAt?: undefined | 'checked' | 'notChecked'
/** 配置每个开关的图标 */
switchProps?: {
checkedIcon?: string
notCheckedIcon?: string
iconPosition?: 'left' | 'right'
}
}
export const createSwitchOptions = (options: SwitchOptions) => {
if (options.radio === undefined) {
options.radio = false
/**
* {@link SwitchMetadata}
*/
export const defineSwitchMetadata = <N extends string, S extends string>(
c: SwitchMetadata<N, S>,
): SwitchMetadata<N, S> => c
/**
* options
*
* {@link SwitchMetadata}
* `SwitchMetadata`
*
* @see {@link SwitchOptions}
*/
export interface SwitchMetadataOption<N extends string, S extends string> {
name: N
switches: SwitchItemsMetadata<S>
radio: boolean
dimAt: undefined | 'checked' | 'notChecked'
switchProps?: {
checkedIcon?: string
notCheckedIcon?: string
iconPosition?: 'left' | 'right'
}
const { name: optionName, switches } = options
const extendComponentOptions: ComponentOptions = {}
Object.entries(switches).forEach(([key, { displayName, defaultValue }]) => {
extendComponentOptions[`switch-${key}`] = {
/** 组件名称 */
componentName: string
/** 组件的 Widget 显示名称 */
optionDisplayName: string
}
/**
* SwitchMetadataOption
*/
const newSwitchMetadataOption = <N extends string, S extends string>(
metadata: SwitchMetadata<N, S>,
componentName: string,
optionDisplayName,
) => ({
...metadata,
radio: metadata.radio === undefined ? false : metadata.radio,
dimAt: metadata.dimAt,
componentName,
optionDisplayName,
})
/**
* `S` 'switch-'
*/
export type SwitchItemNames<S extends string> = `switch-${S}`
/**
* API options
*/
export type SwitchItemOptions<S extends string> = Record<SwitchItemNames<S>, boolean>
/**
* options
*
*
* 1. options
* 2. {@link SwitchMetadata} `name` {@link SwitchMetadataOption}
* 3. {@link SwitchItemsMetadata}
* `SwitchMetadata` `switches`
*/
export type SwitchOptions<O extends UnknownOptions, N extends string, S extends string> = O & {
N: SwitchMetadataOption<N, S>
} & SwitchItemOptions<S>
/**
* SwitchOptions
*/
export type SwitchOptionsOfSwitchMetadata<
O extends UnknownOptions,
C extends SwitchMetadata<string, string>,
> = C extends SwitchMetadata<infer N, infer S> ? SwitchOptions<O, N, S> : never
/**
* SwitchOptions
*/
export type SwitchOptionsOfMetadata<
M extends OptionsMetadata,
C extends SwitchMetadata<string, string>,
> = SwitchOptionsOfSwitchMetadata<OptionsOfMetadata<M>, C>
/**
* OptionsMetadata API options
*/
export type SwitchOptionsMetadata<
O extends UnknownOptions,
N extends string,
S extends string,
> = OptionsMetadata<SwitchOptions<O, N, S>>
/**
* `options` API
*
* `options`
*/
type SwitchOptionsMetadataExtender<S extends string> = <O extends UnknownOptions, N extends string>(
options: OptionsMetadata<O>,
switchMetadataOption: SwitchMetadataOption<N, S>,
) => SwitchOptionsMetadata<O, N, S>
/**
* {@link SwitchOptionsMetadataExtender}
*/
const newSwitchOptionsMetadataExtender = <S extends string>(
itemsMetadata: SwitchItemsMetadata<S>,
): SwitchOptionsMetadataExtender<S> => {
const optionsToExtend = {}
const entries = Object.entries<SwitchItemMetadata>(itemsMetadata)
for (const [key, { displayName, defaultValue }] of entries) {
optionsToExtend[`switch-${key}`] = {
defaultValue,
displayName,
hidden: true,
}
})
return (component: ComponentMetadata) => {
const optionDisplayName = `${component.displayName}选项`
const selfOption = {
componentName: component.name,
optionDisplayName,
}
return <O extends UnknownOptions, N extends string>(
options: OptionsMetadata<O>,
switchMetadataOption: SwitchMetadataOption<N, S>,
) => {
optionsToExtend[switchMetadataOption.name as string] = {
defaultValue: switchMetadataOption,
displayName: switchMetadataOption.optionDisplayName,
}
Object.assign(options, selfOption)
extendComponentOptions[optionName] = {
defaultValue: options,
displayName: optionDisplayName,
}
component.options = { ...component.options, ...extendComponentOptions }
if (!component.widget) {
component.widget = {
component: () => import('./SwitchOptions.vue').then(m => m.default),
options,
}
}
const originalEntry = component.entry
component.entry = async (...args) => {
originalEntry?.(...args)
const { name } = component
const componentOptions = getComponentSettings(name).options
Object.keys(componentOptions).forEach(key => {
if (key.startsWith('switch-')) {
addComponentListener(
`${name}.${key}`,
(value: boolean) => {
document.body.classList.toggle(`${name}-${key}`, value)
},
true,
)
}
})
}
return component
return { ...options, ...optionsToExtend } as SwitchOptionsMetadata<O, N, S>
}
}
/**
* Widget
*/
const newWidget = <N extends string, S extends string>(
options: SwitchMetadataOption<N, S>,
): Omit<Widget, 'name'> => ({
component: () => import('./SwitchOptions.vue').then(m => m.default),
options,
})
/**
* API `options` `entry`
*/
export type SwitchEntry<
O extends UnknownOptions,
N extends string,
S extends string,
T = unknown,
> = ComponentEntry<SwitchOptions<O, N, S>, T>
/**
* `entry`
*/
const newSwitchEntry = <O extends UnknownOptions, N extends string, S extends string>(
component: ComponentMetadata<O>,
): SwitchEntry<O, N, S> => {
const originalEntry = component.entry
return (...args) => {
const result = originalEntry(...args)
const componentOptions = getComponentSettings(component.name).options
Object.keys(componentOptions).forEach(key => {
if (key.startsWith('switch-')) {
addComponentListener(
`${component.name}.${key}`,
(value: boolean) => {
document.body.classList.toggle(`${component.name}-${key}`, value)
},
true,
)
}
})
return result
}
}
/**
* {@link ComponentMetadata}
*/
export type SwitchComponentMetadata<
O extends UnknownOptions,
N extends string,
S extends string,
> = ComponentMetadata<SwitchOptions<O, N, S>>
/**
*
*
* 使 'switch-' option
*
* Widget Widget
* Widget options
*
* `entry`
*
*
*
* @param component -
* @returns
*/
export type SwitchComponentWrapper<N extends string, S extends string> = <O extends UnknownOptions>(
component: ComponentMetadata<O>,
) => SwitchComponentMetadata<O, N, S>
/**
* {@link SwitchComponentWrapper}
*
* 使 {@link defineSwitchMetadata}
*
* @param metadata -
* @returns
*/
export const newSwitchComponentWrapper = <N extends string, S extends string>(
metadata: SwitchMetadata<N, S>,
): SwitchComponentWrapper<N, S> => {
const extendOptions = newSwitchOptionsMetadataExtender(metadata.switches)
// 返回的 wrapper
return <O extends UnknownOptions>(component: ComponentMetadata<O>) => {
const switchMetadataOption = newSwitchMetadataOption(
metadata,
component.name,
`${component.displayName}选项`,
)
// 若没有 Widget则注入一个
if (!component.widget) {
component.widget = newWidget(switchMetadataOption)
}
// 扩展组件 options
component.options = extendOptions(component.options, switchMetadataOption)
// 包装 entry 函数
component.entry = newSwitchEntry(component)
return component as SwitchComponentMetadata<O, N, S>
}
}
export { createSwitchOptions } from './switch-options-old'

View File

@ -43,8 +43,7 @@ export interface ComponentTag {
type ComponentOptionValidator<T> = (value: T, oldValue: T) => T | undefined | null
// TODO: 参考 discussion #3041。当不兼容代码替换完成后将 any 改为 unknown
export type UnknownOptions = Record<string, any>
export type UnknownOptions = Record<string, unknown>
export type EmptyOptions = Record<string, never>
@ -78,10 +77,6 @@ export type OptionsMetadata<O extends UnknownOptions = UnknownOptions> = {
[OptionName in keyof O]: OptionMetadata<O[OptionName]>
}
// TODO: 参考 discussion #3041。当不兼容代码替换完成后删除
export type ComponentOptions = OptionsMetadata
export type ComponentOption = OptionMetadata
/** 组件标签 */
export const componentsTags = {
/** 视频 */

View File

@ -1,480 +0,0 @@
import { select } from '@/core/spin-query'
import { isBwpVideo, raiseEvent } from '@/core/utils'
import { bangumiUrls, matchCurrentPage } from '@/core/utils/urls'
import { bpxPlayerPolyfill } from './player-adaptor/bpx'
import { v3PlayerPolyfill } from './player-adaptor/v3'
/** 元素查询函数, 调用时执行 `SpinQuery.select` 查询, 可访问 `selector` 获取选择器 */
type ElementQuery<Target = HTMLElement> = {
(): Promise<Target | null>
sync: () => Target | null
selector: string
}
interface CustomNestedQuery<QueryResult> {
[key: string]: QueryResult | CustomNestedQuery<QueryResult>
}
interface CustomQuery<QueryResult> extends CustomNestedQuery<QueryResult> {
[key: string]: QueryResult
}
type AgentType = 'video' | 'bangumi'
type CustomQueryProvider<TargetType> = {
[key in AgentType]?: TargetType
} & { video: TargetType }
interface PlayerQuery<QueryResult> extends CustomNestedQuery<QueryResult> {
playerWrap: QueryResult
bilibiliPlayer: QueryResult
playerArea: QueryResult
video: {
element: QueryResult
wrap: QueryResult
top: QueryResult
state: QueryResult
panel: QueryResult
popup: QueryResult
subtitle: QueryResult
basDanmaku: QueryResult
advDanmaku: QueryResult
danmaku: QueryResult
container: QueryResult
}
control: {
element: QueryResult
wrap: QueryResult
mask: QueryResult
top: QueryResult
progress: QueryResult
bottom: QueryResult
bottomLeft: QueryResult
bottomCenter: QueryResult
bottomRight: QueryResult
buttons: {
start: QueryResult
next: QueryResult
time: QueryResult
quality: QueryResult
pageList: QueryResult
speed: QueryResult
subtitle: QueryResult
volume: QueryResult
settings: QueryResult
pip: QueryResult
widescreen: QueryResult
webFullscreen: QueryResult
fullscreen: QueryResult
}
settings: {
wrap: QueryResult
lightOff: QueryResult
}
}
toastWrap: QueryResult
danmakuTipLayer: QueryResult
danmakuSwitch: QueryResult
}
const elementQuery = (selector: string): ElementQuery => {
const func = () => select<HTMLElement>(selector)
func.selector = selector
func.sync = () => dq(selector) as HTMLElement
return func
}
const selectorWrap = (query: CustomNestedQuery<string>): CustomNestedQuery<ElementQuery> => {
const map = (value: string | Record<string, any>): ElementQuery | Record<string, any> => {
if (typeof value !== 'string') {
return lodash.mapValues(value, map)
}
return elementQuery(value)
}
return lodash.mapValues(query, map) as CustomNestedQuery<ElementQuery>
}
const click = (target: ElementQuery) => {
const button = target.sync()
button?.click()
return button
}
export abstract class PlayerAgent {
abstract type: AgentType
abstract query: PlayerQuery<ElementQuery>
provideCustomQuery<CustomQueryType extends CustomQuery<string>>(
config: CustomQueryProvider<CustomQueryType>,
) {
const custom = selectorWrap(config[this.type] ?? config.video) as CustomQuery<ElementQuery>
return {
...this,
custom,
} as this & { custom: { [key in keyof CustomQueryType]: ElementQuery } }
}
widescreen() {
return click(this.query.control.buttons.widescreen)
}
webFullscreen() {
return click(this.query.control.buttons.webFullscreen)
}
fullscreen() {
return click(this.query.control.buttons.fullscreen)
}
togglePlay() {
return click(this.query.control.buttons.start)
}
togglePip() {
return click(this.query.control.buttons.pip)
}
toggleMute() {
return click(this.query.control.buttons.volume)
}
toggleDanmaku() {
const checkbox = this.query.danmakuSwitch.sync() as HTMLInputElement
if (!checkbox) {
return null
}
checkbox.checked = !checkbox.checked
raiseEvent(checkbox, 'change')
return checkbox.checked
}
/** true 开灯false 关灯 */
async toggleLight(on: boolean) {
const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement
// 关灯状态 && 要开灯 -> 开灯
checkbox.checked && on && checkbox.click()
// 开灯状态 && 要关灯 -> 关灯
!checkbox.checked && !on && checkbox.click()
}
// eslint-disable-next-line class-methods-use-this
getPlayerConfig(target: string) {
return lodash.get(JSON.parse(localStorage.getItem('bilibili_player_settings')), target, false)
}
isAutoPlay() {
return this.getPlayerConfig('video_status.autoplay')
}
abstract isMute(): boolean
/** 更改音量 (%) */
abstract changeVolume(change: number): number
/** 跳转到指定时间 */
abstract seek(time: number): number
/** 更改时间 */
abstract changeTime(change: number): number
}
export class VideoPlayerV2Agent extends PlayerAgent {
// eslint-disable-next-line class-methods-use-this
get nativeApi() {
return unsafeWindow.player
}
type: AgentType = 'video'
query = selectorWrap({
playerWrap: '.player-wrap',
bilibiliPlayer: '.bilibili-player',
playerArea: '.bilibili-player-area',
video: {
element: '.bilibili-player-video video',
wrap: '.bilibili-player-video-wrap',
top: '.bilibili-player-video-top',
state: '.bilibili-player-video-state',
panel: '.bilibili-player-video-panel',
popup: '.bilibili-player-video-popup',
subtitle: '.bilibili-player-video-subtitle',
basDanmaku: '.bilibili-player-video-bas-danmaku',
advDanmaku: '.bilibili-player-video-adv-danmaku',
danmaku: '.bilibili-player-video-danmaku',
container: '.bilibili-player-video',
},
control: {
element: '.bilibili-player-control',
wrap: '.bilibili-player-control-wrap',
mask: '.bilibili-player-control-mask',
top: '.bilibili-player-control-top',
progress: '.bilibili-player-video-progress',
bottom: '.bilibili-player-control-bottom',
bottomLeft: '.bilibili-player-control-bottom-left',
bottomCenter: '.bilibili-player-control-bottom-center',
bottomRight: '.bilibili-player-control-bottom-right',
buttons: {
start: '.bilibili-player-video-btn-start',
next: '.bilibili-player-video-btn-next',
time: '.bilibili-player-video-time',
quality: '.bilibili-player-btn-quality',
pageList: '.bilibili-player-video-btn-pagelist',
speed: '.bilibili-player-video-btn-speed',
subtitle: '.bilibili-player-video-btn-subtitle',
volume: '.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume',
settings: '.bilibili-player-video-btn-setting',
pip: '.bilibili-player-video-btn-pip',
widescreen: '.bilibili-player-video-btn-widescreen',
webFullscreen: '.bilibili-player-video-web-fullscreen',
fullscreen: '.bilibili-player-video-btn-fullscreen',
},
settings: {
wrap: '.bilibili-player-video-btn-setting-wrap',
lightOff:
'.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input',
},
},
toastWrap: '.bilibili-player-video-toast-wrp',
danmakuTipLayer: '.bilibili-player-dm-tip-wrap',
danmakuSwitch: '.bilibili-player-video-danmaku-switch input',
}) as PlayerQuery<ElementQuery>
constructor() {
super()
this.checkBwpVideo()
}
checkBwpVideo() {
const videoSelector = this.query.video.element.selector
const bwpSelector = '.bilibili-player-video bwp-video,.bpx-player-video-area bwp-video'
this.query.video.element = (() => {
const func = async () => {
if (await isBwpVideo()) {
return select<HTMLElement>(bwpSelector)
}
return select<HTMLElement>(videoSelector)
}
func.selector = videoSelector
func.sync = () => dq(videoSelector) as HTMLElement
isBwpVideo().then(bwp => {
if (!bwp) {
return
}
func.selector = bwpSelector
func.sync = () => dq(bwpSelector) as HTMLElement
})
return func
})()
}
isMute() {
if (!this.nativeApi) {
return null
}
if (this.nativeApi.isMuted) {
return this.nativeApi.isMuted()
}
return this.nativeApi.isMute()
}
changeVolume(change: number) {
if (!this.nativeApi) {
return null
}
if (this.nativeApi.getVolume) {
const current = this.nativeApi.getVolume()
this.nativeApi.setVolume(current + change / 100)
return Math.round(this.nativeApi.getVolume() * 100)
}
const current = this.nativeApi.volume()
this.nativeApi.volume(current + change / 100)
return Math.round(this.nativeApi.volume() * 100)
}
seek(time: number) {
if (!this.nativeApi) {
return null
}
this.nativeApi.play()
setTimeout(() => {
this.nativeApi.seek(time)
const toastText = dq(
'.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)',
)
if (toastText) {
toastText.textContent = ' 00:00'
}
})
return this.nativeApi.getCurrentTime()
}
changeTime(change: number) {
if (!this.nativeApi) {
return null
}
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
this.nativeApi.seek(video.currentTime + change, video.paused)
return this.nativeApi.getCurrentTime()
}
}
export class BangumiPlayerAgent extends PlayerAgent {
type: AgentType = 'bangumi'
query = selectorWrap({
playerWrap: '.player-module',
bilibiliPlayer: '.bpx-player-container',
playerArea: '.bpx-player-primary-area',
video: {
element: '.bpx-player-video-wrap video',
wrap: '.bpx-player-video-area',
top: '.bpx-player-top-wrap',
state: '.bpx-player-state-wrap',
panel: '.bpx-player-ending-panel',
popup: '.bpx-player-dialog-wrap',
subtitle: '.bpx-player-subtitle-wrap',
basDanmaku: '.bpx-player-bas-dm-wrap',
advDanmaku: '.bpx-player-adv-dm-wrap',
danmaku: '.bpx-player-row-dm-wrap',
container: '.bpx-player-video-wrap',
},
control: {
element: '.squirtle-controller',
wrap: '.bpx-player-control-wrap',
mask: '.bpx-player-control-mask',
top: '.bpx-player-control-top',
progress: '.squirtle-progress-wrap',
bottom: '.squirtle-controller-wrap',
bottomLeft: '.squirtle-controller-wrap-left',
bottomCenter: '.squirtle-controller-wrap-center',
bottomRight: '.squirtle-controller-wrap-right',
buttons: {
start: '.squirtle-video-start',
next: '.squirtle-video-next',
time: '.squirtle-time-wrap',
quality: '.squirtle-video-quality',
pageList: '.squirtle-video-pagelist',
speed: '.squirtle-video-speed',
subtitle: '.squirtle-video-subtitle',
volume: '.squirtle-video-volume .squirtle-volume-icon',
settings: '.squirtle-video-setting',
pip: '.squirtle-video-pip',
widescreen: '.squirtle-video-widescreen',
webFullscreen: '.squirtle-video-pagefullscreen',
fullscreen: '.squirtle-video-fullscreen',
},
settings: {
wrap: '.squirtle-setting-wrap',
lightOff: '.squirtle-lightoff',
},
},
toastWrap: '.bpx-player-tooltip-area',
danmakuTipLayer: '.bpx-player-dialog-wrap',
danmakuSwitch: '.bpx-player-dm-switch input',
}) as PlayerQuery<ElementQuery>
constructor() {
super()
bpxPlayerPolyfill()
}
isMute() {
const icon = this.query.control.buttons.volume.sync() as HTMLElement
return icon?.classList.contains('squirtle-volume-mute-state') ?? false
}
changeVolume(change: number) {
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
video.volume = lodash.clamp(video.volume + change / 100, 0, 1)
return Math.round(video.volume * 100)
}
seek(time: number) {
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
video.play()
setTimeout(() => {
video.currentTime = lodash.clamp(time, 0, video.duration)
const toastText = dq('.bpx-player-toast-row .bpx-player-toast-item .bpx-player-toast-text')
if (toastText?.textContent?.startsWith('已为您定位至')) {
toastText.textContent = '已为您定位至00:00'
}
})
return video.currentTime
}
changeTime(change: number) {
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
video.currentTime = lodash.clamp(video.currentTime + change, 0, video.duration)
return video.currentTime
}
async toggleLight(on: boolean) {
const checkbox = this.query.control.settings.lightOff.sync()
// 开灯状态 && 关灯 -> 关灯
!checkbox.classList.contains('active') && !on && checkbox.click()
// 关灯状态 && 开灯 -> 开灯
checkbox.classList.contains('active') && on && checkbox.click()
}
}
export class VideoPlayerMixedAgent extends VideoPlayerV2Agent {
query = selectorWrap({
playerWrap: '.player-wrap',
bilibiliPlayer: '.bilibili-player, #bilibili-player',
playerArea: '.bilibili-player-area, .bpx-player-primary-area',
video: {
element: '.bilibili-player-video video, .bpx-player-video-wrap video',
wrap: '.bilibili-player-video-wrap, .bpx-player-video-area',
top: '.bilibili-player-video-top, .bpx-player-top-wrap',
state: '.bilibili-player-video-state, .bpx-player-state-wrap',
panel: '.bilibili-player-video-panel, .bpx-player-ending-panel',
popup: '.bilibili-player-video-popup, .bpx-player-dialog-wrap',
subtitle: '.bilibili-player-video-subtitle, .bpx-player-subtitle-wrap',
basDanmaku: '.bilibili-player-video-bas-danmaku, .bpx-player-bas-dm-wrap',
advDanmaku: '.bilibili-player-video-adv-danmaku, .bpx-player-adv-dm-wrap',
danmaku: '.bilibili-player-video-danmaku, .bpx-player-row-dm-wrap',
container: '.bilibili-player-video, .bpx-player-video-perch',
},
control: {
element: '.bilibili-player-control, .bpx-player-control-entity',
wrap: '.bilibili-player-control-wrap, .bpx-player-control-wrap',
mask: '.bilibili-player-control-mask, .bpx-player-control-mask',
top: '.bilibili-player-control-top, .bpx-player-control-top',
progress: '.bilibili-player-video-progress, .bpx-player-progress',
bottom: '.bilibili-player-control-bottom, .bpx-player-control-bottom',
bottomLeft: '.bilibili-player-control-bottom-left, ,.bpx-player-control-bottom-left',
bottomCenter: '.bilibili-player-control-bottom-center, .bpx-player-control-bottom-center',
bottomRight: '.bilibili-player-control-bottom-right, .bpx-player-control-bottom-right',
buttons: {
start: '.bilibili-player-video-btn-start, .bpx-player-ctrl-play',
next: '.bilibili-player-video-btn-next, .bpx-player-ctrl-btn-next',
time: '.bilibili-player-video-time, .bpx-player-ctrl-time',
quality: '.bilibili-player-btn-quality, .bpx-player-ctrl-quality',
pageList: '.bilibili-player-video-btn-pagelist, .bpx-player-ctrl-eplist',
speed: '.bilibili-player-video-btn-speed, .bpx-player-ctrl-playbackrate',
subtitle: '.bilibili-player-video-btn-subtitle, .bpx-player-ctrl-subtitle',
volume:
'.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume, .bpx-player-ctrl-volume .bpx-player-ctrl-volume-icon',
settings: '.bilibili-player-video-btn-setting, .bpx-player-ctrl-setting',
pip: '.bilibili-player-video-btn-pip, .bpx-player-ctrl-pip',
widescreen: '.bilibili-player-video-btn-widescreen, .bpx-player-ctrl-wide',
webFullscreen: '.bilibili-player-video-web-fullscreen, .bpx-player-ctrl-web',
fullscreen: '.bilibili-player-video-btn-fullscreen, .bpx-player-ctrl-full',
},
settings: {
wrap: '.bilibili-player-video-btn-setting-wrap, .bpx-player-ctrl-setting-box',
lightOff:
'.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input, .bpx-player-ctrl-setting-lightoff .bui-checkbox-input',
},
},
toastWrap: '.bilibili-player-video-toast-wrp, .bpx-player-dialog-wrap',
danmakuTipLayer: '.bilibili-player-dm-tip-wrap, .bpx-player-dm-tip',
danmakuSwitch: '.bilibili-player-video-danmaku-switch input, .bpx-player-dm-switch input',
}) as PlayerQuery<ElementQuery>
constructor() {
super()
this.checkBwpVideo()
v3PlayerPolyfill()
}
seek(time: number) {
if (!this.nativeApi) {
return null
}
this.nativeApi.play()
setTimeout(() => {
this.nativeApi.seek(time)
const toastText = dq(
'.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)',
)
if (toastText) {
toastText.textContent = ' 00:00'
}
})
return this.nativeApi.getCurrentTime()
}
}
export const playerAgent = (() => {
if (matchCurrentPage(bangumiUrls)) {
return new BangumiPlayerAgent()
}
return new VideoPlayerMixedAgent()
})()

View File

@ -0,0 +1,104 @@
import { bpxPlayerPolyfill } from '../player-adaptor/bpx'
import { PlayerAgent, selectorWrap } from './base'
import { AgentType, PlayerQuery, ElementQuery } from './types'
export class BangumiPlayerAgent extends PlayerAgent {
type: AgentType = 'bangumi'
query = selectorWrap({
playerWrap: '.player-module',
bilibiliPlayer: '.bpx-player-container',
playerArea: '.bpx-player-primary-area',
video: {
element: '.bpx-player-video-wrap video',
wrap: '.bpx-player-video-area',
top: '.bpx-player-top-wrap',
state: '.bpx-player-state-wrap',
panel: '.bpx-player-ending-panel',
popup: '.bpx-player-dialog-wrap',
subtitle: '.bpx-player-subtitle-wrap',
basDanmaku: '.bpx-player-bas-dm-wrap',
advDanmaku: '.bpx-player-adv-dm-wrap',
danmaku: '.bpx-player-row-dm-wrap',
container: '.bpx-player-video-wrap',
},
control: {
element: '.squirtle-controller',
wrap: '.bpx-player-control-wrap',
mask: '.bpx-player-control-mask',
top: '.bpx-player-control-top',
progress: '.squirtle-progress-wrap',
bottom: '.squirtle-controller-wrap',
bottomLeft: '.squirtle-controller-wrap-left',
bottomCenter: '.squirtle-controller-wrap-center',
bottomRight: '.squirtle-controller-wrap-right',
buttons: {
start: '.squirtle-video-start',
next: '.squirtle-video-next',
time: '.squirtle-time-wrap',
quality: '.squirtle-video-quality',
pageList: '.squirtle-video-pagelist',
speed: '.squirtle-video-speed',
subtitle: '.squirtle-video-subtitle',
volume: '.squirtle-video-volume .squirtle-volume-icon',
settings: '.squirtle-video-setting',
pip: '.squirtle-video-pip',
widescreen: '.squirtle-video-widescreen',
webFullscreen: '.squirtle-video-pagefullscreen',
fullscreen: '.squirtle-video-fullscreen',
},
settings: {
wrap: '.squirtle-setting-wrap',
lightOff: '.squirtle-lightoff',
},
},
toastWrap: '.bpx-player-tooltip-area',
danmakuTipLayer: '.bpx-player-dialog-wrap',
danmakuSwitch: '.bpx-player-dm-switch input',
}) as PlayerQuery<ElementQuery>
constructor() {
super()
bpxPlayerPolyfill()
}
isMute() {
const icon = this.query.control.buttons.volume.sync() as HTMLElement
return icon?.classList.contains('squirtle-volume-mute-state') ?? false
}
changeVolume(change: number) {
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
video.volume = lodash.clamp(video.volume + change / 100, 0, 1)
return Math.round(video.volume * 100)
}
seek(time: number) {
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
video.play()
setTimeout(() => {
video.currentTime = lodash.clamp(time, 0, video.duration)
const toastText = dq('.bpx-player-toast-row .bpx-player-toast-item .bpx-player-toast-text')
if (toastText?.textContent?.startsWith('已为您定位至')) {
toastText.textContent = '已为您定位至00:00'
}
})
return video.currentTime
}
changeTime(change: number) {
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
video.currentTime = lodash.clamp(video.currentTime + change, 0, video.duration)
return video.currentTime
}
async toggleLight(on: boolean) {
const checkbox = this.query.control.settings.lightOff.sync()
// 开灯状态 && 关灯 -> 关灯
!checkbox.classList.contains('active') && !on && checkbox.click()
// 关灯状态 && 开灯 -> 开灯
checkbox.classList.contains('active') && on && checkbox.click()
}
}

View File

@ -0,0 +1,97 @@
import { select } from '@/core/spin-query'
import { raiseEvent } from '@/core/utils'
import {
ElementQuery,
CustomNestedQuery,
AgentType,
PlayerQuery,
CustomQuery,
CustomQueryProvider,
} from './types'
export const elementQuery = (selector: string): ElementQuery => {
const func = () => select<HTMLElement>(selector)
func.selector = selector
func.sync = () => dq(selector) as HTMLElement
return func
}
export const selectorWrap = (query: CustomNestedQuery<string>): CustomNestedQuery<ElementQuery> => {
const map = (value: string | Record<string, any>): ElementQuery | Record<string, any> => {
if (typeof value !== 'string') {
return lodash.mapValues(value, map)
}
return elementQuery(value)
}
return lodash.mapValues(query, map) as CustomNestedQuery<ElementQuery>
}
export const click = (target: ElementQuery) => {
const button = target.sync()
button?.click()
return button
}
export abstract class PlayerAgent {
abstract type: AgentType
abstract query: PlayerQuery<ElementQuery>
provideCustomQuery<CustomQueryType extends CustomQuery<string>>(
config: CustomQueryProvider<CustomQueryType>,
) {
const custom = selectorWrap(config[this.type] ?? config.video) as CustomQuery<ElementQuery>
return {
...this,
custom,
} as this & { custom: { [key in keyof CustomQueryType]: ElementQuery } }
}
widescreen() {
return click(this.query.control.buttons.widescreen)
}
webFullscreen() {
return click(this.query.control.buttons.webFullscreen)
}
fullscreen() {
return click(this.query.control.buttons.fullscreen)
}
togglePlay() {
return click(this.query.control.buttons.start)
}
togglePip() {
return click(this.query.control.buttons.pip)
}
toggleMute() {
return click(this.query.control.buttons.volume)
}
toggleDanmaku() {
const checkbox = this.query.danmakuSwitch.sync() as HTMLInputElement
if (!checkbox) {
return null
}
checkbox.checked = !checkbox.checked
raiseEvent(checkbox, 'change')
return checkbox.checked
}
/** true 开灯false 关灯 */
async toggleLight(on: boolean) {
const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement
// 关灯状态 && 要开灯 -> 开灯
checkbox.checked && on && checkbox.click()
// 开灯状态 && 要关灯 -> 关灯
!checkbox.checked && !on && checkbox.click()
}
// eslint-disable-next-line class-methods-use-this
getPlayerConfig(target: string) {
return lodash.get(JSON.parse(localStorage.getItem('bilibili_player_settings')), target, false)
}
isAutoPlay() {
return this.getPlayerConfig('video_status.autoplay')
}
abstract isMute(): boolean
/** 更改音量 (%) */
abstract changeVolume(change: number): number
/** 跳转到指定时间 */
abstract seek(time: number): number
/** 更改时间 */
abstract changeTime(change: number): number
}

View File

@ -0,0 +1,10 @@
import { bangumiUrls, matchCurrentPage } from '@/core/utils/urls'
import { BangumiPlayerAgent } from './bangumi'
import { VideoPlayerMixedAgent } from './video-player-mixed'
export const playerAgent = (() => {
if (matchCurrentPage(bangumiUrls)) {
return new BangumiPlayerAgent()
}
return new VideoPlayerMixedAgent()
})()

View File

@ -0,0 +1,67 @@
/** 元素查询函数, 调用时执行 `SpinQuery.select` 查询, 可访问 `selector` 获取选择器 */
export type ElementQuery<Target = HTMLElement> = {
(): Promise<Target | null>
sync: () => Target | null
selector: string
}
export interface CustomNestedQuery<QueryResult> {
[key: string]: QueryResult | CustomNestedQuery<QueryResult>
}
export interface CustomQuery<QueryResult> extends CustomNestedQuery<QueryResult> {
[key: string]: QueryResult
}
export type AgentType = 'video' | 'bangumi' | 'live'
export type CustomQueryProvider<TargetType> = {
[key in AgentType]?: TargetType
} & { video: TargetType }
export interface PlayerQuery<QueryResult> extends CustomNestedQuery<QueryResult> {
playerWrap: QueryResult
bilibiliPlayer: QueryResult
playerArea: QueryResult
video: {
element: QueryResult
wrap: QueryResult
top: QueryResult
state: QueryResult
panel: QueryResult
popup: QueryResult
subtitle: QueryResult
basDanmaku: QueryResult
advDanmaku: QueryResult
danmaku: QueryResult
container: QueryResult
}
control: {
element: QueryResult
wrap: QueryResult
mask: QueryResult
top: QueryResult
progress: QueryResult
bottom: QueryResult
bottomLeft: QueryResult
bottomCenter: QueryResult
bottomRight: QueryResult
buttons: {
start: QueryResult
next: QueryResult
time: QueryResult
quality: QueryResult
pageList: QueryResult
speed: QueryResult
subtitle: QueryResult
volume: QueryResult
settings: QueryResult
pip: QueryResult
widescreen: QueryResult
webFullscreen: QueryResult
fullscreen: QueryResult
}
settings: {
wrap: QueryResult
lightOff: QueryResult
}
}
toastWrap: QueryResult
danmakuTipLayer: QueryResult
danmakuSwitch: QueryResult
}

View File

@ -0,0 +1,83 @@
import { v3PlayerPolyfill } from '../player-adaptor/v3'
import { selectorWrap } from './base'
import { PlayerQuery, ElementQuery } from './types'
import { VideoPlayerV2Agent } from './video-player-v2'
export class VideoPlayerMixedAgent extends VideoPlayerV2Agent {
query = selectorWrap({
playerWrap: '.player-wrap',
bilibiliPlayer: '.bilibili-player, #bilibili-player',
playerArea: '.bilibili-player-area, .bpx-player-primary-area',
video: {
element: '.bilibili-player-video video, .bpx-player-video-wrap video',
wrap: '.bilibili-player-video-wrap, .bpx-player-video-area',
top: '.bilibili-player-video-top, .bpx-player-top-wrap',
state: '.bilibili-player-video-state, .bpx-player-state-wrap',
panel: '.bilibili-player-video-panel, .bpx-player-ending-panel',
popup: '.bilibili-player-video-popup, .bpx-player-dialog-wrap',
subtitle: '.bilibili-player-video-subtitle, .bpx-player-subtitle-wrap',
basDanmaku: '.bilibili-player-video-bas-danmaku, .bpx-player-bas-dm-wrap',
advDanmaku: '.bilibili-player-video-adv-danmaku, .bpx-player-adv-dm-wrap',
danmaku: '.bilibili-player-video-danmaku, .bpx-player-row-dm-wrap',
container: '.bilibili-player-video, .bpx-player-video-perch',
},
control: {
element: '.bilibili-player-control, .bpx-player-control-entity',
wrap: '.bilibili-player-control-wrap, .bpx-player-control-wrap',
mask: '.bilibili-player-control-mask, .bpx-player-control-mask',
top: '.bilibili-player-control-top, .bpx-player-control-top',
progress: '.bilibili-player-video-progress, .bpx-player-progress',
bottom: '.bilibili-player-control-bottom, .bpx-player-control-bottom',
bottomLeft: '.bilibili-player-control-bottom-left, ,.bpx-player-control-bottom-left',
bottomCenter: '.bilibili-player-control-bottom-center, .bpx-player-control-bottom-center',
bottomRight: '.bilibili-player-control-bottom-right, .bpx-player-control-bottom-right',
buttons: {
start: '.bilibili-player-video-btn-start, .bpx-player-ctrl-play',
next: '.bilibili-player-video-btn-next, .bpx-player-ctrl-btn-next',
time: '.bilibili-player-video-time, .bpx-player-ctrl-time',
quality: '.bilibili-player-btn-quality, .bpx-player-ctrl-quality',
pageList: '.bilibili-player-video-btn-pagelist, .bpx-player-ctrl-eplist',
speed: '.bilibili-player-video-btn-speed, .bpx-player-ctrl-playbackrate',
subtitle: '.bilibili-player-video-btn-subtitle, .bpx-player-ctrl-subtitle > div > span',
volume:
'.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume, .bpx-player-ctrl-volume .bpx-player-ctrl-volume-icon',
settings: '.bilibili-player-video-btn-setting, .bpx-player-ctrl-setting',
pip: '.bilibili-player-video-btn-pip, .bpx-player-ctrl-pip',
widescreen: '.bilibili-player-video-btn-widescreen, .bpx-player-ctrl-wide',
webFullscreen: '.bilibili-player-video-web-fullscreen, .bpx-player-ctrl-web',
fullscreen: '.bilibili-player-video-btn-fullscreen, .bpx-player-ctrl-full',
},
settings: {
wrap: '.bilibili-player-video-btn-setting-wrap, .bpx-player-ctrl-setting-box',
lightOff:
'.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input, .bpx-player-ctrl-setting-lightoff .bui-checkbox-input',
},
},
toastWrap: '.bilibili-player-video-toast-wrp, .bpx-player-dialog-wrap',
danmakuTipLayer: '.bilibili-player-dm-tip-wrap, .bpx-player-dm-tip',
danmakuSwitch: '.bilibili-player-video-danmaku-switch input, .bpx-player-dm-switch input',
}) as PlayerQuery<ElementQuery>
constructor() {
super()
this.checkBwpVideo()
v3PlayerPolyfill()
}
seek(time: number) {
if (!this.nativeApi) {
return null
}
this.nativeApi.play()
setTimeout(() => {
this.nativeApi.seek(time)
const toastText = dq(
'.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)',
)
if (toastText) {
toastText.textContent = ' 00:00'
}
})
return this.nativeApi.getCurrentTime()
}
}

View File

@ -0,0 +1,141 @@
import { select } from '@/core/spin-query'
import { isBwpVideo } from '@/core/utils'
import { PlayerAgent, selectorWrap } from './base'
import { AgentType, PlayerQuery, ElementQuery } from './types'
export class VideoPlayerV2Agent extends PlayerAgent {
// eslint-disable-next-line class-methods-use-this
get nativeApi() {
return unsafeWindow.player
}
type: AgentType = 'video'
query = selectorWrap({
playerWrap: '.player-wrap',
bilibiliPlayer: '.bilibili-player',
playerArea: '.bilibili-player-area',
video: {
element: '.bilibili-player-video video',
wrap: '.bilibili-player-video-wrap',
top: '.bilibili-player-video-top',
state: '.bilibili-player-video-state',
panel: '.bilibili-player-video-panel',
popup: '.bilibili-player-video-popup',
subtitle: '.bilibili-player-video-subtitle',
basDanmaku: '.bilibili-player-video-bas-danmaku',
advDanmaku: '.bilibili-player-video-adv-danmaku',
danmaku: '.bilibili-player-video-danmaku',
container: '.bilibili-player-video',
},
control: {
element: '.bilibili-player-control',
wrap: '.bilibili-player-control-wrap',
mask: '.bilibili-player-control-mask',
top: '.bilibili-player-control-top',
progress: '.bilibili-player-video-progress',
bottom: '.bilibili-player-control-bottom',
bottomLeft: '.bilibili-player-control-bottom-left',
bottomCenter: '.bilibili-player-control-bottom-center',
bottomRight: '.bilibili-player-control-bottom-right',
buttons: {
start: '.bilibili-player-video-btn-start',
next: '.bilibili-player-video-btn-next',
time: '.bilibili-player-video-time',
quality: '.bilibili-player-btn-quality',
pageList: '.bilibili-player-video-btn-pagelist',
speed: '.bilibili-player-video-btn-speed',
subtitle: '.bilibili-player-video-btn-subtitle',
volume: '.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume',
settings: '.bilibili-player-video-btn-setting',
pip: '.bilibili-player-video-btn-pip',
widescreen: '.bilibili-player-video-btn-widescreen',
webFullscreen: '.bilibili-player-video-web-fullscreen',
fullscreen: '.bilibili-player-video-btn-fullscreen',
},
settings: {
wrap: '.bilibili-player-video-btn-setting-wrap',
lightOff:
'.bilibili-player-video-btn-setting-right-others-content-lightoff .bui-checkbox-input',
},
},
toastWrap: '.bilibili-player-video-toast-wrp',
danmakuTipLayer: '.bilibili-player-dm-tip-wrap',
danmakuSwitch: '.bilibili-player-video-danmaku-switch input',
}) as PlayerQuery<ElementQuery>
constructor() {
super()
this.checkBwpVideo()
}
checkBwpVideo() {
const videoSelector = this.query.video.element.selector
const bwpSelector = '.bilibili-player-video bwp-video,.bpx-player-video-area bwp-video'
this.query.video.element = (() => {
const func = async () => {
if (await isBwpVideo()) {
return select<HTMLElement>(bwpSelector)
}
return select<HTMLElement>(videoSelector)
}
func.selector = videoSelector
func.sync = () => dq(videoSelector) as HTMLElement
isBwpVideo().then(bwp => {
if (!bwp) {
return
}
func.selector = bwpSelector
func.sync = () => dq(bwpSelector) as HTMLElement
})
return func
})()
}
isMute() {
if (!this.nativeApi) {
return null
}
if (this.nativeApi.isMuted) {
return this.nativeApi.isMuted()
}
return this.nativeApi.isMute()
}
changeVolume(change: number) {
if (!this.nativeApi) {
return null
}
if (this.nativeApi.getVolume) {
const current = this.nativeApi.getVolume()
this.nativeApi.setVolume(current + change / 100)
return Math.round(this.nativeApi.getVolume() * 100)
}
const current = this.nativeApi.volume()
this.nativeApi.volume(current + change / 100)
return Math.round(this.nativeApi.volume() * 100)
}
seek(time: number) {
if (!this.nativeApi) {
return null
}
this.nativeApi.play()
setTimeout(() => {
this.nativeApi.seek(time)
const toastText = dq(
'.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)',
)
if (toastText) {
toastText.textContent = ' 00:00'
}
})
return this.nativeApi.getCurrentTime()
}
changeTime(change: number) {
if (!this.nativeApi) {
return null
}
const video = this.query.video.element.sync() as HTMLVideoElement
if (!video) {
return null
}
this.nativeApi.seek(video.currentTime + change, video.paused)
return this.nativeApi.getCurrentTime()
}
}

View File

@ -60,12 +60,11 @@ const emptySettings: ComponentSettings = {
),
}
// TODO: 参考 discussion #3041。
// 当不兼容代码替换完成后将 R 的默认类型替换为 UnknownOptions
/**
*
*
* ComponentSettings
* 使
* ComponentSettings
* ```js
* {
* enabled: false,
@ -75,9 +74,9 @@ const emptySettings: ComponentSettings = {
*
* @param component
*/
export const getComponentSettings = <R extends UnknownOptions = UnknownOptions>(
component: ComponentMetadata | string,
): ComponentSettings<R> => {
export const getComponentSettings = <O extends UnknownOptions>(
component: ComponentMetadata<O> | string,
): ComponentSettings<O> => {
let componentMetadata: ComponentMetadata
if (typeof component === 'string') {
if (componentsMap[component] === undefined) {

View File

@ -91,7 +91,7 @@ export default Vue.extend({
&.left-icon {
.icon-container {
order: -1;
margin: 2px 8px 2px 0;
margin: 2px 6px 2px 0;
}
}
}