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:
|
attributes:
|
||||||
label: 播放器版本
|
label: 播放器版本
|
||||||
description: |
|
description: |
|
||||||
在 b 站播放器上右键可以看到播放器版本号, 大于 3 的别反馈视频相关功能了, 去 [#3187](https://github.com/the1812/Bilibili-Evolved/issues/3187) 里有. 如果是 2, 还要确认下是不是新版播放页, 需要返回旧版才能使用脚本功能. (辨认方法可参考 [#3384](https://github.com/the1812/Bilibili-Evolved/discussions/3384))
|
在 b 站播放器上右键可以看到播放器版本号
|
||||||
placeholder: 请输入
|
placeholder: 请输入
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@ -71,8 +71,8 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: 错误信息
|
label: 错误信息
|
||||||
description: |
|
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">
|
脚本直接报告的错误信息, 或者浏览器开发者工具 (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
|
render: Shell
|
||||||
placeholder: 请输入
|
placeholder: 请输入
|
||||||
validations:
|
validations:
|
||||||
required: true
|
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
|
## v2.5.2-preview
|
||||||
|
`2022-11-19`
|
||||||
|
|
||||||
包含 v2.5.2 的所有更新内容.
|
包含 v2.5.2 的所有更新内容.
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ pnpm install
|
|||||||
// @description Bilibili Evolved (本地)
|
// @description Bilibili Evolved (本地)
|
||||||
// @version 300.0
|
// @version 300.0
|
||||||
// @author Grant Howard, Coulomb-G
|
// @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
|
// @license MIT
|
||||||
// @match *://*.bilibili.com/*
|
// @match *://*.bilibili.com/*
|
||||||
// @exclude *://*.bilibili.com/*/mobile.html
|
// @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 源) 和预览版须翻墙.**
|
- **⚠ 使用正式版 (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) |
|
| [安装](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位 | 金额 |
|
| 时间 | 用户名 | 单号后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.18 18:53:22 | 匿名 | 4381 | ¥3.00 |
|
||||||
| 2022.11.15 17:47:37 | *歌 | 4853 | ¥1.00 |
|
| 2022.11.15 17:47:37 | *歌 | 4853 | ¥1.00 |
|
||||||
| 2022.11.08 09:13:48 | 匿名 | 7879 | ¥5.00 |
|
| 2022.11.08 09:13:48 | 匿名 | 7879 | ¥5.00 |
|
||||||
|
|||||||
@ -447,6 +447,14 @@
|
|||||||
"fullRelativePath": "../../registry/dist/components/utils/image-resolution.js",
|
"fullRelativePath": "../../registry/dist/components/utils/image-resolution.js",
|
||||||
"fullAbsolutePath": "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",
|
"type": "component",
|
||||||
"name": "keymap",
|
"name": "keymap",
|
||||||
@ -627,7 +635,7 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "extendVideoSpeed",
|
"name": "extendVideoSpeed",
|
||||||
"displayName": "扩展倍速",
|
"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",
|
"fullRelativePath": "../../registry/dist/components/video/player/extend-speed.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/video/player/extend-speed.js"
|
"fullAbsolutePath": "registry/dist/components/video/player/extend-speed.js"
|
||||||
},
|
},
|
||||||
@ -675,7 +683,7 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "removePlayerPopup",
|
"name": "removePlayerPopup",
|
||||||
"displayName": "删除视频弹窗",
|
"displayName": "删除视频弹窗",
|
||||||
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.",
|
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.\r\n",
|
||||||
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
|
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
|
||||||
"fullAbsolutePath": "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",
|
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-danmaku-list.js",
|
||||||
"fullAbsolutePath": "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",
|
"type": "plugin",
|
||||||
"name": "vLoading.reimu",
|
"name": "vLoading.reimu",
|
||||||
|
|||||||
@ -565,6 +565,17 @@ by [@snowraincloud](https://github.com/snowraincloud)
|
|||||||
|
|
||||||
根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)
|
根据屏幕 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)
|
### [快捷键扩展](../../registry/dist/components/utils/keymap.js)
|
||||||
`keymap`
|
`keymap`
|
||||||
|
|
||||||
@ -809,6 +820,10 @@ by [@JLoeve](https://github.com/LonelySteve)
|
|||||||
|
|
||||||
- `隐藏滚动条`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.
|
- `隐藏滚动条`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.
|
||||||
|
|
||||||
|
- `隐藏移除图标`:如果认为倍速右侧的移除倍速图标有些突兀,可以开启此选项隐藏.
|
||||||
|
|
||||||
|
- `隐藏新增图标`:如果认为顶部的新增倍速图标有些突兀,可以开启此选项隐藏.
|
||||||
|
|
||||||
### **新增倍速**
|
### **新增倍速**
|
||||||
|
|
||||||
开启组件后,在默认情况下,播放器的倍速菜单就会新增 2.5x 和 3.0x 两个倍速选项.
|
开启组件后,在默认情况下,播放器的倍速菜单就会新增 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)
|
### [加载提示 - 灵梦油库里](../../registry/dist/plugins/v-loading/reimu.js)
|
||||||
`vLoading.reimu`
|
`vLoading.reimu`
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "removePlayerPopup",
|
"name": "removePlayerPopup",
|
||||||
"displayName": "删除视频弹窗",
|
"displayName": "删除视频弹窗",
|
||||||
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.",
|
"description": "删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.\r\n",
|
||||||
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
|
"fullRelativePath": "../../registry/dist/components/video/player/remove-popup.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/video/player/remove-popup.js"
|
"fullAbsolutePath": "registry/dist/components/video/player/remove-popup.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -18,8 +18,6 @@ Issues 用于记录 Bug 等发生异常问题的情况, 如果您希望讨论新
|
|||||||
|
|
||||||
Bug 反馈不仅仅是描述您遇到了什么样的 bug, 更重要的是总结一套步骤, 按照这个步骤来就能稳定遇到 bug. (对应模板中的`问题描述`) 其次是一些环境信息, 模板中也已经给出, 通常包括`脚本版本`, `浏览器版本`, `错误信息`等. 有时候我可能还会需要您导出脚本设置.
|
Bug 反馈不仅仅是描述您遇到了什么样的 bug, 更重要的是总结一套步骤, 按照这个步骤来就能稳定遇到 bug. (对应模板中的`问题描述`) 其次是一些环境信息, 模板中也已经给出, 通常包括`脚本版本`, `浏览器版本`, `错误信息`等. 有时候我可能还会需要您导出脚本设置.
|
||||||
|
|
||||||
> 提示: 最好先在预览离线版(Preview Offline)中测试一下, 有些问题可能已经修复了但还未正式发布.
|
|
||||||
|
|
||||||
我们对此的处理方式如下:
|
我们对此的处理方式如下:
|
||||||
1. 对于给出了所有必要信息的 bug 反馈, 我们会尽量尝试重现并修复这个 bug, 优先级高于功能请求.
|
1. 对于给出了所有必要信息的 bug 反馈, 我们会尽量尝试重现并修复这个 bug, 优先级高于功能请求.
|
||||||
2. 信息不全或者无法重现的, 我们有空的话会追问更多信息, 争取转变为上一条的情况; 没空的话可能就没有回复了, 但会留个心眼, 如果我们之后遇到了会再找过来解决的, 当然您主动提供更多信息自然更好.
|
2. 信息不全或者无法重现的, 我们有空的话会追问更多信息, 争取转变为上一条的情况; 没空的话可能就没有回复了, 但会留个心眼, 如果我们之后遇到了会再找过来解决的, 当然您主动提供更多信息自然更好.
|
||||||
@ -37,10 +35,10 @@ Bug 反馈不一定会做出回复, 特别是内容写得非常清晰时, 我就
|
|||||||
附部分标签含义解释:
|
附部分标签含义解释:
|
||||||
> 讨论区的功能对应的标签这里不再列出.
|
> 讨论区的功能对应的标签这里不再列出.
|
||||||
|
|
||||||
- `bug`: 确认为Bug, 将会处理
|
- `bug`: 确认为 Bug, 将会处理
|
||||||
- `maybe-bug`: 可能是Bug, 待测试验证
|
- `maybe-bug`: 可能是 Bug, 待测试验证
|
||||||
- `adaptation`: b站的变化导致脚本未跟进适配
|
- `adaptation`: b 站的变化导致脚本未跟进适配
|
||||||
- `bad-issue`: 不符合以上规范的issue
|
- `bad-issue`: 不符合以上规范的 issue
|
||||||
- `compatibility`: 兼容性问题
|
- `compatibility`: 兼容性问题
|
||||||
- `duplicate`: 重复
|
- `duplicate`: 重复
|
||||||
- `help-wanted`: 没啥解决思路
|
- `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 { FeedsCard } from '@/components/feeds/api'
|
||||||
import { feedsUrls } from '@/core/utils/urls'
|
import { feedsUrls } from '@/core/utils/urls'
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const entry = async () => {
|
|||||||
added: addCopyLinkButton,
|
added: addCopyLinkButton,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'copyFeedsLink',
|
name: 'copyFeedsLink',
|
||||||
displayName: '复制动态链接',
|
displayName: '复制动态链接',
|
||||||
description: {
|
description: {
|
||||||
@ -26,4 +26,4 @@ export const component: ComponentMetadata = {
|
|||||||
entry,
|
entry,
|
||||||
urlInclude: feedsUrls,
|
urlInclude: feedsUrls,
|
||||||
tags: [componentsTags.feeds, componentsTags.utils],
|
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 { FeedsCard, feedsCardTypes } from '@/components/feeds/api'
|
||||||
import { feedsUrls } from '@/core/utils/urls'
|
import { feedsUrls } from '@/core/utils/urls'
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ const entry = async () => {
|
|||||||
added: disableDetails,
|
added: disableDetails,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'disableFeedsDetails',
|
name: 'disableFeedsDetails',
|
||||||
displayName: '禁止跳转动态详情',
|
displayName: '禁止跳转动态详情',
|
||||||
tags: [componentsTags.feeds],
|
tags: [componentsTags.feeds],
|
||||||
@ -103,4 +103,4 @@ export const component: ComponentMetadata = {
|
|||||||
addStyle()
|
addStyle()
|
||||||
enabled = true
|
enabled = true
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ComponentMetadata } from '@/components/types'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import { styledComponentEntry } from '@/components/styled-component'
|
import { styledComponentEntry } from '@/components/styled-component'
|
||||||
|
|
||||||
interface LiveInfo {
|
interface LiveInfo {
|
||||||
@ -69,7 +69,7 @@ const entry = async () => {
|
|||||||
console.log(presented, extend)
|
console.log(presented, extend)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'extendFeedsLive',
|
name: 'extendFeedsLive',
|
||||||
displayName: '直播信息扩充',
|
displayName: '直播信息扩充',
|
||||||
description: {
|
description: {
|
||||||
@ -78,4 +78,4 @@ export const component: ComponentMetadata = {
|
|||||||
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
|
entry: styledComponentEntry(() => import('./extend-feeds-live.scss'), entry),
|
||||||
tags: [componentsTags.feeds, componentsTags.live],
|
tags: [componentsTags.feeds, componentsTags.live],
|
||||||
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
|
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
|
||||||
}
|
})
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
import {
|
||||||
|
defineOptionsMetadata,
|
||||||
|
defineComponentMetadata,
|
||||||
|
OptionsOfMetadata,
|
||||||
|
} from '@/components/define'
|
||||||
import { feedsCardsManager } from '@/components/feeds/api'
|
import { feedsCardsManager } from '@/components/feeds/api'
|
||||||
import { ComponentMetadata, ComponentOptions } from '@/components/types'
|
|
||||||
import { feedsFilterPlugin } from './plugin'
|
import { feedsFilterPlugin } from './plugin'
|
||||||
|
|
||||||
const entry = async () => {
|
const entry = async () => {
|
||||||
@ -21,36 +25,34 @@ const entry = async () => {
|
|||||||
const { mountVueComponent } = await import('@/core/utils')
|
const { mountVueComponent } = await import('@/core/utils')
|
||||||
leftPanel.insertAdjacentElement('afterbegin', mountVueComponent(FeedsFilterCard).$el)
|
leftPanel.insertAdjacentElement('afterbegin', mountVueComponent(FeedsFilterCard).$el)
|
||||||
}
|
}
|
||||||
const options: ComponentOptions = {
|
|
||||||
|
const options = defineOptionsMetadata({
|
||||||
types: {
|
types: {
|
||||||
defaultValue: [],
|
defaultValue: [] as number[],
|
||||||
displayName: '过滤动态类型',
|
displayName: '过滤动态类型',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
patterns: {
|
patterns: {
|
||||||
defaultValue: [],
|
defaultValue: [] as string[],
|
||||||
displayName: '过滤关键词',
|
displayName: '过滤关键词',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
sideCards: {
|
sideCards: {
|
||||||
defaultValue: [],
|
/** FIXME: 虽然代码里按照`number[]`, 但其实存进去的是`string[]`? */
|
||||||
|
defaultValue: [] as number[],
|
||||||
displayName: '过滤侧边栏',
|
displayName: '过滤侧边栏',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
specialTypes: {
|
specialTypes: {
|
||||||
defaultValue: [],
|
defaultValue: [] as number[],
|
||||||
displayName: '过滤特殊动态类型',
|
displayName: '过滤特殊动态类型',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
export type FeedsFilterOptions = {
|
|
||||||
types: number[]
|
export type FeedsFilterOptions = OptionsOfMetadata<typeof options>
|
||||||
patterns: string[]
|
|
||||||
/** FIXME: 虽然代码里按照`number[]`, 但其实存进去的是`string[]`? */
|
export const component = defineComponentMetadata({
|
||||||
sideCards: number[]
|
|
||||||
specialTypes: number[]
|
|
||||||
}
|
|
||||||
export const component: ComponentMetadata = {
|
|
||||||
name: 'feedsFilter',
|
name: 'feedsFilter',
|
||||||
displayName: '动态过滤器',
|
displayName: '动态过滤器',
|
||||||
description: {
|
description: {
|
||||||
@ -66,4 +68,4 @@ export const component: ComponentMetadata = {
|
|||||||
/^https:\/\/t\.bilibili\.com\/$/,
|
/^https:\/\/t\.bilibili\.com\/$/,
|
||||||
],
|
],
|
||||||
plugin: feedsFilterPlugin,
|
plugin: feedsFilterPlugin,
|
||||||
}
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ComponentMetadata } from '@/components/types'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
|
||||||
const id = 'fixed-sidebars-style'
|
const id = 'fixed-sidebars-style'
|
||||||
const entry = async () => {
|
const entry = async () => {
|
||||||
@ -6,7 +6,7 @@ const entry = async () => {
|
|||||||
disableProfilePopup()
|
disableProfilePopup()
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'fixedFeedsSidebars',
|
name: 'fixedFeedsSidebars',
|
||||||
instantStyles: [
|
instantStyles: [
|
||||||
{
|
{
|
||||||
@ -22,4 +22,4 @@ export const component: ComponentMetadata = {
|
|||||||
tags: [componentsTags.feeds],
|
tags: [componentsTags.feeds],
|
||||||
entry,
|
entry,
|
||||||
urlInclude: [/^https:\/\/t\.bilibili\.com\/$/],
|
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 { styledComponentEntry } from '@/components/styled-component'
|
||||||
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
|
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
|
||||||
import { feedsCardsManager } from '@/components/feeds/api'
|
import { feedsCardsManager } from '@/components/feeds/api'
|
||||||
@ -24,7 +24,7 @@ const entry = async () => {
|
|||||||
button.innerHTML = '收起评论'
|
button.innerHTML = '收起评论'
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
clickHandler()
|
clickHandler()
|
||||||
card.scrollIntoView()
|
card.scrollIntoView({ behavior: 'smooth' })
|
||||||
})
|
})
|
||||||
commentBox.insertAdjacentElement('beforeend', button)
|
commentBox.insertAdjacentElement('beforeend', button)
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ const entry = async () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'foldComments',
|
name: 'foldComments',
|
||||||
displayName: '快速收起评论',
|
displayName: '快速收起评论',
|
||||||
description: {
|
description: {
|
||||||
@ -85,4 +85,4 @@ export const component: ComponentMetadata = {
|
|||||||
urlInclude: feedsUrlsWithoutDetail,
|
urlInclude: feedsUrlsWithoutDetail,
|
||||||
tags: [componentsTags.feeds],
|
tags: [componentsTags.feeds],
|
||||||
entry: styledComponentEntry(() => import('./fold-comment.scss'), entry),
|
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 { toggleStyle } from '@/components/styled-component'
|
||||||
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
|
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
...toggleStyle('fullFeedsContent', () => import('./full-content.scss')),
|
...toggleStyle('fullFeedsContent', () => import('./full-content.scss')),
|
||||||
displayName: '展开动态内容',
|
displayName: '展开动态内容',
|
||||||
description: {
|
description: {
|
||||||
@ -10,4 +10,4 @@ export const component: ComponentMetadata = {
|
|||||||
},
|
},
|
||||||
tags: [componentsTags.style, componentsTags.feeds],
|
tags: [componentsTags.style, componentsTags.feeds],
|
||||||
urlInclude: feedsUrlsWithoutDetail,
|
urlInclude: feedsUrlsWithoutDetail,
|
||||||
}
|
})
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import { toggleStyle } from '@/components/styled-component'
|
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')),
|
...toggleStyle('fullFeedsTitle', () => import('./full-feeds-title.scss')),
|
||||||
displayName: '展开动态标题',
|
displayName: '展开动态标题',
|
||||||
description: {
|
description: {
|
||||||
'zh-CN': '在顶栏的视频动态中, 无论标题多长总是完全展开.',
|
'zh-CN': '在顶栏的视频动态中, 无论标题多长总是完全展开.',
|
||||||
},
|
},
|
||||||
tags: [componentsTags.feeds, componentsTags.style],
|
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 { select } from '@/core/spin-query'
|
||||||
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
|
import { feedsUrlsWithoutDetail } from '@/core/utils/urls'
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'unfoldFeeds',
|
name: 'unfoldFeeds',
|
||||||
displayName: '动态反折叠',
|
displayName: '动态反折叠',
|
||||||
tags: [componentsTags.feeds],
|
tags: [componentsTags.feeds],
|
||||||
@ -28,4 +28,4 @@ export const component: ComponentMetadata = {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|||||||
@ -168,6 +168,8 @@ export default Vue.extend({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
.custom-navbar-item {
|
.custom-navbar-item {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -229,7 +231,7 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active .main-content {
|
&.active .main-content {
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -409,7 +409,7 @@ export default Vue.extend({
|
|||||||
.title {
|
.title {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
@include max-line(2);
|
@include max-line(2);
|
||||||
-webkit-box-align: start;
|
-webkit-box-align: start;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@ -25,15 +25,15 @@
|
|||||||
<div class="row-title">过滤:</div>
|
<div class="row-title">过滤:</div>
|
||||||
<div class="type-filters">
|
<div class="type-filters">
|
||||||
<div v-for="t of types" :key="t.name" class="type-filter">
|
<div v-for="t of types" :key="t.name" class="type-filter">
|
||||||
<VButton
|
<RadioButton
|
||||||
round
|
|
||||||
:title="(t.checked ? '不显示' : '显示') + t.displayName"
|
:title="(t.checked ? '不显示' : '显示') + t.displayName"
|
||||||
:class="{ checked: t.checked }"
|
:class="{ checked: t.checked }"
|
||||||
@click="toggleTypeFilter(t)"
|
:checked="t.checked"
|
||||||
|
:disabled="loading"
|
||||||
|
@change="toggleTypeFilter(t)"
|
||||||
>
|
>
|
||||||
<VIcon :icon="t.icon" :size="18"></VIcon>
|
|
||||||
{{ t.displayName }}
|
{{ t.displayName }}
|
||||||
</VButton>
|
</RadioButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -108,14 +108,24 @@
|
|||||||
import { bilibiliApi, getJsonWithCredentials, postTextWithCredentials } from '@/core/ajax'
|
import { bilibiliApi, getJsonWithCredentials, postTextWithCredentials } from '@/core/ajax'
|
||||||
import { formData, getCsrf } from '@/core/utils'
|
import { formData, getCsrf } from '@/core/utils'
|
||||||
import { descendingSort } from '@/core/utils/sort'
|
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 { popperMixin } from '../mixins'
|
||||||
import { types, TypeFilter, HistoryItem, getHistoryItems, group } from './types'
|
import { types, TypeFilter, HistoryItem, getHistoryItems, group, HistoryType } from './types'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
VButton,
|
VButton,
|
||||||
VIcon,
|
VIcon,
|
||||||
|
RadioButton,
|
||||||
TextBox,
|
TextBox,
|
||||||
VLoading,
|
VLoading,
|
||||||
VEmpty,
|
VEmpty,
|
||||||
@ -137,17 +147,12 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
canNextPage() {
|
canNextPage() {
|
||||||
return (
|
return this.search === '' && !this.loading && this.hasMorePage
|
||||||
this.search === '' &&
|
|
||||||
!this.loading &&
|
|
||||||
this.hasMorePage &&
|
|
||||||
this.types.every((t: TypeFilter) => t.checked)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
search: lodash.debounce(function search() {
|
search: lodash.debounce(function search() {
|
||||||
this.updateGroups()
|
this.reloadHistoryItems()
|
||||||
}, 200),
|
}, 200),
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@ -159,11 +164,22 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleTypeFilter(typeFilter: TypeFilter) {
|
toggleTypeFilter(typeFilter: TypeFilter) {
|
||||||
typeFilter.checked = !typeFilter.checked
|
types.forEach(t => (t.checked = t.name === typeFilter.name))
|
||||||
this.updateGroups()
|
this.reloadHistoryItems()
|
||||||
|
},
|
||||||
|
async reloadHistoryItems() {
|
||||||
|
this.viewTime = 0
|
||||||
|
this.hasMorePage = true
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
await this.nextPage()
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
filterFunc(item: HistoryItem) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@ -178,7 +194,10 @@ export default Vue.extend({
|
|||||||
this.groups = group(this.cards.filter(this.filterFunc))
|
this.groups = group(this.cards.filter(this.filterFunc))
|
||||||
},
|
},
|
||||||
async nextPage() {
|
async nextPage() {
|
||||||
const items = await getHistoryItems(this.viewTime)
|
const items = await getHistoryItems(
|
||||||
|
this.viewTime,
|
||||||
|
types.find(t => t.checked),
|
||||||
|
)
|
||||||
const cards: HistoryItem[] = lodash.uniqBy(
|
const cards: HistoryItem[] = lodash.uniqBy(
|
||||||
this.cards.concat(items).sort(descendingSort((item: HistoryItem) => item.viewAt)),
|
this.cards.concat(items).sort(descendingSort((item: HistoryItem) => item.viewAt)),
|
||||||
item => item.id,
|
item => item.id,
|
||||||
@ -188,7 +207,10 @@ export default Vue.extend({
|
|||||||
if (cards.length > 0) {
|
if (cards.length > 0) {
|
||||||
this.viewTime = lodash.last(cards).viewAt
|
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() {
|
async updateHistoryPauseState() {
|
||||||
const result = await bilibiliApi(
|
const result = await bilibiliApi(
|
||||||
@ -223,7 +245,7 @@ export default Vue.extend({
|
|||||||
@import '../popup';
|
@import '../popup';
|
||||||
|
|
||||||
.custom-navbar-history-list {
|
.custom-navbar-history-list {
|
||||||
width: 350px;
|
width: 400px;
|
||||||
@include navbar-popup-height();
|
@include navbar-popup-height();
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -251,23 +273,22 @@ export default Vue.extend({
|
|||||||
margin: 16px 12px 4px 12px;
|
margin: 16px 12px 4px 12px;
|
||||||
.header-row {
|
.header-row {
|
||||||
@include h-stretch(8px);
|
@include h-stretch(8px);
|
||||||
justify-content: space-between;
|
|
||||||
.row-title {
|
.row-title {
|
||||||
@include h-center();
|
@include h-center();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.type-filters {
|
.type-filters {
|
||||||
@include h-center(8px);
|
@include h-center(6px);
|
||||||
.type-filter {
|
.type-filter {
|
||||||
.be-button {
|
.be-button {
|
||||||
padding: 4px 8px 4px 6px;
|
padding: 4px 8px 4px 6px;
|
||||||
color: #8888;
|
// color: #8888;
|
||||||
.be-icon {
|
// .be-icon {
|
||||||
margin-right: 6px;
|
// margin-right: 6px;
|
||||||
}
|
// }
|
||||||
&.checked {
|
// &.checked {
|
||||||
color: inherit;
|
// color: inherit;
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -402,8 +423,8 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
|
@include semi-bold();
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
font-weight: bold;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export const history: CustomNavbarItemInit = {
|
|||||||
active: document.URL.replace(/\?.*$/, '') === href,
|
active: document.URL.replace(/\?.*$/, '') === href,
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
|
||||||
boundingWidth: 350,
|
boundingWidth: 400,
|
||||||
noPopupPadding: true,
|
noPopupPadding: true,
|
||||||
popupContent: () => import('./NavbarHistory.vue').then(m => m.default),
|
popupContent: () => import('./NavbarHistory.vue').then(m => m.default),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { formatDuration } from '@/core/utils/formatters'
|
|||||||
|
|
||||||
/** 历史项目类型, 值为 API 中的 `history.business` */
|
/** 历史项目类型, 值为 API 中的 `history.business` */
|
||||||
export enum HistoryType {
|
export enum HistoryType {
|
||||||
|
All = 'all',
|
||||||
Video = 'archive',
|
Video = 'archive',
|
||||||
Live = 'live',
|
Live = 'live',
|
||||||
Article = 'article',
|
Article = 'article',
|
||||||
@ -48,32 +49,44 @@ export interface TypeFilter {
|
|||||||
displayName: string
|
displayName: string
|
||||||
icon: string
|
icon: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
|
apiType: string
|
||||||
}
|
}
|
||||||
/** 所有的历史记录类型 */
|
/** 所有的历史记录类型 */
|
||||||
export const types = [
|
export const types = [
|
||||||
|
{
|
||||||
|
name: HistoryType.All,
|
||||||
|
displayName: '全部',
|
||||||
|
icon: '',
|
||||||
|
checked: true,
|
||||||
|
apiType: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: HistoryType.Video,
|
name: HistoryType.Video,
|
||||||
displayName: '视频',
|
displayName: '视频',
|
||||||
icon: 'mdi-play-circle-outline',
|
icon: 'mdi-play-circle-outline',
|
||||||
checked: true,
|
checked: false,
|
||||||
|
apiType: 'archive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: HistoryType.Bangumi,
|
name: HistoryType.Bangumi,
|
||||||
displayName: '番剧',
|
displayName: '番剧',
|
||||||
icon: 'mdi-television-classic',
|
icon: 'mdi-television-classic',
|
||||||
checked: true,
|
checked: false,
|
||||||
|
apiType: 'archive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: HistoryType.Live,
|
name: HistoryType.Live,
|
||||||
displayName: '直播',
|
displayName: '直播',
|
||||||
icon: 'mdi-video-wireless-outline',
|
icon: 'mdi-video-wireless-outline',
|
||||||
checked: true,
|
checked: false,
|
||||||
|
apiType: 'live',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: HistoryType.Article,
|
name: HistoryType.Article,
|
||||||
displayName: '专栏',
|
displayName: '专栏',
|
||||||
icon: 'mdi-newspaper-variant-outline',
|
icon: 'mdi-newspaper-variant-outline',
|
||||||
checked: true,
|
checked: false,
|
||||||
|
apiType: 'article',
|
||||||
},
|
},
|
||||||
] as TypeFilter[]
|
] as TypeFilter[]
|
||||||
|
|
||||||
@ -188,12 +201,17 @@ const parseHistoryItem = (item: any): HistoryItem => {
|
|||||||
* 获取指定观看时间之前的一页历史记录, 不指定则返回最新的历史记录
|
* 获取指定观看时间之前的一页历史记录, 不指定则返回最新的历史记录
|
||||||
* @param viewTime 观看时间
|
* @param viewTime 观看时间
|
||||||
*/
|
*/
|
||||||
export const getHistoryItems = async (viewTime?: number) => {
|
export const getHistoryItems = async (viewTime?: number, type?: TypeFilter) => {
|
||||||
let api = 'https://api.bilibili.com/x/web-interface/history/cursor'
|
const api = 'https://api.bilibili.com/x/web-interface/history/cursor'
|
||||||
|
const params = new URLSearchParams()
|
||||||
if (viewTime) {
|
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)) {
|
if (!Array.isArray(list)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,7 +186,7 @@ export default Vue.extend({
|
|||||||
.title {
|
.title {
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
}
|
}
|
||||||
.grow {
|
.grow {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@ -233,7 +233,7 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@ -325,6 +325,8 @@ export default Vue.extend({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
.user-info-panel {
|
.user-info-panel {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -441,7 +443,7 @@ export default Vue.extend({
|
|||||||
.welcome,
|
.welcome,
|
||||||
.name {
|
.name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
margin: 46px 0 16px 0;
|
margin: 46px 0 16px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
@ -495,7 +497,7 @@ export default Vue.extend({
|
|||||||
color: var(--theme-color) !important;
|
color: var(--theme-color) !important;
|
||||||
}
|
}
|
||||||
.stats-number {
|
.stats-number {
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|||||||
@ -326,7 +326,7 @@ export default Vue.extend({
|
|||||||
.title {
|
.title {
|
||||||
grid-area: title;
|
grid-area: title;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
@include semi-bold();
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
|||||||
@ -891,3 +891,6 @@
|
|||||||
.right-area .tip-wrap .icon path[fill="#00AEEC"] {
|
.right-area .tip-wrap .icon path[fill="#00AEEC"] {
|
||||||
@include theme-fill();
|
@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 { LifeCycleEventTypes } from '@/core/life-cycle'
|
||||||
import { darkExcludes } from '../dark-urls'
|
import { darkExcludes } from '../dark-urls'
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component = defineComponentMetadata({
|
||||||
name: 'darkModeFollowSystem',
|
name: 'darkModeFollowSystem',
|
||||||
displayName: '夜间模式跟随系统',
|
displayName: '夜间模式跟随系统',
|
||||||
entry: () => {
|
entry: () => {
|
||||||
@ -30,4 +30,4 @@ export const component: ComponentMetadata = {
|
|||||||
> 注:在某些浏览器 (如 \`Microsoft Edge\`) 中,夜间模式仅会同步浏览器的亮 / 暗主题.
|
> 注:在某些浏览器 (如 \`Microsoft Edge\`) 中,夜间模式仅会同步浏览器的亮 / 暗主题.
|
||||||
`.trim(),
|
`.trim(),
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { ComponentMetadata } from '@/components/types'
|
import {
|
||||||
|
defineComponentMetadata,
|
||||||
|
defineOptionsMetadata,
|
||||||
|
OptionsOfMetadata,
|
||||||
|
} from '@/components/define'
|
||||||
import { fullyLoaded } from '@/core/life-cycle'
|
import { fullyLoaded } from '@/core/life-cycle'
|
||||||
import { ComponentSettings, getComponentSettings } from '@/core/settings'
|
import { ComponentSettings, getComponentSettings } from '@/core/settings'
|
||||||
import { Range } from '@/ui/range'
|
import { Range } from '@/ui/range'
|
||||||
@ -87,34 +91,8 @@ class ScheduleTime {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const checkTime = (settings: ComponentSettings) => {
|
|
||||||
const start = new ScheduleTime(settings.options.range.start)
|
const options = defineOptionsMetadata({
|
||||||
const end = new ScheduleTime(settings.options.range.end)
|
|
||||||
const now = new ScheduleTime()
|
|
||||||
const useDarkMode = now.isInRange(start, end)
|
|
||||||
const darkModeSettings = getComponentSettings('darkMode')
|
|
||||||
if (darkModeSettings.enabled !== useDarkMode) {
|
|
||||||
darkModeSettings.enabled = useDarkMode
|
|
||||||
}
|
|
||||||
let timeout = 0
|
|
||||||
if (useDarkMode) {
|
|
||||||
timeout = ScheduleTime.millisecondsBefore(end)
|
|
||||||
} else {
|
|
||||||
timeout = ScheduleTime.millisecondsBefore(start)
|
|
||||||
}
|
|
||||||
if (timeout !== 0) {
|
|
||||||
setTimeout(() => checkTime(settings), timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const component: ComponentMetadata = {
|
|
||||||
name: 'darkModeSchedule',
|
|
||||||
displayName: '夜间模式计划时段',
|
|
||||||
description:
|
|
||||||
'设置一个使用夜间模式的时间段, 进入 / 离开此时间段时, 会自动开启 / 关闭夜间模式. 结束时间小于起始时间时将视为次日, 如 `18:00` 至 `6:00` 表示晚上 18:00 到次日 6:00. 请勿和 `夜间模式跟随系统` 一同使用.',
|
|
||||||
tags: [componentsTags.style, componentsTags.general],
|
|
||||||
entry: ({ settings }) => fullyLoaded(() => checkTime(settings)),
|
|
||||||
urlExclude: darkExcludes,
|
|
||||||
options: {
|
|
||||||
range: {
|
range: {
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
start: '18:00',
|
start: '18:00',
|
||||||
@ -135,5 +113,37 @@ export const component: ComponentMetadata = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
const useDarkMode = now.isInRange(start, end)
|
||||||
|
const darkModeSettings = getComponentSettings('darkMode')
|
||||||
|
if (darkModeSettings.enabled !== useDarkMode) {
|
||||||
|
darkModeSettings.enabled = useDarkMode
|
||||||
|
}
|
||||||
|
let timeout = 0
|
||||||
|
if (useDarkMode) {
|
||||||
|
timeout = ScheduleTime.millisecondsBefore(end)
|
||||||
|
} else {
|
||||||
|
timeout = ScheduleTime.millisecondsBefore(start)
|
||||||
|
}
|
||||||
|
if (timeout !== 0) {
|
||||||
|
setTimeout(() => checkTime(settings), timeout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'darkModeSchedule',
|
||||||
|
displayName: '夜间模式计划时段',
|
||||||
|
description:
|
||||||
|
'设置一个使用夜间模式的时间段, 进入 / 离开此时间段时, 会自动开启 / 关闭夜间模式. 结束时间小于起始时间时将视为次日, 如 `18:00` 至 `6:00` 表示晚上 18:00 到次日 6:00. 请勿和 `夜间模式跟随系统` 一同使用.',
|
||||||
|
tags: [componentsTags.style, componentsTags.general],
|
||||||
|
entry: ({ settings }) => fullyLoaded(() => checkTime(settings)),
|
||||||
|
urlExclude: darkExcludes,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
|||||||
@ -27,11 +27,11 @@ $prefix: 'simplifyComments-switch';
|
|||||||
margin-bottom: 4px !important;
|
margin-bottom: 4px !important;
|
||||||
max-height: 19.5px !important;
|
max-height: 19.5px !important;
|
||||||
}
|
}
|
||||||
.reply-content {
|
.reply-content-container {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
flex-basis: 100% !important;
|
flex-basis: 100% !important;
|
||||||
}
|
}
|
||||||
.root-reply:not(.reply-content) {
|
.root-reply:not(.reply-content-container) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { createSwitchOptions, SwitchOptions } from '@/components/switch-options'
|
import { newSwitchComponentWrapper, defineSwitchMetadata } from '@/components/switch-options'
|
||||||
import { ComponentMetadata } from '@/components/types'
|
|
||||||
import { addComponentListener, getComponentSettings } from '@/core/settings'
|
import { addComponentListener, getComponentSettings } from '@/core/settings'
|
||||||
import { sq } from '@/core/spin-query'
|
import { sq } from '@/core/spin-query'
|
||||||
import { addStyle } from '@/core/style'
|
import { addStyle } from '@/core/style'
|
||||||
@ -7,7 +7,7 @@ import { getCookieValue, matchUrlPattern } from '@/core/utils'
|
|||||||
import { useScopedConsole } from '@/core/utils/log'
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
import { mainSiteUrls } from '@/core/utils/urls'
|
import { mainSiteUrls } from '@/core/utils/urls'
|
||||||
|
|
||||||
const switchOptions: SwitchOptions = {
|
const switchMetadata = defineSwitchMetadata({
|
||||||
name: 'simplifyOptions',
|
name: 'simplifyOptions',
|
||||||
dimAt: 'checked',
|
dimAt: 'checked',
|
||||||
switchProps: {
|
switchProps: {
|
||||||
@ -44,9 +44,10 @@ const switchOptions: SwitchOptions = {
|
|||||||
displayName: '右侧分区导航(旧)',
|
displayName: '右侧分区导航(旧)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
const console = useScopedConsole('简化首页')
|
const console = useScopedConsole('简化首页')
|
||||||
const metadata: ComponentMetadata = {
|
|
||||||
|
export const component = newSwitchComponentWrapper(switchMetadata)({
|
||||||
name: 'simplifyHome',
|
name: 'simplifyHome',
|
||||||
displayName: '简化首页',
|
displayName: '简化首页',
|
||||||
description: '隐藏原版首页不需要的元素 / 分区.',
|
description: '隐藏原版首页不需要的元素 / 分区.',
|
||||||
@ -58,7 +59,7 @@ const metadata: ComponentMetadata = {
|
|||||||
],
|
],
|
||||||
urlInclude: mainSiteUrls,
|
urlInclude: mainSiteUrls,
|
||||||
tags: [componentsTags.style],
|
tags: [componentsTags.style],
|
||||||
entry: async () => {
|
entry: async ({ metadata }) => {
|
||||||
// 正好是首页时提供首页分区的简化选项
|
// 正好是首页时提供首页分区的简化选项
|
||||||
const isHome = matchUrlPattern('https://www.bilibili.com/')
|
const isHome = matchUrlPattern('https://www.bilibili.com/')
|
||||||
if (!isHome) {
|
if (!isHome) {
|
||||||
@ -144,10 +145,10 @@ const metadata: ComponentMetadata = {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
switchOptions.switches[key] = option
|
switchMetadata.switches[key] = option
|
||||||
generatedSwitches[key] = option
|
generatedSwitches[key] = option
|
||||||
})
|
})
|
||||||
options.simplifyOptions.switches = generatedSwitches
|
;(options.simplifyOptions as any).switches = generatedSwitches
|
||||||
const generatedStyles = Object.keys(generatedOptions)
|
const generatedStyles = Object.keys(generatedOptions)
|
||||||
.map(name =>
|
.map(name =>
|
||||||
`
|
`
|
||||||
@ -160,6 +161,4 @@ const metadata: ComponentMetadata = {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
addStyle(generatedStyles, 'simplify-home-generated')
|
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 { styledComponentEntry } from '@/components/styled-component'
|
||||||
import { liveUrls } from '@/core/utils/urls'
|
import { liveUrls } from '@/core/utils/urls'
|
||||||
|
|
||||||
export const component = createSwitchOptions({
|
export const component = newSwitchComponentWrapper({
|
||||||
name: 'simplifyOptions',
|
name: 'simplifyOptions',
|
||||||
dimAt: 'checked',
|
dimAt: 'checked',
|
||||||
switchProps: {
|
switchProps: {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
OptionsOfMetadata,
|
OptionsOfMetadata,
|
||||||
} from '@/components/define'
|
} from '@/components/define'
|
||||||
import { ComponentEntry } from '@/components/types'
|
import { ComponentEntry } from '@/components/types'
|
||||||
import { toggleStyle } from '@/components/styled-component'
|
|
||||||
import { playerUrls } from '@/core/utils/urls'
|
import { playerUrls } from '@/core/utils/urls'
|
||||||
import { addComponentListener } from '@/core/settings'
|
import { addComponentListener } from '@/core/settings'
|
||||||
|
|
||||||
@ -33,10 +32,18 @@ const entry: ComponentEntry<Options> = ({ metadata, settings }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const name = 'disableSpecialDanmaku'
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
|
name,
|
||||||
|
entry,
|
||||||
displayName: '禁用特殊弹幕样式',
|
displayName: '禁用特殊弹幕样式',
|
||||||
tags: [componentsTags.style],
|
tags: [componentsTags.style],
|
||||||
...toggleStyle('disableSpecialDanmaku', () => import('./special-danmaku.scss'), entry),
|
instantStyles: [
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
style: () => import('./special-danmaku.scss'),
|
||||||
|
},
|
||||||
|
],
|
||||||
urlInclude: playerUrls,
|
urlInclude: playerUrls,
|
||||||
description: {
|
description: {
|
||||||
'zh-CN': '移除高赞弹幕或 UP 主弹幕的特殊样式, 弹幕内容不会移除.',
|
'zh-CN': '移除高赞弹幕或 UP 主弹幕的特殊样式, 弹幕内容不会移除.',
|
||||||
|
|||||||
@ -1,21 +1,30 @@
|
|||||||
body.disable-highlight-danmaku-style .b-danmaku-high {
|
body.disable-highlight-danmaku-style {
|
||||||
|
.bili-dm.bili-high,
|
||||||
|
.b-danmaku-high {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
line-height: 1.125 !important;
|
line-height: 1.125 !important;
|
||||||
|
.bili-high-icon,
|
||||||
.b-danmaku-high-icon {
|
.b-danmaku-high-icon {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
.bili-high-text,
|
||||||
.b-danmaku-high-text {
|
.b-danmaku-high-text {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
text-shadow: inherit;
|
text-shadow: inherit;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
body.disable-up-danmaku-style .b-danmaku-up {
|
body.disable-up-danmaku-style {
|
||||||
|
.bili-dm.bili-up,
|
||||||
|
.b-danmaku-up {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
line-height: 1.125 !important;
|
line-height: 1.125 !important;
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
.bili-up-tip,
|
||||||
.b-danmaku-up-tip {
|
.b-danmaku-up-tip {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export const enableTouchMove = (element: HTMLElement, options?: TouchMoveOptions
|
|||||||
options,
|
options,
|
||||||
'minMoveDistance',
|
'minMoveDistance',
|
||||||
getComponentSettings('touchMiniPlayer').options.touchMoveDistance,
|
getComponentSettings('touchMiniPlayer').options.touchMoveDistance,
|
||||||
)
|
) as number
|
||||||
// const scroll = lodash.get(options, 'scroll', false)
|
// const scroll = lodash.get(options, 'scroll', false)
|
||||||
const touchstart = (e: TouchEvent) => {
|
const touchstart = (e: TouchEvent) => {
|
||||||
if (e.touches.length < 1) {
|
if (e.touches.length < 1) {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const createPosition = (e: TouchEvent, element: HTMLElement) => {
|
|||||||
* - `cancel`: 请求取消调整
|
* - `cancel`: 请求取消调整
|
||||||
*/
|
*/
|
||||||
export class SwipeAction extends EventTarget {
|
export class SwipeAction extends EventTarget {
|
||||||
minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance
|
minSwipeDistance = getComponentSettings('touchPlayerGestures').options.swiperDistance as number
|
||||||
startPosition: Position = null
|
startPosition: Position = null
|
||||||
lastAction: {
|
lastAction: {
|
||||||
type: 'brightness' | 'volume' | 'progress'
|
type: 'brightness' | 'volume' | 'progress'
|
||||||
|
|||||||
@ -6,8 +6,11 @@ import { Toast } from '@/core/toast'
|
|||||||
import { matchUrlPattern, retrieveImageUrl } from '@/core/utils'
|
import { matchUrlPattern, retrieveImageUrl } from '@/core/utils'
|
||||||
import { formatTitle } from '@/core/utils/title'
|
import { formatTitle } from '@/core/utils/title'
|
||||||
import { feedsUrls } from '@/core/utils/urls'
|
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))) {
|
if (!feedsUrls.some(url => matchUrlPattern(url))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,25 @@
|
|||||||
import { defineComponentMetadata } from '@/components/define'
|
import {
|
||||||
|
defineComponentMetadata,
|
||||||
|
defineOptionsMetadata,
|
||||||
|
OptionsOfMetadata,
|
||||||
|
} from '@/components/define'
|
||||||
import { matchUrlPattern } from '@/core/utils'
|
import { matchUrlPattern } from '@/core/utils'
|
||||||
import { columnUrls, feedsUrls } from '@/core/utils/urls'
|
import { columnUrls, feedsUrls } from '@/core/utils/urls'
|
||||||
import { setupFeedImageExporter } from './feed'
|
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({
|
export const component = defineComponentMetadata({
|
||||||
name: 'imageExporter',
|
name: 'imageExporter',
|
||||||
displayName: '图片批量导出',
|
displayName: '图片批量导出',
|
||||||
@ -15,14 +32,5 @@ export const component = defineComponentMetadata({
|
|||||||
component: () => import('./Widget.vue').then(m => m.default),
|
component: () => import('./Widget.vue').then(m => m.default),
|
||||||
},
|
},
|
||||||
urlInclude: [...feedsUrls, ...columnUrls],
|
urlInclude: [...feedsUrls, ...columnUrls],
|
||||||
options: {
|
options,
|
||||||
columnFormat: {
|
|
||||||
defaultValue: '[title][ - n]',
|
|
||||||
displayName: '专栏图片命名格式',
|
|
||||||
},
|
|
||||||
feedFormat: {
|
|
||||||
defaultValue: '[user][ - id][ - n]',
|
|
||||||
displayName: '动态图片命名格式',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,20 @@
|
|||||||
import { defineComponentMetadata } from '@/components/define'
|
import {
|
||||||
|
defineComponentMetadata,
|
||||||
|
defineOptionsMetadata,
|
||||||
|
OptionsOfMetadata,
|
||||||
|
} from '@/components/define'
|
||||||
import { startResolution } from './resolution'
|
import { startResolution } from './resolution'
|
||||||
|
|
||||||
|
const options = defineOptionsMetadata({
|
||||||
|
scale: {
|
||||||
|
displayName: '缩放级别',
|
||||||
|
defaultValue: 'auto',
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Options = OptionsOfMetadata<typeof options>
|
||||||
|
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
name: 'imageResolution',
|
name: 'imageResolution',
|
||||||
displayName: '高分辨率图片',
|
displayName: '高分辨率图片',
|
||||||
@ -11,11 +25,5 @@ export const component = defineComponentMetadata({
|
|||||||
'zh-CN':
|
'zh-CN':
|
||||||
'根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)',
|
'根据屏幕 DPI 请求更高分辨率的图片, 例如 DPI 缩放 200% 则请求 2 倍的分辨率, 加载时间也会相应变长一些. (也会导致某些浏览器里出现图片闪动, 因为本质上是更换了图片源)',
|
||||||
},
|
},
|
||||||
options: {
|
options,
|
||||||
scale: {
|
|
||||||
displayName: '缩放级别',
|
|
||||||
defaultValue: 'auto',
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { styledComponentEntry } from '@/components/styled-component'
|
import { styledComponentEntry } from '@/components/styled-component'
|
||||||
|
import { Options } from '.'
|
||||||
|
|
||||||
const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/
|
const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/
|
||||||
const excludeSelectors = ['#certify-img1', '#certify-img2']
|
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'),
|
() => import('./fix.scss'),
|
||||||
async ({ settings }) => {
|
async ({ settings }) => {
|
||||||
const { allMutations } = await import('@/core/observer')
|
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 { playerAgent } from '@/components/video/player-agent'
|
||||||
import { getComponentSettings } from '@/core/settings'
|
import { getComponentSettings } from '@/core/settings'
|
||||||
import { registerAndGetData } from '@/plugins/data'
|
import { registerAndGetData } from '@/plugins/data'
|
||||||
|
import { Options } from '.'
|
||||||
import { KeyBindingAction, KeyBindingActionContext } from './bindings'
|
import { KeyBindingAction, KeyBindingActionContext } from './bindings'
|
||||||
|
|
||||||
export const clickElement = (target: string | HTMLElement, context: KeyBindingActionContext) => {
|
export const clickElement = (target: string | HTMLElement, context: KeyBindingActionContext) => {
|
||||||
@ -181,11 +182,13 @@ export const builtInActions: Record<string, KeyBindingAction> = {
|
|||||||
},
|
},
|
||||||
longJumpBackward: {
|
longJumpBackward: {
|
||||||
displayName: '长倒退',
|
displayName: '长倒退',
|
||||||
run: () => playerAgent.changeTime(-getComponentSettings('keymap').options.longJumpSeconds),
|
run: () =>
|
||||||
|
playerAgent.changeTime(-getComponentSettings<Options>('keymap').options.longJumpSeconds),
|
||||||
},
|
},
|
||||||
longJumpForward: {
|
longJumpForward: {
|
||||||
displayName: '长前进',
|
displayName: '长前进',
|
||||||
run: () => playerAgent.changeTime(getComponentSettings('keymap').options.longJumpSeconds),
|
run: () =>
|
||||||
|
playerAgent.changeTime(getComponentSettings<Options>('keymap').options.longJumpSeconds),
|
||||||
},
|
},
|
||||||
jumpBackward: {
|
jumpBackward: {
|
||||||
displayName: '倒退',
|
displayName: '倒退',
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export interface KeyBindingActionContext {
|
|||||||
}
|
}
|
||||||
export interface KeyBindingAction {
|
export interface KeyBindingAction {
|
||||||
displayName: string
|
displayName: string
|
||||||
run: (context: KeyBindingActionContext) => any
|
run: (context: KeyBindingActionContext) => unknown
|
||||||
prevent?: boolean
|
prevent?: boolean
|
||||||
ignoreTyping?: boolean
|
ignoreTyping?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,41 @@
|
|||||||
import { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
|
import { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
|
||||||
import { styledComponentEntry } from '@/components/styled-component'
|
import { styledComponentEntry } from '@/components/styled-component'
|
||||||
import { ComponentEntry } from '@/components/types'
|
import {
|
||||||
import { defineComponentMetadata } from '@/components/define'
|
defineComponentMetadata,
|
||||||
|
defineOptionsMetadata,
|
||||||
|
OptionsOfMetadata,
|
||||||
|
} from '@/components/define'
|
||||||
import { addComponentListener } from '@/core/settings'
|
import { addComponentListener } from '@/core/settings'
|
||||||
import { actions } from './actions'
|
import { actions } from './actions'
|
||||||
import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings'
|
import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings'
|
||||||
import { presetBase, presets } from './presets'
|
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
|
let config: KeyBindingConfig = null
|
||||||
const parseBindings = (bindings: Record<string, string>) =>
|
const parseBindings = (bindings: Record<string, string>): KeyBinding[] => {
|
||||||
Object.entries(bindings).map(([actionName, keyString]) => {
|
const parseBinding = (actionName: string, keyString: string) => {
|
||||||
const keys = keyString.split(' ').filter(it => it !== '')
|
const keys = keyString.split(' ').filter(it => it !== '')
|
||||||
return {
|
return { keys, action: actions[actionName] }
|
||||||
keys,
|
}
|
||||||
action: actions[actionName] || none,
|
return Object.entries(bindings).map(([n, k]) => parseBinding(n, k))
|
||||||
} as KeyBinding
|
}
|
||||||
})
|
const entry = styledComponentEntry<Options>(
|
||||||
const entry: ComponentEntry = styledComponentEntry(
|
|
||||||
() => import('./playback-tip.scss'),
|
() => import('./playback-tip.scss'),
|
||||||
async ({ settings }) => {
|
async ({ settings }) => {
|
||||||
const update = () => {
|
const update = () => {
|
||||||
@ -58,22 +77,7 @@ export const component = defineComponentMetadata({
|
|||||||
'zh-CN': '为脚本的功能和 b 站的功能启用键盘快捷键支持, 快捷键列表可在`快捷键设置`中查看和配置.',
|
'zh-CN': '为脚本的功能和 b 站的功能启用键盘快捷键支持, 快捷键列表可在`快捷键设置`中查看和配置.',
|
||||||
},
|
},
|
||||||
extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default),
|
extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default),
|
||||||
options: {
|
options,
|
||||||
longJumpSeconds: {
|
|
||||||
defaultValue: 85,
|
|
||||||
displayName: '长跳跃秒数',
|
|
||||||
},
|
|
||||||
customKeyBindings: {
|
|
||||||
defaultValue: {},
|
|
||||||
displayName: '自定义键位',
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
preset: {
|
|
||||||
defaultValue: 'Default',
|
|
||||||
displayName: '预设',
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugin: {
|
plugin: {
|
||||||
displayName: '快捷键扩展 - 搜索支持',
|
displayName: '快捷键扩展 - 搜索支持',
|
||||||
setup: ({ addData }) => {
|
setup: ({ addData }) => {
|
||||||
|
|||||||
@ -74,6 +74,13 @@ const entry: ComponentEntry = async ({ settings, metadata }) => {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
addComponentListener(
|
||||||
|
`${metadata.name}.showPlaceholder`,
|
||||||
|
(value: boolean) => {
|
||||||
|
document.body.classList.toggle('promotion-show-placeholder', value)
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
name: 'removePromotions',
|
name: 'removePromotions',
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
@import 'common';
|
||||||
|
|
||||||
#slide_ad,
|
#slide_ad,
|
||||||
.v-wrap .vcd,
|
.v-wrap .vcd,
|
||||||
.ad-report,
|
.ad-report,
|
||||||
@ -80,3 +82,25 @@ body.dark .blocked-ads.new {
|
|||||||
body:not(.preserve-feed-goods) .bili-dyn-content__orig__additional:has(.dyn-goods) {
|
body:not(.preserve-feed-goods) .bili-dyn-content__orig__additional:has(.dyn-goods) {
|
||||||
margin: 0 !important;
|
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',
|
'-Arouter',
|
||||||
'vd_source',
|
'vd_source',
|
||||||
'is_story_h5',
|
'is_story_h5',
|
||||||
|
'buvid',
|
||||||
]
|
]
|
||||||
const [blockParams] = registerAndGetData('urlParamsClean.params', builtInBlockParams)
|
const [blockParams] = registerAndGetData('urlParamsClean.params', builtInBlockParams)
|
||||||
const builtInSiteSpecifiedParams = [
|
const builtInSiteSpecifiedParams = [
|
||||||
@ -63,6 +64,14 @@ const entry = async () => {
|
|||||||
match: /\/\/www\.bilibili\.com\/bangumi\//,
|
match: /\/\/www\.bilibili\.com\/bangumi\//,
|
||||||
param: 'theme',
|
param: 'theme',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: /\/\/www\.bilibili\.com\/video\//,
|
||||||
|
param: 'mid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
match: /\/\/www\.bilibili\.com\/video\//,
|
||||||
|
param: 'up_id',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
const [siteSpecifiedParams] = registerAndGetData(
|
const [siteSpecifiedParams] = registerAndGetData(
|
||||||
'urlParamsClean.siteSpecifiedParams',
|
'urlParamsClean.siteSpecifiedParams',
|
||||||
|
|||||||
@ -63,9 +63,8 @@
|
|||||||
<div
|
<div
|
||||||
v-if="selectedOutput && selectedOutput.description"
|
v-if="selectedOutput && selectedOutput.description"
|
||||||
class="download-video-config-description"
|
class="download-video-config-description"
|
||||||
>
|
v-html="selectedOutput.description"
|
||||||
{{ selectedOutput.description }}
|
></div>
|
||||||
</div>
|
|
||||||
<component
|
<component
|
||||||
:is="selectedOutput.component"
|
:is="selectedOutput.component"
|
||||||
v-if="selectedOutput && selectedOutput.component"
|
v-if="selectedOutput && selectedOutput.component"
|
||||||
@ -417,8 +416,11 @@ export default Vue.extend({
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
.download-video-config-description {
|
.download-video-config-description {
|
||||||
opacity: 0.5;
|
color: #888d;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
|
a {
|
||||||
|
color: var(--theme-color-70);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&-footer {
|
&-footer {
|
||||||
@include h-center();
|
@include h-center();
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import { StreamSaverLibrary } from '@/core/runtime-library'
|
|||||||
import { Toast } from '@/core/toast'
|
import { Toast } from '@/core/toast'
|
||||||
import { DownloadVideoOutput } from '../types'
|
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 = {
|
export const streamSaverOutput: DownloadVideoOutput = {
|
||||||
name: 'steamSaver',
|
name: 'steamSaver',
|
||||||
displayName: 'StreamSaver',
|
displayName: 'StreamSaver',
|
||||||
description: '使用 StreamSaver 输出到本地文件, 下载过程中请勿关闭页面.',
|
description: `使用 StreamSaver 输出到本地文件, 下载过程中请勿关闭页面. 注意: ${serviceWorkerMessage}, 否则弹出的链接点击后会没有反应.`,
|
||||||
runAction: async action => {
|
runAction: async action => {
|
||||||
const streamSaver = await StreamSaverLibrary
|
const streamSaver = await StreamSaverLibrary
|
||||||
const fragments = action.infos.flatMap(it => it.titledFragments)
|
const fragments = action.infos.flatMap(it => it.titledFragments)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
body {
|
body {
|
||||||
&.full-episode-title {
|
&.full-episode-title:not(.disable-full-episode-list) {
|
||||||
.base-video-sections-v1 {
|
.base-video-sections-v1 {
|
||||||
.video-episode-card {
|
.video-episode-card {
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
@ -43,7 +43,7 @@ body {
|
|||||||
.video-sections-content-list {
|
.video-sections-content-list {
|
||||||
max-height: unset !important;
|
max-height: unset !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
.video-section-list {
|
.video-section-list:not([style*="height: 0px"]) {
|
||||||
height: auto !important;
|
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 { CoreApis } from '@/core/core-apis'
|
||||||
import { addComponentListener, ComponentSettings } from '@/core/settings'
|
import { addComponentListener, ComponentSettings } from '@/core/settings'
|
||||||
import { logError } from '@/core/utils/log'
|
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 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>(
|
static create: <OO extends Record<string, any> = unknown>(
|
||||||
metadata: Omit<ComponentMetadata, 'entry' | 'reload' | 'unload' | 'options'> & {
|
metadata: Omit<ComponentMetadata, 'entry' | 'reload' | 'unload' | 'options'> & {
|
||||||
options?: { [K in keyof OO]: ComponentOption }
|
options?: { [K in keyof OO]: OptionMetadata }
|
||||||
},
|
},
|
||||||
) => ComponentMetadata
|
) => ComponentMetadata
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,15 @@ export const ERROR_MESSAGE_DURATION = 5000
|
|||||||
/** 计算菜单项 order */
|
/** 计算菜单项 order */
|
||||||
export const calcOrder = (value: number) => ((MAX_BROWSER_SPEED_VALUE - value) * 10000).toString()
|
export const calcOrder = (value: number) => ((MAX_BROWSER_SPEED_VALUE - value) * 10000).toString()
|
||||||
|
|
||||||
export interface Options {
|
export type Options = {
|
||||||
/** 最大菜单高度 */
|
/** 最大菜单高度 */
|
||||||
maxMenuHeight: boolean
|
maxMenuHeight: boolean
|
||||||
/** 隐藏进度条 */
|
/** 隐藏进度条 */
|
||||||
hideScrollbar: boolean
|
hideScrollbar: boolean
|
||||||
|
/** 隐藏移除图标 */
|
||||||
|
hideRemoveBtn: boolean
|
||||||
|
/** 隐藏新增图标 */
|
||||||
|
hideAddBtn: boolean
|
||||||
/** 扩展倍速列表 */
|
/** 扩展倍速列表 */
|
||||||
extendSpeedList: number[]
|
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
|
let nativeSpeedValue = 1
|
||||||
|
|
||||||
playbackRate$
|
playbackRate$
|
||||||
|
|||||||
@ -18,6 +18,10 @@ export const component = ExtendSpeedComponent.create<Options>({
|
|||||||
|
|
||||||
- \`隐藏滚动条\`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.
|
- \`隐藏滚动条\`:如果添加的倍速过多,倍速菜单将出现滚动条,在 Windows 下,若没有安装并启用「细滚动条」组件会显得比较挤,建议开启此选项隐藏滚动条.
|
||||||
|
|
||||||
|
- \`隐藏移除图标\`:如果认为倍速右侧的移除倍速图标有些突兀,可以开启此选项隐藏.
|
||||||
|
|
||||||
|
- \`隐藏新增图标\`:如果认为顶部的新增倍速图标有些突兀,可以开启此选项隐藏.
|
||||||
|
|
||||||
### **新增倍速**
|
### **新增倍速**
|
||||||
|
|
||||||
开启组件后,在默认情况下,播放器的倍速菜单就会新增 2.5x 和 3.0x 两个倍速选项.
|
开启组件后,在默认情况下,播放器的倍速菜单就会新增 2.5x 和 3.0x 两个倍速选项.
|
||||||
@ -47,6 +51,14 @@ export const component = ExtendSpeedComponent.create<Options>({
|
|||||||
displayName: '隐藏滚动条',
|
displayName: '隐藏滚动条',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
hideRemoveBtn: {
|
||||||
|
displayName: '隐藏移除图标',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
hideAddBtn: {
|
||||||
|
displayName: '隐藏新增图标',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
extendSpeedList: {
|
extendSpeedList: {
|
||||||
displayName: '扩展倍速列表',
|
displayName: '扩展倍速列表',
|
||||||
defaultValue: [2.5, 3],
|
defaultValue: [2.5, 3],
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { EntrySpeedComponent, VideoIdObject } from '../common/speed'
|
|||||||
import { NoSuchSpeedMenuItemElementError, SpeedContext } from '../common/speed/context'
|
import { NoSuchSpeedMenuItemElementError, SpeedContext } from '../common/speed/context'
|
||||||
import { formatSpeedText } from '../common/speed/utils'
|
import { formatSpeedText } from '../common/speed/utils'
|
||||||
|
|
||||||
export interface Options {
|
export type Options = {
|
||||||
/** 全局倍速 */
|
/** 全局倍速 */
|
||||||
globalSpeed: number
|
globalSpeed: number
|
||||||
/** 固定全局倍速 */
|
/** 固定全局倍速 */
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.
|
||||||
@ -29,9 +29,6 @@ export const component = defineComponentMetadata({
|
|||||||
],
|
],
|
||||||
displayName: '删除视频弹窗',
|
displayName: '删除视频弹窗',
|
||||||
tags: [componentsTags.video, componentsTags.style],
|
tags: [componentsTags.video, componentsTags.style],
|
||||||
description: {
|
|
||||||
'zh-CN': '删除视频播放器中出现的各种弹窗, 类别可在选项中分别选择.',
|
|
||||||
},
|
|
||||||
urlInclude: playerUrls,
|
urlInclude: playerUrls,
|
||||||
options: {
|
options: {
|
||||||
votes: {
|
votes: {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
body {
|
body {
|
||||||
&.remove-player-popup {
|
&.remove-player-popup {
|
||||||
|
// https://www.bilibili.com/video/BV1V54y1b7yW/
|
||||||
&-combo-likes {
|
&-combo-likes {
|
||||||
|
.bili-guide,
|
||||||
.bilibili-player-video-popup-three,
|
.bilibili-player-video-popup-three,
|
||||||
.bilibili-player-video-popup-three-animate,
|
.bilibili-player-video-popup-three-animate,
|
||||||
.bilibili-player-video-popup-follow,
|
.bilibili-player-video-popup-follow,
|
||||||
@ -12,14 +14,18 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// https://www.bilibili.com/video/BV11h411k72c/
|
||||||
&-related-videos {
|
&-related-videos {
|
||||||
|
.bili-link,
|
||||||
.bilibili-player-video-link,
|
.bilibili-player-video-link,
|
||||||
.bilibili-player-link,
|
.bilibili-player-link,
|
||||||
.bpx-player-link {
|
.bpx-player-link {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// https://www.bilibili.com/video/BV12Y4y1L7SR/
|
||||||
&-votes {
|
&-votes {
|
||||||
|
.bili-vote,
|
||||||
.bilibili-player-video-popup-vote,
|
.bilibili-player-video-popup-vote,
|
||||||
.bpx-player-popup-dm-close,
|
.bpx-player-popup-dm-close,
|
||||||
.bpx-player-popup-dm-shrink,
|
.bpx-player-popup-dm-shrink,
|
||||||
@ -27,7 +33,9 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// https://www.bilibili.com/video/BV1V54y1b7yW/
|
||||||
&-rates {
|
&-rates {
|
||||||
|
.bili-score,
|
||||||
.bilibili-player-score,
|
.bilibili-player-score,
|
||||||
.bpx-player-popup-dm-close,
|
.bpx-player-popup-dm-close,
|
||||||
.bpx-player-score-summary-wrap,
|
.bpx-player-score-summary-wrap,
|
||||||
@ -35,7 +43,9 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// https://www.bilibili.com/video/BV1fe4y1b7NE/
|
||||||
&-reservations {
|
&-reservations {
|
||||||
|
.bili-reserve,
|
||||||
.bpx-player-reserve {
|
.bpx-player-reserve {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { playerReady } from '@/core/utils'
|
|||||||
import { videoUrls } from '@/core/utils/urls'
|
import { videoUrls } from '@/core/utils/urls'
|
||||||
import { getVueData } from '@/components/feeds/api'
|
import { getVueData } from '@/components/feeds/api'
|
||||||
import { VideoInfo } from '@/components/video/video-info'
|
import { VideoInfo } from '@/components/video/video-info'
|
||||||
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
|
|
||||||
interface RecommendList extends Vue {
|
interface RecommendList extends Vue {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@ -15,35 +16,41 @@ interface RecommendList extends Vue {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}[]
|
}[]
|
||||||
$children: VideoPageCard[]
|
$children: (RecommendList & VideoPageCard)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoPageCard extends Vue {
|
interface VideoPageCard extends Vue {
|
||||||
name: string
|
name: string
|
||||||
title: string
|
title: string
|
||||||
|
// 组件添加元素,非b站自有元素
|
||||||
mark: boolean
|
mark: boolean
|
||||||
item: {
|
item: {
|
||||||
aid: string
|
aid: string
|
||||||
ctime: number
|
ctime: number
|
||||||
owner: {
|
owner: {
|
||||||
|
// 组件添加元素,非b站自有元素
|
||||||
mark: boolean
|
mark: boolean
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayName = '显示视频投稿时间'
|
||||||
|
const console = useScopedConsole(displayName)
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
author: { name: 'wisokey', link: 'https://github.com/wisokey' },
|
author: { name: 'wisokey', link: 'https://github.com/wisokey' },
|
||||||
name: 'showUploadTime',
|
name: 'showUploadTime',
|
||||||
displayName: '显示视频投稿时间',
|
displayName,
|
||||||
description: '为视频播放页面的推荐列表中的视频添加显示视频投稿时间.',
|
description: '为视频播放页面的推荐列表中的视频添加显示视频投稿时间.',
|
||||||
tags: [componentsTags.video],
|
tags: [componentsTags.video],
|
||||||
urlInclude: videoUrls,
|
urlInclude: videoUrls,
|
||||||
entry: async () => {
|
entry: async () => {
|
||||||
const showUploadTime = (relist: VideoPageCard[]) => {
|
const showUploadTime = (relist: VideoPageCard[]) => {
|
||||||
relist.forEach(async video => {
|
relist.forEach(async video => {
|
||||||
|
// 确认存放推荐视频列表的List中的元素是否被更新
|
||||||
if (!video.item.owner.mark) {
|
if (!video.item.owner.mark) {
|
||||||
video.item.owner.mark = true
|
video.item.owner.mark = true
|
||||||
|
// 确认推荐视频卡片是否被更新
|
||||||
if (!video.mark) {
|
if (!video.mark) {
|
||||||
video.mark = true
|
video.mark = true
|
||||||
let createTime: Date
|
let createTime: Date
|
||||||
@ -58,22 +65,39 @@ export const component = defineComponentMetadata({
|
|||||||
createTime.getMonth() + 1
|
createTime.getMonth() + 1
|
||||||
}-${createTime.getDate()}`
|
}-${createTime.getDate()}`
|
||||||
}
|
}
|
||||||
|
// 保存生成后的name
|
||||||
video.item.owner.name = video.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 () => {
|
urlChange(async () => {
|
||||||
await playerReady()
|
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,
|
video => video.$el.className.indexOf('special') === -1,
|
||||||
)
|
)
|
||||||
showUploadTime(relist)
|
showUploadTime(relist)
|
||||||
})
|
})
|
||||||
|
|
||||||
await playerReady()
|
await playerReady()
|
||||||
|
// 监视推荐列表是否打开,如果打开则更新
|
||||||
childList(dq('#reco_list .rec-list'), async () => {
|
childList(dq('#reco_list .rec-list'), async () => {
|
||||||
const recoList: RecommendList = getVueData(dq('#reco_list'))
|
const recoList: RecommendList = getRecoList()
|
||||||
if (recoList.isOpen) {
|
if (recoList.isOpen) {
|
||||||
const relist: VideoPageCard[] = recoList.$children.filter(
|
const relist: VideoPageCard[] = recoList.$children.filter(
|
||||||
video => video.$el.className.indexOf('special') === -1,
|
video => video.$el.className.indexOf('special') === -1,
|
||||||
|
|||||||
@ -9,7 +9,10 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
|
|||||||
'../subtitle-converter'
|
'../subtitle-converter'
|
||||||
)
|
)
|
||||||
const { playerAgent } = await import('@/components/video/player-agent')
|
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) {
|
if (!playerSettingsText) {
|
||||||
return [SubtitleConverter.defaultConfig, '']
|
return [SubtitleConverter.defaultConfig, '']
|
||||||
}
|
}
|
||||||
@ -24,8 +27,11 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
|
|||||||
1.6: SubtitleSize.VeryLarge,
|
1.6: SubtitleSize.VeryLarge,
|
||||||
}
|
}
|
||||||
const size = fontSizeMapping[subtitleSettings.fontsize]
|
const size = fontSizeMapping[subtitleSettings.fontsize]
|
||||||
const color = subtitleSettings.color.toString(16)
|
const color =
|
||||||
const opacity = subtitleSettings.backgroundopacity
|
typeof subtitleSettings.color === 'number'
|
||||||
|
? subtitleSettings.color.toString(16)
|
||||||
|
: parseInt(subtitleSettings.color).toString(16)
|
||||||
|
const opacity = subtitleSettings.backgroundopacity ?? subtitleSettings.opacity
|
||||||
|
|
||||||
const positions = {
|
const positions = {
|
||||||
bc: SubtitleLocation.BottomCenter,
|
bc: SubtitleLocation.BottomCenter,
|
||||||
@ -34,6 +40,12 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
|
|||||||
tc: SubtitleLocation.TopCenter,
|
tc: SubtitleLocation.TopCenter,
|
||||||
tl: SubtitleLocation.TopLeft,
|
tl: SubtitleLocation.TopLeft,
|
||||||
tr: SubtitleLocation.TopRight,
|
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]
|
const subtitleLocation = positions[subtitleSettings.position]
|
||||||
|
|
||||||
@ -76,7 +88,7 @@ export const getBlobByType = async (
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const [config, language] = await getSubtitleConfig()
|
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 json = await getJson(subtitle.url)
|
||||||
const rawData = json.body
|
const rawData = json.body
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ComponentMetadata } from '@/components/types'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import { cdnRoots } from '@/core/cdn-types'
|
import { cdnRoots } from '@/core/cdn-types'
|
||||||
import { branches } from '@/core/meta'
|
import { branches } from '@/core/meta'
|
||||||
import { getComponentsDoc } from './components-doc'
|
import { getComponentsDoc } from './components-doc'
|
||||||
@ -92,7 +92,7 @@ ${getDocText(pluginsDoc.title, pluginsDoc.items)}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const doc: ComponentMetadata = {
|
export const doc = defineComponentMetadata({
|
||||||
name: 'featureDocsGenerator',
|
name: 'featureDocsGenerator',
|
||||||
displayName: '功能文档生成器',
|
displayName: '功能文档生成器',
|
||||||
entry,
|
entry,
|
||||||
@ -101,4 +101,4 @@ export const doc: ComponentMetadata = {
|
|||||||
delete unsafeWindow.generateDocs
|
delete unsafeWindow.generateDocs
|
||||||
},
|
},
|
||||||
tags: [componentsTags.utils],
|
tags: [componentsTags.utils],
|
||||||
}
|
})
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { CustomNavbarOptions } from 'registry/lib/components/style/custom-navbar'
|
||||||
import { PluginMetadata } from '@/plugins/plugin'
|
import { PluginMetadata } from '@/plugins/plugin'
|
||||||
import { getComponentSettings } from '@/core/settings'
|
import { getComponentSettings } from '@/core/settings'
|
||||||
import type { CustomNavbarItemInit } from '../../../components/style/custom-navbar/custom-navbar-item'
|
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 defaultLink = 'https://www.bilibili.com/v/channel/'
|
||||||
const name = 'channel'
|
const name = 'channel'
|
||||||
const isOpenInNewTab = () => {
|
const isOpenInNewTab = () => {
|
||||||
const { options } = getComponentSettings('customNavbar')
|
const { options } = getComponentSettings('customNavbar') as { options: CustomNavbarOptions }
|
||||||
if (name in options.openInNewTabOverrides) {
|
if (name in options.openInNewTabOverrides) {
|
||||||
return options.openInNewTabOverrides[name]
|
return options.openInNewTabOverrides[name]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { KeyBindingAction } from 'registry/lib/components/utils/keymap/bindings'
|
import { KeyBindingAction } from 'registry/lib/components/utils/keymap/bindings'
|
||||||
import { PluginMetadata } from '@/plugins/plugin'
|
import { PluginMetadata } from '@/plugins/plugin'
|
||||||
import { select } from '@/core/spin-query'
|
|
||||||
|
|
||||||
export const plugin: PluginMetadata = {
|
export const plugin: PluginMetadata = {
|
||||||
name: 'keymap.actions.toggleDanmakuList',
|
name: 'keymap.actions.toggleDanmakuList',
|
||||||
@ -10,10 +9,10 @@ export const plugin: PluginMetadata = {
|
|||||||
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
|
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
|
||||||
actions.toggleDanmakuList = {
|
actions.toggleDanmakuList = {
|
||||||
displayName: '开关弹幕列表',
|
displayName: '开关弹幕列表',
|
||||||
prevent: true,
|
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const button = (await select('.bui-collapse-header')) as HTMLDivElement
|
const button = dq('.bui-collapse-header') as HTMLDivElement
|
||||||
button?.click()
|
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",
|
"author": "Grant Howard, Coulomb-G",
|
||||||
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
UpdateCheckItem,
|
UpdateCheckItem,
|
||||||
CheckSingleTypeUpdateConfig,
|
CheckSingleTypeUpdateConfig,
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
import { Options } from '.'
|
||||||
|
|
||||||
export const checkUpdate = async (config: CheckUpdateConfig) => {
|
export const checkUpdate = async (config: CheckUpdateConfig) => {
|
||||||
const {
|
const {
|
||||||
@ -137,7 +138,7 @@ export const checkAllUpdate = async (config: CheckSingleTypeUpdateConfig) => {
|
|||||||
}
|
}
|
||||||
export const silentCheckUpdate = () =>
|
export const silentCheckUpdate = () =>
|
||||||
checkAllUpdate({
|
checkAllUpdate({
|
||||||
maxCount: getComponentSettings(name).options.maxUpdateCount,
|
maxCount: getComponentSettings<Options>(name).options.maxUpdateCount,
|
||||||
})
|
})
|
||||||
export const silentCheckUpdateAndReload = reload(silentCheckUpdate)
|
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 } }) => {
|
const entry: ComponentEntry<Options> = async ({ settings: { options: opt } }) => {
|
||||||
if (isIframe()) {
|
if (isIframe()) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { addData } from '@/plugins/data'
|
import { addData } from '@/plugins/data'
|
||||||
import { getComponentSettings } from '@/core/settings'
|
import { getComponentSettings } from '@/core/settings'
|
||||||
import { languageNameToCode } from '@/core/utils/i18n'
|
import { languageNameToCode } from '@/core/utils/i18n'
|
||||||
import { Translation, GeneralTranslation, RegexTranslation } from './types'
|
import { Translation, GeneralTranslation, RegexTranslation, Options as I18nOptions } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在`plugin.setup`中可使用此帮助函数快速注入翻译数据
|
* 在`plugin.setup`中可使用此帮助函数快速注入翻译数据
|
||||||
@ -22,6 +22,6 @@ export const addI18nData = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
export const getSelectedLanguage = () => {
|
export const getSelectedLanguage = () => {
|
||||||
const settings = getComponentSettings('i18n')
|
const settings = getComponentSettings<I18nOptions>('i18n')
|
||||||
return languageNameToCode(settings.options.language)
|
return languageNameToCode(settings.options.language)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { getComponentSettings } from '@/core/settings'
|
|||||||
import { languageNameToCode } from '@/core/utils/i18n'
|
import { languageNameToCode } from '@/core/utils/i18n'
|
||||||
import { registerAndGetData } from '@/plugins/data'
|
import { registerAndGetData } from '@/plugins/data'
|
||||||
import { formData } from '@/core/utils'
|
import { formData } from '@/core/utils'
|
||||||
|
import { Options as I18nOptions } from '../types'
|
||||||
|
|
||||||
export abstract class MachineTranslateProvider {
|
export abstract class MachineTranslateProvider {
|
||||||
abstract translate(text: string): Promise<string>
|
abstract translate(text: string): Promise<string>
|
||||||
@ -11,7 +12,7 @@ export abstract class MachineTranslateProvider {
|
|||||||
abstract link: string
|
abstract link: string
|
||||||
abstract defaultLanguage: string
|
abstract defaultLanguage: string
|
||||||
protected getTargetLanguage() {
|
protected getTargetLanguage() {
|
||||||
const i18n = getComponentSettings('i18n')
|
const i18n = getComponentSettings<I18nOptions>('i18n')
|
||||||
if (i18n.enabled) {
|
if (i18n.enabled) {
|
||||||
return languageNameToCode(i18n.options.language)
|
return languageNameToCode(i18n.options.language)
|
||||||
}
|
}
|
||||||
@ -111,7 +112,7 @@ export const translateProviderNames = Object.keys(translateProviders)
|
|||||||
export const getTranslator = (): MachineTranslateProvider => {
|
export const getTranslator = (): MachineTranslateProvider => {
|
||||||
const {
|
const {
|
||||||
options: { translator },
|
options: { translator },
|
||||||
} = getComponentSettings('i18n')
|
} = getComponentSettings<I18nOptions>('i18n')
|
||||||
const provider = translateProviders[translator] || translateProviders.GoogleCN
|
const provider = translateProviders[translator] || translateProviders.GoogleCN
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,3 +10,7 @@ export type LanguagePack = {
|
|||||||
map?: [string, Translation][]
|
map?: [string, Translation][]
|
||||||
regex?: RegexTranslation
|
regex?: RegexTranslation
|
||||||
}
|
}
|
||||||
|
export type Options = {
|
||||||
|
language: string
|
||||||
|
translator: string
|
||||||
|
}
|
||||||
|
|||||||
@ -1,8 +1,30 @@
|
|||||||
import { childList } from '@/core/observer'
|
import { childList } from '@/core/observer'
|
||||||
import { select } from '@/core/spin-query'
|
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'
|
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, 所以想要额外元素常驻就得用这种方式)
|
* (鼠标进入/移出会创建/销毁控制栏的DOM, 所以想要额外元素常驻就得用这种方式)
|
||||||
|
|||||||
@ -90,7 +90,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { VButton, VIcon, SwitchBox, MiniToast } from '@/ui'
|
import { VButton, VIcon, SwitchBox, MiniToast } from '@/ui'
|
||||||
import { visible } from '@/core/observer'
|
import { visible } from '@/core/observer'
|
||||||
import { ComponentOptions } from '../component'
|
import { OptionsMetadata } from '../component'
|
||||||
import ComponentDescription from './ComponentDescription.vue'
|
import ComponentDescription from './ComponentDescription.vue'
|
||||||
import ComponentOption from './ComponentOption.vue'
|
import ComponentOption from './ComponentOption.vue'
|
||||||
import { componentSettingsMixin } from './mixins'
|
import { componentSettingsMixin } from './mixins'
|
||||||
@ -126,7 +126,7 @@ export default Vue.extend({
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
generatedOptions() {
|
generatedOptions() {
|
||||||
return Object.entries((this.componentData.options ?? {}) as ComponentOptions).filter(
|
return Object.entries((this.componentData.options ?? {}) as OptionsMetadata).filter(
|
||||||
([, option]) => !option.hidden,
|
([, option]) => !option.hidden,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -56,7 +56,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TextBox, SwitchBox, ColorPicker, RangeInput, VDropdown, ImagePicker, VSlider } from '@/ui'
|
import { TextBox, SwitchBox, ColorPicker, RangeInput, VDropdown, ImagePicker, VSlider } from '@/ui'
|
||||||
import { getComponentSettings, ComponentSettings } from '@/core/settings'
|
import { getComponentSettings, ComponentSettings } from '@/core/settings'
|
||||||
import { ComponentOption } from '../component'
|
import { OptionMetadata } from '../component'
|
||||||
import { getDropdownItems } from './dropdown'
|
import { getDropdownItems } from './dropdown'
|
||||||
import SwitchOptions from '../SwitchOptions.vue'
|
import SwitchOptions from '../SwitchOptions.vue'
|
||||||
|
|
||||||
@ -104,7 +104,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
type() {
|
type() {
|
||||||
const option = this.option as ComponentOption
|
const option = this.option as OptionMetadata
|
||||||
const { defaultValue } = option
|
const { defaultValue } = option
|
||||||
// console.log(option)
|
// console.log(option)
|
||||||
switch (typeof defaultValue) {
|
switch (typeof defaultValue) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { none } from '@/core/utils'
|
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 组件入口函数
|
* @param entry 组件入口函数
|
||||||
*/
|
*/
|
||||||
export const styledComponentEntry =
|
export const styledComponentEntry =
|
||||||
(styleImport: () => Promise<{ default: string }>, entry: ComponentEntry): ComponentEntry =>
|
<O extends UnknownOptions>(
|
||||||
|
styleImport: () => Promise<{ default: string }>,
|
||||||
|
entry: ComponentEntry<O>,
|
||||||
|
): ComponentEntry<O> =>
|
||||||
async context => {
|
async context => {
|
||||||
const { default: style } = await styleImport()
|
const { default: style } = await styleImport()
|
||||||
const { addStyle } = await import('@/core/style')
|
const { addStyle } = await import('@/core/style')
|
||||||
@ -20,11 +23,11 @@ export const styledComponentEntry =
|
|||||||
* @param styleImport 动态导入样式的函数
|
* @param styleImport 动态导入样式的函数
|
||||||
* @param entry 组件入口函数
|
* @param entry 组件入口函数
|
||||||
*/
|
*/
|
||||||
export const toggleStyle = (
|
export const toggleStyle = <O extends UnknownOptions>(
|
||||||
name: string,
|
name: string,
|
||||||
styleImport: () => Promise<{ default: string }>,
|
styleImport: () => Promise<{ default: string }>,
|
||||||
entry: ComponentEntry = none,
|
entry: ComponentEntry<O> = none,
|
||||||
): Pick<ComponentMetadata, 'name' | 'entry' | 'reload' | 'unload'> => {
|
): Pick<ComponentMetadata<O>, 'name' | 'entry' | 'reload' | 'unload'> => {
|
||||||
let styleElement: HTMLStyleElement = null
|
let styleElement: HTMLStyleElement = null
|
||||||
const styleEntry = async () => {
|
const styleEntry = async () => {
|
||||||
if (styleElement) {
|
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 = {
|
import { getComponentSettings, addComponentListener } from '@/core/settings'
|
||||||
[key: string]: {
|
import {
|
||||||
|
ComponentEntry,
|
||||||
|
ComponentMetadata,
|
||||||
|
OptionsMetadata,
|
||||||
|
OptionsOfMetadata,
|
||||||
|
UnknownOptions,
|
||||||
|
} from './component'
|
||||||
|
import { Widget } from './widget'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个开关的设置
|
||||||
|
*/
|
||||||
|
export type SwitchItemMetadata = {
|
||||||
|
/** 开关的显示名称 */
|
||||||
displayName: string
|
displayName: string
|
||||||
|
/** 开关的默认开启状态 */
|
||||||
defaultValue: boolean
|
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?: {
|
switchProps?: {
|
||||||
checkedIcon?: string
|
checkedIcon?: string
|
||||||
notCheckedIcon?: string
|
notCheckedIcon?: string
|
||||||
iconPosition?: 'left' | 'right'
|
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 = {}
|
componentName: string
|
||||||
Object.entries(switches).forEach(([key, { displayName, defaultValue }]) => {
|
/** 组件的 Widget 显示名称 */
|
||||||
extendComponentOptions[`switch-${key}`] = {
|
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,
|
defaultValue,
|
||||||
displayName,
|
displayName,
|
||||||
hidden: true,
|
hidden: true,
|
||||||
}
|
}
|
||||||
})
|
|
||||||
return (component: ComponentMetadata) => {
|
|
||||||
const optionDisplayName = `${component.displayName}选项`
|
|
||||||
const selfOption = {
|
|
||||||
componentName: component.name,
|
|
||||||
optionDisplayName,
|
|
||||||
}
|
}
|
||||||
Object.assign(options, selfOption)
|
return <O extends UnknownOptions, N extends string>(
|
||||||
extendComponentOptions[optionName] = {
|
options: OptionsMetadata<O>,
|
||||||
defaultValue: options,
|
switchMetadataOption: SwitchMetadataOption<N, S>,
|
||||||
displayName: optionDisplayName,
|
) => {
|
||||||
|
optionsToExtend[switchMetadataOption.name as string] = {
|
||||||
|
defaultValue: switchMetadataOption,
|
||||||
|
displayName: switchMetadataOption.optionDisplayName,
|
||||||
}
|
}
|
||||||
component.options = { ...component.options, ...extendComponentOptions }
|
return { ...options, ...optionsToExtend } as SwitchOptionsMetadata<O, N, S>
|
||||||
if (!component.widget) {
|
}
|
||||||
component.widget = {
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认 Widget
|
||||||
|
*/
|
||||||
|
const newWidget = <N extends string, S extends string>(
|
||||||
|
options: SwitchMetadataOption<N, S>,
|
||||||
|
): Omit<Widget, 'name'> => ({
|
||||||
component: () => import('./SwitchOptions.vue').then(m => m.default),
|
component: () => import('./SwitchOptions.vue').then(m => m.default),
|
||||||
options,
|
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
|
const originalEntry = component.entry
|
||||||
component.entry = async (...args) => {
|
return (...args) => {
|
||||||
originalEntry?.(...args)
|
const result = originalEntry(...args)
|
||||||
const { name } = component
|
const componentOptions = getComponentSettings(component.name).options
|
||||||
const componentOptions = getComponentSettings(name).options
|
|
||||||
Object.keys(componentOptions).forEach(key => {
|
Object.keys(componentOptions).forEach(key => {
|
||||||
if (key.startsWith('switch-')) {
|
if (key.startsWith('switch-')) {
|
||||||
addComponentListener(
|
addComponentListener(
|
||||||
`${name}.${key}`,
|
`${component.name}.${key}`,
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
document.body.classList.toggle(`${name}-${key}`, value)
|
document.body.classList.toggle(`${component.name}-${key}`, value)
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
return result
|
||||||
return component
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 被包装后的 {@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
|
type ComponentOptionValidator<T> = (value: T, oldValue: T) => T | undefined | null
|
||||||
|
|
||||||
// TODO: 参考 discussion #3041。当不兼容代码替换完成后将 any 改为 unknown
|
export type UnknownOptions = Record<string, unknown>
|
||||||
export type UnknownOptions = Record<string, any>
|
|
||||||
|
|
||||||
export type EmptyOptions = Record<string, never>
|
export type EmptyOptions = Record<string, never>
|
||||||
|
|
||||||
@ -78,10 +77,6 @@ export type OptionsMetadata<O extends UnknownOptions = UnknownOptions> = {
|
|||||||
[OptionName in keyof O]: OptionMetadata<O[OptionName]>
|
[OptionName in keyof O]: OptionMetadata<O[OptionName]>
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 参考 discussion #3041。当不兼容代码替换完成后删除
|
|
||||||
export type ComponentOptions = OptionsMetadata
|
|
||||||
export type ComponentOption = OptionMetadata
|
|
||||||
|
|
||||||
/** 组件标签 */
|
/** 组件标签 */
|
||||||
export const componentsTags = {
|
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
|
* ```js
|
||||||
* {
|
* {
|
||||||
* enabled: false,
|
* enabled: false,
|
||||||
@ -75,9 +74,9 @@ const emptySettings: ComponentSettings = {
|
|||||||
*
|
*
|
||||||
* @param component 组件或组件名称
|
* @param component 组件或组件名称
|
||||||
*/
|
*/
|
||||||
export const getComponentSettings = <R extends UnknownOptions = UnknownOptions>(
|
export const getComponentSettings = <O extends UnknownOptions>(
|
||||||
component: ComponentMetadata | string,
|
component: ComponentMetadata<O> | string,
|
||||||
): ComponentSettings<R> => {
|
): ComponentSettings<O> => {
|
||||||
let componentMetadata: ComponentMetadata
|
let componentMetadata: ComponentMetadata
|
||||||
if (typeof component === 'string') {
|
if (typeof component === 'string') {
|
||||||
if (componentsMap[component] === undefined) {
|
if (componentsMap[component] === undefined) {
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export default Vue.extend({
|
|||||||
&.left-icon {
|
&.left-icon {
|
||||||
.icon-container {
|
.icon-container {
|
||||||
order: -1;
|
order: -1;
|
||||||
margin: 2px 8px 2px 0;
|
margin: 2px 6px 2px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user