mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge branch 'preview-features' into preview
This commit is contained in:
commit
3397e56dc0
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@ -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 的所有更新内容.
|
||||
|
||||
|
||||
@ -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
21
LICENCE
@ -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
1
LICENCE.md
Normal file
@ -0,0 +1 @@
|
||||
见 [开源许可](./README.md#开源许可).
|
||||
18
README.md
18
README.md
@ -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`) 是没关系的.
|
||||
|
||||
# 第三方开源组件
|
||||
👍感谢这些组件帮助我们极大地提升了开发效率.
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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
21
doc/rollback.md
Normal 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`, 并点击查看详情.
|
||||
|
||||

|
||||
|
||||
在详情页中右键 `Raw`, 选择 `Save link as...` (链接另存为), 会下载脚本的内容.
|
||||
|
||||

|
||||
|
||||
将脚本内容粘贴覆盖到油猴中, 并删掉这两行检查更新用的配置.
|
||||
|
||||

|
||||
|
||||
保存后, 进入 b 站, 从脚本设置中关闭 `自动更新器`.
|
||||
|
||||

|
||||
@ -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],
|
||||
}
|
||||
})
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -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\/$/],
|
||||
}
|
||||
})
|
||||
|
||||
@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@ -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\/$/],
|
||||
}
|
||||
})
|
||||
|
||||
@ -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),
|
||||
}
|
||||
})
|
||||
|
||||
@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@ -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],
|
||||
}
|
||||
})
|
||||
|
||||
@ -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 = {
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
}
|
||||
|
||||
@ -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 []
|
||||
}
|
||||
|
||||
@ -186,7 +186,7 @@ export default Vue.extend({
|
||||
.title {
|
||||
margin-left: 6px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
@include semi-bold();
|
||||
}
|
||||
.grow {
|
||||
flex: 1;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 主弹幕的特殊样式, 弹幕内容不会移除.',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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')
|
||||
|
||||
17
registry/lib/components/utils/import-series/index.ts
Normal file
17
registry/lib/components/utils/import-series/index.ts
Normal 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\//],
|
||||
})
|
||||
151
registry/lib/components/utils/import-series/logic.ts
Normal file
151
registry/lib/components/utils/import-series/logic.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
@ -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: '倒退',
|
||||
|
||||
@ -13,7 +13,7 @@ export interface KeyBindingActionContext {
|
||||
}
|
||||
export interface KeyBindingAction {
|
||||
displayName: string
|
||||
run: (context: KeyBindingActionContext) => any
|
||||
run: (context: KeyBindingActionContext) => unknown
|
||||
prevent?: boolean
|
||||
ignoreTyping?: boolean
|
||||
}
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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$
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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
|
||||
/** 固定全局倍速 */
|
||||
|
||||
@ -0,0 +1 @@
|
||||
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.
|
||||
@ -29,9 +29,6 @@ export const component = defineComponentMetadata({
|
||||
],
|
||||
displayName: '删除视频弹窗',
|
||||
tags: [componentsTags.video, componentsTags.style],
|
||||
description: {
|
||||
'zh-CN': '删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.',
|
||||
},
|
||||
urlInclude: playerUrls,
|
||||
options: {
|
||||
votes: {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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],
|
||||
}
|
||||
})
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
24
registry/lib/plugins/utils/keymap-toggle-subtitle/index.ts
Normal file
24
registry/lib/plugins/utils/keymap-toggle-subtitle/index.ts
Normal 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'
|
||||
})
|
||||
},
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -10,3 +10,7 @@ export type LanguagePack = {
|
||||
map?: [string, Translation][]
|
||||
regex?: RegexTranslation
|
||||
}
|
||||
export type Options = {
|
||||
language: string
|
||||
translator: string
|
||||
}
|
||||
|
||||
@ -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, 所以想要额外元素常驻就得用这种方式)
|
||||
|
||||
@ -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,
|
||||
)
|
||||
},
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
74
src/components/switch-options-old.ts
Normal file
74
src/components/switch-options-old.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
|
||||
@ -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 = {
|
||||
/** 视频 */
|
||||
|
||||
@ -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()
|
||||
})()
|
||||
104
src/components/video/player-agent/bangumi.ts
Normal file
104
src/components/video/player-agent/bangumi.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
97
src/components/video/player-agent/base.ts
Normal file
97
src/components/video/player-agent/base.ts
Normal 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
|
||||
}
|
||||
10
src/components/video/player-agent/index.ts
Normal file
10
src/components/video/player-agent/index.ts
Normal 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()
|
||||
})()
|
||||
0
src/components/video/player-agent/live-player.ts
Normal file
0
src/components/video/player-agent/live-player.ts
Normal file
67
src/components/video/player-agent/types.ts
Normal file
67
src/components/video/player-agent/types.ts
Normal 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
|
||||
}
|
||||
83
src/components/video/player-agent/video-player-mixed.ts
Normal file
83
src/components/video/player-agent/video-player-mixed.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
141
src/components/video/player-agent/video-player-v2.ts
Normal file
141
src/components/video/player-agent/video-player-v2.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -91,7 +91,7 @@ export default Vue.extend({
|
||||
&.left-icon {
|
||||
.icon-container {
|
||||
order: -1;
|
||||
margin: 2px 8px 2px 0;
|
||||
margin: 2px 6px 2px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user