mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge branch 'preview-fixes'
This commit is contained in:
commit
3981ed7b68
@ -28,7 +28,7 @@
|
||||
"name": "Install dependencies",
|
||||
"uses": "pnpm/action-setup@v2.2.4",
|
||||
"with": {
|
||||
"version": "8.9.0",
|
||||
"version": "8.12.1",
|
||||
"run_install": true
|
||||
}
|
||||
},
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
"name": "Install dependencies",
|
||||
"uses": "pnpm/action-setup@v2.2.4",
|
||||
"with": {
|
||||
"version": "8.9.0",
|
||||
"version": "8.12.1",
|
||||
"run_install": true
|
||||
}
|
||||
},
|
||||
|
||||
71
CHANGELOG.md
71
CHANGELOG.md
@ -1,6 +1,75 @@
|
||||
<!-- spell-checker: disable -->
|
||||
# 更新日志
|
||||
|
||||
## v2.8.8 / v2.8.8-preview
|
||||
`2024-01-18`
|
||||
|
||||
<details>
|
||||
<summary>正式版用户将获得 v2.8.6-preview ~ v2.8.7-preview 的所有新功能, 点击展开查看</summary>
|
||||
|
||||
✨新增
|
||||
- 新增插件 `下载视频 - MPV 输出支持加强版`. (PR #4448 by [weapon!](https://github.com/Asukaaaaaa))
|
||||
> 同时支持单文件和多文件, 不需要额外下载程序处理 mpv 协议, 配置方式请参考 [README](https://github.com/Asukaaaaaa/tricks/blob/main/Bilibili-Evolved%20mpv-ex%20%E6%8F%92%E4%BB%B6.md)
|
||||
|
||||
- `查看封面` 组件安装后可为 `下载视频` 提供下载封面支持. (#889, PR #4473 by [Oxygenくん](https://github.com/oxygenkun))
|
||||
- 新增插件 `下载视频 - WASM 混流输出`. (PR #4521 by [WakelessSloth56](https://github.com/WakelessSloth56))
|
||||
> 使用 WASM 在浏览器中下载并合并音视频,运行过程中请勿关闭页面,初次使用或清除缓存后需要加载约 30 MB 的 WASM 文件
|
||||
|
||||
- 使用流量计费网络时, 不再触发本体和功能的更新检查. (仅 Chrome) (#4477)
|
||||
- `快捷键扩展` 的音量调整支持自定义幅度. (#2594)
|
||||
- `自定义字体` 改善了组件代码和功能. (PR #4485 by [Tinhone](https://github.com/Tinhone))
|
||||
|
||||
☕开发者相关
|
||||
- PlayerAgent API 重构为调用 `window.player` 和 `window.nano` API, 可以解决 #2544 的问题, 感谢 [timongh](https://github.com/timongh) 和 [WakelessSloth56](https://github.com/WakelessSloth56) 的发现. (#4330, #4341)
|
||||
|
||||
</details>
|
||||
|
||||
⚠ 注意
|
||||
- Firefox 的最低版本要求提升至 121, 顺便更新了下配置要求, 分为了最低配置和推荐配置, CPU 统一为桌面级型号.
|
||||
- 移除对旧版视频播放器的支持, 仅支持新版视频播放器 (BPX 播放器)
|
||||
|
||||
✨新增
|
||||
- 新增插件 `动态过滤器 - 移除充电专属动态`. (#4033)
|
||||
> 移除动态里的充电专属动态, 装有 `动态过滤器` 时生效.
|
||||
|
||||
- `删除视频弹窗` 支持屏蔽 "心动" 弹窗, `禁用特殊弹幕样式` 支持屏蔽相应的带货弹幕. (#4565)
|
||||
- `删除广告` 支持屏蔽首页的桌面端弹窗广告. (#4590)
|
||||
- 新增插件 `下载视频 - 空输出`. (#4581)
|
||||
> 提供一个 "空" 的输出选项, 只想获取下载视频的附带产物 (弹幕, 字幕等) 时可以使用此插件.
|
||||
|
||||
- `下载视频` 支持下载合集. (#3240)
|
||||
- 视频相关功能和快捷键扩展支持新版番剧播放器. (#4571)
|
||||
|
||||
🐛修复
|
||||
- 修复 `评论区IP属地显示` 在小黑屋中失效的问题. (#4572, PR #4573 by [Light_Quanta](https://github.com/LightQuanta))
|
||||
- `自定义字体` 改进: (PR #4585 by [Tinhone](https://github.com/Tinhone))
|
||||
- 修复 `自定义字体` 导致快速收藏图标消失. (#4566)
|
||||
- 将 `覆盖选项` 迁移到了更多选项中.
|
||||
- 因为文泉驿微米黑(WenQuanYi Micro Hei)字体的部分版本存在朝鲜语(韩语)显示不正确的问题,所以从默认设置中删除,使用 Malgun Gothic 字体替换.
|
||||
- 夜间模式适配播放页的充电卡片. (#4599, PR #4600 by [Pencil](https://github.com/pencilqaq))
|
||||
- `直播信息扩充` 使用新的 API, 能够显示悄悄关注的主播. (PR #4587 by [Oxygenくん](https://github.com/oxygenkun))
|
||||
- 修复自定义顶栏在搜索页面的表现: 关键词同步 & 禁用全局固定. (#1431)
|
||||
- `删除广告` 使用 CSS `:has` 功能实现了移除广告的空白占位区域. (#3997)
|
||||
- 修复新版的动态多图界面中 `图片批量导出` 检测不到图片. (#4586)
|
||||
- 修复使用 `pluginApis.installStyle` 安装带有特殊字符的名称的样式后, 无法删除的问题. (#4557)
|
||||
- 修复快速收起评论遮挡了评论的操作菜单. (#4595)
|
||||
- 更新了 `稍后再看重定向` 的说明文字, 明确了功能作用范围. (#4555)
|
||||
> 将稍后再看的链接重定向为普通播放网址.
|
||||
> - `重定向页面`: 对稍后再看列表页面里的链接重定向.
|
||||
> - `重定向顶栏`: 对 `自定义顶栏` 里的稍后再看链接重定向.
|
||||
|
||||
- 修复文件命名格式中 `[title]`, `[ep]`, `[lid]` 变量的值获取不正确. (#4575)
|
||||
|
||||
☕开发者相关
|
||||
- pnpm 更新至 8.12.1.
|
||||
- README 调整了一些中英文字符之间的空格. (PR #4602 by [Shen-Linwood](https://github.com/Shen-Linwood))
|
||||
- PlayerAgent.getPlayerConfig 支持 BPX 播放器, 且默认认为当前 Agent 为 BPX 类型.
|
||||
- 修复 DownloadPackage 中同名文件会互相覆盖. (#4576)
|
||||
- 动态过滤器中使用 CSS `.plugin-block` 可以隐藏某个动态卡片, 插件结合 `forEachFeedsCard` 等 API 可以实现自定义的过滤逻辑. (#4033)
|
||||
- retrieveImageUrl 支持传入 picture 元素本身.
|
||||
- 内置 UA 更新至 Firefox 123.
|
||||
- 增加了 simulateClick API, 可以模拟一次点击. (依次触发 `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click` 事件)
|
||||
|
||||
## v2.8.7 / v2.8.7-preview
|
||||
`2023-11-23`
|
||||
|
||||
@ -46,7 +115,7 @@
|
||||
- 修复 `自定义顶栏` 历史面板无法正确跳转课程. (#4484)
|
||||
- 修复 `自定义顶栏` 在历史记录页面产生的布局错位. (#4459)
|
||||
- 修复 `网址参数清理` 处理相对路径 URL 时产生了错误的结果. (#4471)
|
||||
- 修复 `自定义顶栏` 在专栏页面挡住了作者信息顶栏.
|
||||
- 修复 `自定义顶栏` 在专栏页面挡住了作者信息顶栏. (#4540)
|
||||
|
||||
☕开发者相关
|
||||
- 锁定了 PR Check 中的 pnpm 版本, 避免 pnpm 更新后出现不兼容 lockfile 导致 PR Check 失败.
|
||||
|
||||
@ -42,7 +42,7 @@ pnpm install
|
||||
// @description Bilibili Evolved (本地)
|
||||
// @version 300.0
|
||||
// @author Grant Howard, Coulomb-G
|
||||
// @copyright 2023, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)
|
||||
// @copyright 2024, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)
|
||||
// @license MIT
|
||||
// @match *://*.bilibili.com/*
|
||||
// @exclude *://*.bilibili.com/*/mobile.html
|
||||
|
||||
32
README.md
32
README.md
@ -32,15 +32,12 @@
|
||||
|
||||
**注意事项**
|
||||
- **⚠ 使用正式版 (GitHub 源) 和预览版须翻墙.**
|
||||
- 默认不预装任何组件, 需要在按[设置](#设置)中的说明添加感兴趣的功能.
|
||||
- 默认不预装任何组件, 需要在按[设置](#设置)中的说明添加感兴趣的功能; 默认会屏蔽双击全屏, 如有需要可以安装 `双击全屏` 组件来恢复.
|
||||
- 这个是一个综合性的脚本, 如果只是想下载视频建议去 [GreasyFork](https://greasyfork.org/zh-CN/scripts/by-site/bilibili.com) 找个更专业的.
|
||||
- 全景视频不能用鼠标拖拽视角(只能用键盘操作).
|
||||
- 默认屏蔽双击全屏, 如有需要可以安装 `双击全屏` 组件来恢复.
|
||||
- 对性能有较大影响.
|
||||
- 对性能有较大影响, 详见[配置要求](#配置).
|
||||
- 默认不对未登录的状态做适配.
|
||||
- 新版本一旦正式发布, 就不再对旧版本做任何技术支持.
|
||||
- 使用外部网站的链接时 (如将下载任务发送到自己的服务器 / 使用链接安装组件等) 可能会提示"脚本试图访问跨域资源", 请选择"始终允许".
|
||||
- 需要在至少 1400 x 800 以上的逻辑分辨率下使用.
|
||||
- 使用外部网站的链接时 (如将下载任务发送到自己的服务器 / 使用链接安装组件等) 可能会提示"脚本试图访问跨域资源", 请选择"始终允许" 避免下次还提示.
|
||||
|
||||
| 正式版 (jsDelivr 源) | 正式版 (GitHub 源) | 预览版 |
|
||||
| -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
@ -73,16 +70,27 @@
|
||||
|
||||
<img alt="about-panel" src="images/v2/about-panel.jpg" width="450">
|
||||
|
||||
# 推荐配置
|
||||
- 操作系统: 64-bit Windows 10 / macOS Sonoma
|
||||
- 分辨率: 3840x2160 / 192ppi
|
||||
- 浏览器: Chrome 100+ / Firefox 103+ / Safari 15.4+ (仅理论上能运行, 没测试过)
|
||||
- 处理器: Intel Core i7-9750H / AMD Ryzen 5 5600H
|
||||
# 配置
|
||||
## 最低配置
|
||||
- 操作系统: Windows 8.1 / macOS Big Sur
|
||||
- 分辨率: 1920x1080, 缩放 125%
|
||||
- 浏览器: Chrome 105+ / Firefox 121+ / Safari 15.4+ (仅理论上能运行, 没测试过)
|
||||
- 处理器: Intel Core i5-10500 / AMD Ryzen 5 3600
|
||||
- 内存: 8GB
|
||||
- 脚本管理器: Tampermonkey 4.14 / Violentmonkey 2.13
|
||||
- 脚本管理器: Tampermonkey 5.0 / Violentmonkey 2.18
|
||||
- 显卡: GeForce GTX 1050 / Radeon HD 7870
|
||||
- 网络: 10MB/s
|
||||
|
||||
## 推荐配置
|
||||
- 操作系统: Windows 10 / macOS Sonoma
|
||||
- 分辨率: 3840x2160, 缩放 200%
|
||||
- 浏览器: Chrome 120+ / Firefox 121+
|
||||
- 处理器: Intel Core i7-11700 / AMD Ryzen 7 5800
|
||||
- 内存: 32GB
|
||||
- 脚本管理器: Tampermonkey 5.0 / Violentmonkey 2.18
|
||||
- 显卡: GeForce RTX 3060 / Radeon RX 6600XT
|
||||
- 网络: 20MB/s
|
||||
|
||||
# 兼容性
|
||||
|
||||
## 脚本管理器
|
||||
|
||||
@ -29,6 +29,11 @@ https://afdian.net/@the1812?tab=sponsor
|
||||
|
||||
| 时间 | 用户名 | 单号后4位 | 金额 |
|
||||
| ------------------- | --------------------- | --------- | ------- |
|
||||
| 2024.01.12 22:02:24 | 简式谐_LastofNESTS | 5700 | ¥22.33 |
|
||||
| 2024.01.02 16:44:38 | O*h | 5101 | ¥10.00 |
|
||||
| 2023.12.18 21:46:53 | *京 | 1206 | ¥20.00 |
|
||||
| 2023.12.12 19:57:57 | *克 | 0688 | ¥2.33 |
|
||||
| 2023.11.30 09:23:38 | *漩 | 2405 | ¥5.00 |
|
||||
| 2023.11.22 15:55:16 | *楠 | 2917 | ¥30.00 |
|
||||
| 2023.10.30 11:02:52 | *分 | 9485 | ¥3.00 |
|
||||
| 2023.10.17 09:38:26 | f*t | 0728 | ¥10.00 |
|
||||
|
||||
@ -195,7 +195,7 @@
|
||||
"type": "component",
|
||||
"name": "customFontFamily",
|
||||
"displayName": "自定义字体",
|
||||
"description": "by [@Tinhone](https://github.com/Tinhone)\n\n使用组件提供的字体设置覆盖原版的主站字体,并使主站字体可被自定义。字体设置写法请参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family) 、默认设置与设置说明",
|
||||
"description": "by [@Tinhone](https://github.com/Tinhone)\n\n使用组件提供的字体设置覆盖原版的主站字体,并使主站字体可被自定义。\r\n\r\n当组件被启用后,几乎所有的元素会立即应用组件提供的字体设置。\r\n\r\n选项说明:\r\n- `禁用标题标点符号缩进`: 在新版视频页中,推荐视频栏中的视频标题,如果首个字符是特定的标点符号,则文本会缩入左侧。这个选项可以禁用这种样式\r\n- `更多选项`:\r\n - `自定义字体`: 设置自定义字体。写法请参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family)、默认设置与设置说明。\r\n - `覆盖选项`: 相当于一个白名单,使用了特殊字体的元素会被加入其中。默认情况下这些元素不会应用组件提供的字体设置,只有在启用对应的选项后才会应用。",
|
||||
"fullRelativePath": "../../registry/dist/components/style/custom-font-family.js",
|
||||
"fullAbsolutePath": "registry/dist/components/style/custom-font-family.js"
|
||||
},
|
||||
@ -619,7 +619,7 @@
|
||||
"type": "component",
|
||||
"name": "watchlaterRedirect",
|
||||
"displayName": "稍后再看重定向",
|
||||
"description": "将稍后再看的链接重定向为普通播放网址.",
|
||||
"description": "将稍后再看的链接重定向为普通播放网址.\r\n- `重定向页面`: 对稍后再看列表页面里的链接重定向.\r\n- `重定向顶栏`: 对 `自定义顶栏` 里的稍后再看链接重定向.\r\n",
|
||||
"fullRelativePath": "../../registry/dist/components/utils/watchlater-redirect.js",
|
||||
"fullAbsolutePath": "registry/dist/components/utils/watchlater-redirect.js"
|
||||
},
|
||||
@ -904,6 +904,14 @@
|
||||
"description": "by FoundTheWOUT\n\n在视频播放器右上角显示系统时间.",
|
||||
"owner": "FoundTheWOUT"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "feedsFilter.pluginBlocks.chargeFeeds",
|
||||
"displayName": "动态过滤器 - 移除充电专属动态",
|
||||
"description": "移除动态里的充电专属动态, 装有 `动态过滤器` 时生效.\r\n",
|
||||
"fullRelativePath": "../../registry/dist/plugins/feeds/filter/hide-charge-feeds.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/feeds/filter/hide-charge-feeds.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "launchBar.actions.audioSearch",
|
||||
@ -1032,6 +1040,14 @@
|
||||
"fullRelativePath": "../../registry/dist/plugins/video/download/aria2-output.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/video/download/aria2-output.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "downloadVideo.outputs.empty",
|
||||
"displayName": "下载视频 - 空输出",
|
||||
"description": "提供一个 \"空\" 的输出选项, 只想获取下载视频的附带产物 (弹幕, 字幕等) 时可以使用此插件.\r\n",
|
||||
"fullRelativePath": "../../registry/dist/plugins/video/download/empty-output.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/video/download/empty-output.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "downloadVideo.outputs.idm",
|
||||
@ -1056,6 +1072,14 @@
|
||||
"fullRelativePath": "../../registry/dist/plugins/video/download/motrix-output.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/video/download/motrix-output.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "downloadVideo.outputs.mpv-ex",
|
||||
"displayName": "下载视频 - MPV 输出支持加强版",
|
||||
"description": "by [@asuaaa](https://github.com/Asukaaaaaa)\n\n为下载视频增加 MPV 输出,支持导出列表, 配置方式请参考 [README](https://github.com/Asukaaaaaa/tricks/blob/main/Bilibili-Evolved%20mpv-ex%20%E6%8F%92%E4%BB%B6.md)",
|
||||
"fullRelativePath": "../../registry/dist/plugins/video/download/mpv-output-ex.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/video/download/mpv-output-ex.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "downloadVideo.outputs.mpv-playlist",
|
||||
@ -1072,6 +1096,14 @@
|
||||
"fullRelativePath": "../../registry/dist/plugins/video/download/mpv-output.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/video/download/mpv-output.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "downloadVideo.outputs.wasm",
|
||||
"displayName": "下载视频 - WASM 混流输出",
|
||||
"description": "by [@WakelessSloth56](https://github.com/WakelessSloth56)\n\n使用 WASM 在浏览器中下载并合并音视频",
|
||||
"fullRelativePath": "../../registry/dist/plugins/video/download/wasm-output.js",
|
||||
"fullAbsolutePath": "registry/dist/plugins/video/download/wasm-output.js"
|
||||
},
|
||||
{
|
||||
"type": "plugin",
|
||||
"name": "speed.keymap",
|
||||
|
||||
@ -242,7 +242,15 @@ by [@RieN7](https://github.com/rien7)
|
||||
|
||||
by [@Tinhone](https://github.com/Tinhone)
|
||||
|
||||
使用组件提供的字体设置覆盖原版的主站字体,并使主站字体可被自定义。字体设置写法请参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family) 、默认设置与设置说明
|
||||
使用组件提供的字体设置覆盖原版的主站字体,并使主站字体可被自定义。
|
||||
|
||||
当组件被启用后,几乎所有的元素会立即应用组件提供的字体设置。
|
||||
|
||||
选项说明:
|
||||
- `禁用标题标点符号缩进`: 在新版视频页中,推荐视频栏中的视频标题,如果首个字符是特定的标点符号,则文本会缩入左侧。这个选项可以禁用这种样式
|
||||
- `更多选项`:
|
||||
- `自定义字体`: 设置自定义字体。写法请参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family)、默认设置与设置说明。
|
||||
- `覆盖选项`: 相当于一个白名单,使用了特殊字体的元素会被加入其中。默认情况下这些元素不会应用组件提供的字体设置,只有在启用对应的选项后才会应用。
|
||||
|
||||
### [自定义顶栏](../../registry/dist/components/style/custom-navbar.js)
|
||||
`customNavbar`
|
||||
@ -821,6 +829,8 @@ by [@Light_Quanta](https://github.com/LightQuanta)
|
||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/watchlater-redirect.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/watchlater-redirect.js)
|
||||
|
||||
将稍后再看的链接重定向为普通播放网址.
|
||||
- `重定向页面`: 对稍后再看列表页面里的链接重定向.
|
||||
- `重定向顶栏`: 对 `自定义顶栏` 里的稍后再看链接重定向.
|
||||
|
||||
### [自动移出稍后再看](../../registry/dist/components/video/auto-remove-watchlater.js)
|
||||
`autoRemoveWatchlater`
|
||||
@ -1242,6 +1252,15 @@ by FoundTheWOUT
|
||||
在视频播放器右上角显示系统时间.
|
||||
## 插件
|
||||
|
||||
### [动态过滤器 - 移除充电专属动态](../../registry/dist/plugins/feeds/filter/hide-charge-feeds.js)
|
||||
`feedsFilter.pluginBlocks.chargeFeeds`
|
||||
|
||||
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/feeds/filter/hide-charge-feeds.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/feeds/filter/hide-charge-feeds.js)
|
||||
|
||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/feeds/filter/hide-charge-feeds.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/feeds/filter/hide-charge-feeds.js)
|
||||
|
||||
移除动态里的充电专属动态, 装有 `动态过滤器` 时生效.
|
||||
|
||||
### [搜索栏 - 音频跳转](../../registry/dist/plugins/launch-bar/audio-search.js)
|
||||
`launchBar.actions.audioSearch`
|
||||
|
||||
@ -1388,6 +1407,15 @@ by FoundTheWOUT
|
||||
|
||||
为下载视频增加 aria2 文件导出和 RPC 输出支持.
|
||||
|
||||
### [下载视频 - 空输出](../../registry/dist/plugins/video/download/empty-output.js)
|
||||
`downloadVideo.outputs.empty`
|
||||
|
||||
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/video/download/empty-output.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/video/download/empty-output.js)
|
||||
|
||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/video/download/empty-output.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/video/download/empty-output.js)
|
||||
|
||||
提供一个 "空" 的输出选项, 只想获取下载视频的附带产物 (弹幕, 字幕等) 时可以使用此插件.
|
||||
|
||||
### [下载视频 - IDM 输出支持](../../registry/dist/plugins/video/download/idm-output.js)
|
||||
`downloadVideo.outputs.idm`
|
||||
|
||||
@ -1415,6 +1443,17 @@ by FoundTheWOUT
|
||||
|
||||
为下载视频增加 Motrix 输出支持.
|
||||
|
||||
### [下载视频 - MPV 输出支持加强版](../../registry/dist/plugins/video/download/mpv-output-ex.js)
|
||||
`downloadVideo.outputs.mpv-ex`
|
||||
|
||||
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/video/download/mpv-output-ex.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/video/download/mpv-output-ex.js)
|
||||
|
||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/video/download/mpv-output-ex.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/video/download/mpv-output-ex.js)
|
||||
|
||||
by [@asuaaa](https://github.com/Asukaaaaaa)
|
||||
|
||||
为下载视频增加 MPV 输出,支持导出列表, 配置方式请参考 [README](https://github.com/Asukaaaaaa/tricks/blob/main/Bilibili-Evolved%20mpv-ex%20%E6%8F%92%E4%BB%B6.md)
|
||||
|
||||
### [下载视频 - MPV 播放支持(列表)](../../registry/dist/plugins/video/download/mpv-output-playlist.js)
|
||||
`downloadVideo.outputs.mpv-playlist`
|
||||
|
||||
@ -1437,6 +1476,17 @@ by [@diannaojiang](https://github.com/diannaojiang)
|
||||
|
||||
为下载视频增加 MPV 输出支持, 配置方式请参考 [Bilibili-Playin-Mpv](https://github.com/diannaojiang/Bilibili-Playin-Mpv)
|
||||
|
||||
### [下载视频 - WASM 混流输出](../../registry/dist/plugins/video/download/wasm-output.js)
|
||||
`downloadVideo.outputs.wasm`
|
||||
|
||||
**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/video/download/wasm-output.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/video/download/wasm-output.js)
|
||||
|
||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/video/download/wasm-output.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/video/download/wasm-output.js)
|
||||
|
||||
by [@WakelessSloth56](https://github.com/WakelessSloth56)
|
||||
|
||||
使用 WASM 在浏览器中下载并合并音视频
|
||||
|
||||
### [快捷键扩展 - 视频倍速](../../registry/dist/plugins/video/player/speed.js)
|
||||
`speed.keymap`
|
||||
|
||||
|
||||
@ -109,20 +109,65 @@ export default Vue.extend({
|
||||
try {
|
||||
this.items = []
|
||||
this.loaded = false
|
||||
const pageSize = 24
|
||||
const [promise] = responsiveGetPages<LiveInfo>({
|
||||
const [, promise] = responsiveGetPages<LiveInfo>({
|
||||
api: page =>
|
||||
getJsonWithCredentials(
|
||||
`https://api.live.bilibili.com/relation/v1/feed/feed_list?page=${page}&pagesize=${pageSize}`,
|
||||
`https://api.live.bilibili.com/xlive/web-ucenter/v1/xfetter/GetWebList?page=${page}`,
|
||||
),
|
||||
getList: json => lodash.get(json, 'data.list', []),
|
||||
getTotal: json => lodash.get(json, 'data.results', 0),
|
||||
getList: json =>
|
||||
lodash.get(json, 'data.list', []).map(item => {
|
||||
const { face, uname, title, room_id, cover_from_user, online, uid, link } = item
|
||||
return {
|
||||
cover: face,
|
||||
face,
|
||||
uname,
|
||||
title,
|
||||
roomid: room_id,
|
||||
pic: cover_from_user,
|
||||
online,
|
||||
uid,
|
||||
link,
|
||||
}
|
||||
}),
|
||||
getTotal: json => lodash.get(json, 'data.count', 0),
|
||||
})
|
||||
this.items = await promise
|
||||
|
||||
const [allItems, recommendItems] = await Promise.all([promise, this.fetchRecommendItems()])
|
||||
this.items = this.sortByRecommend(allItems, recommendItems)
|
||||
} finally {
|
||||
this.loaded = true
|
||||
}
|
||||
},
|
||||
|
||||
async fetchRecommendItems(): Promise<LiveInfo[]> {
|
||||
// 动态portal接口会获取推荐的top30直播。同时这个接口不会忽略悄悄关注的up的直播。
|
||||
const portalList = await getJsonWithCredentials(
|
||||
'https://api.bilibili.com/x/polymer/web-dynamic/v1/portal',
|
||||
)
|
||||
const recommendLiveItems = portalList.data.live_users.items.map(item => {
|
||||
const { jump_url, room_id, face, title, uname, mid } = item
|
||||
return {
|
||||
cover: face,
|
||||
face,
|
||||
uname,
|
||||
title,
|
||||
roomid: room_id,
|
||||
pic: '', // portal接口没有
|
||||
online: 0, // portal接口没有
|
||||
uid: mid,
|
||||
link: jump_url,
|
||||
}
|
||||
})
|
||||
return recommendLiveItems
|
||||
},
|
||||
|
||||
sortByRecommend(feedItems: LiveInfo[], recommendItems: LiveInfo[]) {
|
||||
// recommendItems里的pic和online为默认值,但是ui也没用到。
|
||||
// 所以方便起见,直接拼接没出现在recommendItems里的item
|
||||
const recommendRoomIds = recommendItems.map(item => item.roomid)
|
||||
const feedConcatItems = feedItems.filter(item => !recommendRoomIds.includes(item.roomid))
|
||||
return lodash.concat(recommendItems, feedConcatItems)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@ -136,96 +181,119 @@ export default Vue.extend({
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
|
||||
body.dark & {
|
||||
background-color: #444;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
&-header {
|
||||
@include h-center();
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-title {
|
||||
@include h-center(4px);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-count {
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
@include h-center(4px);
|
||||
}
|
||||
|
||||
&-refresh {
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
|
||||
.be-icon {
|
||||
opacity: 0.5;
|
||||
transition: 0.35s ease-out;
|
||||
}
|
||||
|
||||
&:hover .be-icon {
|
||||
transform: rotate(360deg);
|
||||
opacity: 1;
|
||||
color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-more {
|
||||
opacity: 0.5;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
@include h-center();
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: var(--theme-color);
|
||||
}
|
||||
}
|
||||
|
||||
&-search {
|
||||
@include h-center(8px);
|
||||
|
||||
.be-icon {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.be-textbox {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
@include no-scrollbar();
|
||||
@include v-stretch(8px);
|
||||
// 默认状态
|
||||
max-height: calc(100vh - 358px);
|
||||
|
||||
// 开启过滤器
|
||||
body.enable-feeds-filter & {
|
||||
max-height: calc(100vh - 414px);
|
||||
}
|
||||
|
||||
// 禁掉 profile 后
|
||||
body.feeds-filter-side-block-profile & {
|
||||
max-height: calc(100vh - 218px);
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
@include h-center(8px);
|
||||
cursor: pointer;
|
||||
|
||||
&-avatar {
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
border: 1px solid #8884;
|
||||
}
|
||||
|
||||
&-info {
|
||||
@include v-stretch(2px);
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
&-title,
|
||||
&-user {
|
||||
@include single-line();
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
&-user {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:hover &-title {
|
||||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
@ -227,6 +227,7 @@ body.enable-feeds-filter:not(.disable-feeds-filter) {
|
||||
@include type-block();
|
||||
@include side-block();
|
||||
@include pattern-block();
|
||||
@include plugin-block();
|
||||
}
|
||||
body.disable-feeds-filter {
|
||||
.feeds-filter-section {
|
||||
|
||||
@ -107,3 +107,9 @@
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@mixin plugin-block {
|
||||
.bili-dyn-list__item.plugin-block,
|
||||
.feed-card .card.plugin-block {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
.at-panel,
|
||||
.emoji-panel {
|
||||
.emoji-panel,
|
||||
.reply-operation .operation-list {
|
||||
z-index: 111;
|
||||
}
|
||||
}
|
||||
|
||||
20
registry/lib/components/style/custom-font-family/data.ts
Normal file
20
registry/lib/components/style/custom-font-family/data.ts
Normal file
@ -0,0 +1,20 @@
|
||||
export const fontFamilyDefaultValue =
|
||||
'Microsoft Yahei, Helvetica Neue, Helvetica, Arial, Hiragino Sans GB, Heiti SC, Malgun Gothic'
|
||||
|
||||
export const coverOptionsName = [
|
||||
{ camel: 'coverOrnament', kebab: 'cover-ornament', display: '装扮字体' },
|
||||
{ camel: 'coverFansMedal', kebab: 'cover-fans-medal', display: '粉丝勋章字体' },
|
||||
{ camel: 'coverDanmaku', kebab: 'cover-danmaku', display: '弹幕字体' },
|
||||
{ camel: 'coverIconFont', kebab: 'cover-icon-font', display: '图标字体' },
|
||||
{ camel: 'coverColumn', kebab: 'cover-column', display: '专栏自定义字体' },
|
||||
{ camel: 'coverScore', kebab: 'cover-score', display: '评分字体' },
|
||||
]
|
||||
|
||||
export const coverOptionsDefaultValue = {
|
||||
coverOrnament: false,
|
||||
coverFansMedal: false,
|
||||
coverDanmaku: false,
|
||||
coverIconFont: false,
|
||||
coverColumn: false,
|
||||
coverScore: false,
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
$name: 'custom-font-family';
|
||||
|
||||
html[#{$name}--options--disable-quotation-mark-text-indent='true']
|
||||
:is(
|
||||
p[title^='「'],
|
||||
p[title^='『'],
|
||||
p[title^='【'],
|
||||
h1[title^='「'],
|
||||
h1[title^='『'],
|
||||
h1[title^='【'],
|
||||
h3[title^='「'],
|
||||
h3[title^='『'],
|
||||
h3[title^='【']
|
||||
) {
|
||||
text-indent: initial !important;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
$name: 'custom-font-family';
|
||||
|
||||
html[#{$name}--options--disable-title-punctuation-text-indent='true']
|
||||
:is(
|
||||
p[title^='「'],
|
||||
p[title^='『'],
|
||||
p[title^='【'],
|
||||
h1[title^='「'],
|
||||
h1[title^='『'],
|
||||
h1[title^='【'],
|
||||
h3[title^='「'],
|
||||
h3[title^='『'],
|
||||
h3[title^='【'],
|
||||
p[title^='~'],
|
||||
h3[title^='~'],
|
||||
p[title^='《'],
|
||||
h3[title^='《'],
|
||||
p[title^='“'],
|
||||
h3[title^='“'],
|
||||
p[title^='~'],
|
||||
h3[title^='~'],
|
||||
p[title^='《'],
|
||||
h3[title^='《']
|
||||
) {
|
||||
text-indent: initial !important;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="custom-font-family-extra-options-entry">
|
||||
<VButton @mouseover="loadPanel()" @click="togglePanelDisplay()">
|
||||
更多选项<VIcon icon="right-arrow" :size="16"></VIcon>
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { VIcon, VButton } from '@/ui'
|
||||
import { loadPanel, togglePanelDisplay } from './vm'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VIcon,
|
||||
VButton,
|
||||
},
|
||||
|
||||
methods: {
|
||||
loadPanel,
|
||||
togglePanelDisplay,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-font-family-extra-options-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<VPopup v-model="popupOpen" class="be-extra-options-panel" fixed :lazy="false">
|
||||
<div class="be-eop-header">
|
||||
<div class="be-eop-h-title">
|
||||
<VIcon class="be-eop-h-t-icon" :icon="initData.header.title.icon" :size="24"></VIcon>
|
||||
<div class="be-eop-h-t-text">{{ initData.header.title.text }}</div>
|
||||
</div>
|
||||
|
||||
<div class="be-eop-h-actions">
|
||||
<div v-for="action in initData.header.actions" :key="action.id" class="be-eop-h-a-action">
|
||||
<VIcon
|
||||
:ref="`action${action.id}`"
|
||||
:class="`action-${action.actionClassNameSuffix}`"
|
||||
:title="action.title"
|
||||
:icon="action.icon"
|
||||
:size="24"
|
||||
></VIcon>
|
||||
</div>
|
||||
<div class="be-eop-h-a-action">
|
||||
<VIcon
|
||||
class="action-close"
|
||||
title="关闭"
|
||||
icon="mdi-close"
|
||||
:size="24"
|
||||
@click="popupOpen = false"
|
||||
></VIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="be-eop-separator"></div>
|
||||
|
||||
<div class="be-eop-content">
|
||||
<div v-for="option in initData.content.options" :key="option.id" class="be-eop-c-option">
|
||||
<div class="be-eop-c-o-title">{{ option.title }}</div>
|
||||
<div class="be-eop-c-o-description">{{ option.description }}</div>
|
||||
<div class="be-eop-c-o-input" :class="`input-${option.inputClassNameSuffix}`">
|
||||
<slot :name="`input${option.id}`">
|
||||
选项输入入口默认文字,使用含 v-slot 指令的 template 元素以替换默认内容
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VPopup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { VPopup, VIcon } from '@/ui'
|
||||
import { defaultInitData } from './extra-options-panel'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ExtraOptionsPanel',
|
||||
|
||||
components: {
|
||||
VPopup,
|
||||
VIcon,
|
||||
},
|
||||
|
||||
props: {
|
||||
initData: {
|
||||
type: Object,
|
||||
default: defaultInitData,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
popupOpen: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './extra-options-panel';
|
||||
|
||||
.be-extra-options-panel {
|
||||
@include extra-options-panel();
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<ExtraOptionsPanel
|
||||
ref="extraOptionsPanel"
|
||||
class="custom-font-family-extra-options-panel"
|
||||
:class="{ peek: isPeeking }"
|
||||
:init-data="initData"
|
||||
>
|
||||
<template #input0>
|
||||
<TextArea v-model="inputFontFamily" />
|
||||
</template>
|
||||
|
||||
<template #input1>
|
||||
<SwitchOptionsMin :options="switchOptionsComponentOptions"></SwitchOptionsMin>
|
||||
</template>
|
||||
</ExtraOptionsPanel>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { TextArea } from '@/ui'
|
||||
import SwitchOptionsMin from './SwitchOptionsMin.vue'
|
||||
import { SwitchMetadataOption } from '@/components/switch-options'
|
||||
import ExtraOptionsPanel from './ExtraOptionsPanel.vue'
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { delay } from '@/core/utils'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import { fontFamilyDefaultValue, coverOptionsName, coverOptionsDefaultValue } from '../data'
|
||||
import { ExtraOptionsPanelInitData } from './extra-options-panel'
|
||||
|
||||
const initData: ExtraOptionsPanelInitData = {
|
||||
header: {
|
||||
title: {
|
||||
text: '自定义字体',
|
||||
icon: 'mdi-format-font',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 0,
|
||||
title: '重置面板中的所有选项为默认值',
|
||||
icon: 'mdi-cog-sync-outline',
|
||||
actionClassNameSuffix: 'reset',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '透视',
|
||||
icon: 'mdi-eye-outline',
|
||||
actionClassNameSuffix: 'peek',
|
||||
},
|
||||
],
|
||||
},
|
||||
content: {
|
||||
options: [
|
||||
{
|
||||
id: 0,
|
||||
title: '自定义字体',
|
||||
description: '输入需要设置的字体,不同字体之间必须以英文逗号分隔',
|
||||
inputClassNameSuffix: 'input-font-family',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '覆盖选项',
|
||||
description: '下面的元素使用了特殊字体,启用选项后可在对应的元素上应用组件提供的字体设置',
|
||||
inputClassNameSuffix: 'cover-options',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const switchOptionsComponentOptions: SwitchMetadataOption<string, string> = {
|
||||
name: 'customFontFamilyCoverOptions',
|
||||
switches: {},
|
||||
radio: false,
|
||||
dimAt: 'notChecked',
|
||||
switchProps: {
|
||||
checkedIcon: 'mdi-checkbox-marked-circle',
|
||||
notCheckedIcon: 'mdi-checkbox-blank-circle-outline',
|
||||
},
|
||||
componentName: 'customFontFamily',
|
||||
optionDisplayName: '自定义字体覆盖选项',
|
||||
}
|
||||
|
||||
for (const coverOptionName of coverOptionsName) {
|
||||
switchOptionsComponentOptions.switches[coverOptionName.camel] = {
|
||||
displayName: coverOptionName.display,
|
||||
defaultValue: coverOptionsDefaultValue[coverOptionName.camel],
|
||||
}
|
||||
}
|
||||
|
||||
const console = useScopedConsole('自定义字体')
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ExtraOptionsPanel,
|
||||
TextArea,
|
||||
SwitchOptionsMin,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isPeeking: false,
|
||||
isMouseOverPeekIcon: false,
|
||||
initData,
|
||||
// 设置 inputFontFamily 初始值为组件 fontFamily 选项的值
|
||||
inputFontFamily: getComponentSettings('customFontFamily').options.fontFamily,
|
||||
switchOptionsComponentOptions,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// 监听 inputFontFamily 修改动作,实时修改组件 fontFamily 选项为 inputFontFamily 修改后的新值,并拥有 1000ms 防抖
|
||||
inputFontFamily: lodash.debounce(value => {
|
||||
getComponentSettings('customFontFamily').options.fontFamily = value
|
||||
}, 1000),
|
||||
|
||||
async isMouseOverPeekIcon(value: boolean) {
|
||||
if (!value) {
|
||||
this.isPeeking = false
|
||||
return
|
||||
}
|
||||
if (value) {
|
||||
await delay(200)
|
||||
}
|
||||
if (this.isMouseOverPeekIcon) {
|
||||
this.isPeeking = true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// 当在 v-for 中使用模板引用时,相应的引用中包含的值是一个数组
|
||||
// 但 ref 数组并不保证与源数组相同的顺序,所以统一命名且不加用以区分的后缀,仅靠数组选择元素是不现实的
|
||||
// https://cn.vuejs.org/guide/essentials/template-refs.html#refs-inside-v-for
|
||||
|
||||
const action0Reset = this.$refs.extraOptionsPanel.$refs.action0[0].$el as HTMLElement
|
||||
action0Reset.addEventListener('click', this.confirmResetOptions)
|
||||
|
||||
const action1Peek = this.$refs.extraOptionsPanel.$refs.action1[0].$el as HTMLElement
|
||||
action1Peek.addEventListener('mouseover', this.setIsMouseOverPeekIconToTrue)
|
||||
action1Peek.addEventListener('mouseout', this.setIsMouseOverPeekIconToFalse)
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDisplay() {
|
||||
this.$refs.extraOptionsPanel.popupOpen = !this.$refs.extraOptionsPanel.popupOpen
|
||||
},
|
||||
|
||||
setIsMouseOverPeekIconToTrue() {
|
||||
this.isMouseOverPeekIcon = true
|
||||
},
|
||||
|
||||
setIsMouseOverPeekIconToFalse() {
|
||||
this.isMouseOverPeekIcon = false
|
||||
},
|
||||
|
||||
confirmResetOptions() {
|
||||
if (confirm('确定将面板中的所有选项重置为默认值吗?')) {
|
||||
this.resetOptions()
|
||||
}
|
||||
},
|
||||
|
||||
resetOptions() {
|
||||
// 重置字体选项
|
||||
getComponentSettings('customFontFamily').options.fontFamily = fontFamilyDefaultValue
|
||||
this.inputFontFamily = fontFamilyDefaultValue
|
||||
|
||||
// 重置覆盖选项
|
||||
for (const coverOptionName of coverOptionsName) {
|
||||
const defaultValue = coverOptionsDefaultValue[coverOptionName.camel]
|
||||
getComponentSettings('customFontFamily').options[coverOptionName.camel] = defaultValue
|
||||
}
|
||||
|
||||
Toast.success('更多选项面板中的所有选项已成功被重置为默认值', '自定义字体', 2000)
|
||||
console.log('更多选项面板中的所有选项已成功被重置为默认值')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-font-family-extra-options-panel {
|
||||
&.peek {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
> .be-eop-content > .be-eop-c-option > .be-eop-c-o-input.input-input-font-family > .be-text-area {
|
||||
min-height: 160px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,115 @@
|
||||
<!-- 这个 Vue 组件是因为无法正常从 @/components/SwitchOptions.vue 导入 SwitchOptions 组件而新建的 -->
|
||||
|
||||
<template>
|
||||
<div class="switch-options-min">
|
||||
<div class="switch-options-min-grid">
|
||||
<component
|
||||
:is="options.radio ? 'RadioButton' : 'CheckBox'"
|
||||
v-for="name of Object.keys(options.switches)"
|
||||
:key="name"
|
||||
:class="{ dim: !(Number(componentOptions[name]) ^ Number(isDimAtChecked)) }"
|
||||
v-bind="mergedSwitchProps"
|
||||
:checked="componentOptions[name]"
|
||||
@change="componentOptions[name] = $event"
|
||||
>
|
||||
<!-- dim 这里原来的写法在使用 export default defineComponent 的情况下会报错,虽然能运行,但还是改了改 -->
|
||||
{{ options.switches[name].displayName }}
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { CheckBox, RadioButton } from '@/ui'
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SwitchOptionsMin',
|
||||
|
||||
components: {
|
||||
CheckBox,
|
||||
RadioButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
const { componentName } = this.options
|
||||
const componentOptions = getComponentSettings(componentName).options
|
||||
const getIsDimAtChecked = () => {
|
||||
if (this.options.dimAt === 'checked' || this.options.dimAt === undefined) {
|
||||
return true
|
||||
}
|
||||
if (this.options.dimAt === 'notChecked') {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
const isDimAtChecked = getIsDimAtChecked()
|
||||
|
||||
return {
|
||||
componentOptions,
|
||||
isDimAtChecked,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
mergedSwitchProps() {
|
||||
return {
|
||||
checkedIcon: 'mdi-eye-off-outline',
|
||||
notCheckedIcon: 'mdi-eye-outline',
|
||||
...this.options.switchProps,
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
options() {
|
||||
this.updateColumnsCount()
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateColumnsCount()
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateColumnsCount() {
|
||||
const element = this.$el as HTMLElement
|
||||
const columns = Math.ceil(Object.keys(this.options.switches).length / 12)
|
||||
element.style.setProperty('--columns', columns.toString())
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common';
|
||||
|
||||
.switch-options-min {
|
||||
position: relative;
|
||||
--columns: 1;
|
||||
width: 100%;
|
||||
|
||||
.switch-icon {
|
||||
margin-right: 8px;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.dim {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
& > &-grid {
|
||||
font-size: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, 50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,45 +0,0 @@
|
||||
<template>
|
||||
<div class="custom-font-family-extra-options-entry">
|
||||
<VButton ref="button" @mouseover="loadExtraOptions()" @click="toggleExtraOptionsDisplay()">
|
||||
字体设置<VIcon icon="right-arrow" :size="16"></VIcon>
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { VIcon, VButton } from '@/ui'
|
||||
import {
|
||||
setTriggerElement,
|
||||
getExtraOptionsLoadState,
|
||||
loadExtraOptions,
|
||||
toggleExtraOptionsDisplay,
|
||||
} from './vm'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VIcon,
|
||||
VButton,
|
||||
},
|
||||
|
||||
methods: {
|
||||
async loadExtraOptions() {
|
||||
const isLoaded = await getExtraOptionsLoadState()
|
||||
if (!isLoaded) {
|
||||
await loadExtraOptions()
|
||||
const triggerButton = this.$refs.button.$el as HTMLElement
|
||||
setTriggerElement(triggerButton)
|
||||
}
|
||||
},
|
||||
toggleExtraOptionsDisplay,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-font-family-extra-options-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,59 +1,52 @@
|
||||
@import 'common';
|
||||
|
||||
$pref: 'eop';
|
||||
$prefH: 'eop-h';
|
||||
$prefHL: 'eop-h-l';
|
||||
$prefHR: 'eop-h-r';
|
||||
$prefC: 'eop-c';
|
||||
$prefCS: 'eop-c-s';
|
||||
|
||||
$panelPadding: 22px;
|
||||
$pref: 'be-eop';
|
||||
$panel-padding: 18px;
|
||||
$internal-spacing: 14px;
|
||||
|
||||
@mixin extra-options-panel {
|
||||
&.extra-options-panel {
|
||||
&.be-extra-options-panel {
|
||||
@include popup();
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
padding: $panelPadding 0;
|
||||
padding: $panel-padding 0;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translateX(-50%) translateY(-50%) scale(0.95);
|
||||
transition: all 0.2s ease-out;
|
||||
font-size: 14px;
|
||||
line-height: normal;
|
||||
z-index: 100002;
|
||||
|
||||
&.open {
|
||||
transform: translateX(-50%) translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
&.peek {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
> .#{$pref}-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 $panelPadding 18px $panelPadding;
|
||||
padding: 0 $panel-padding $internal-spacing $panel-padding;
|
||||
flex-shrink: 0;
|
||||
|
||||
> .#{$prefH}-left {
|
||||
> .#{$pref}-h-title {
|
||||
display: flex;
|
||||
column-gap: 4px;
|
||||
|
||||
> .#{$prefHL}-title {
|
||||
> .#{$pref}-h-t-text {
|
||||
@include semi-bold();
|
||||
font-size: 19px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
> .#{$prefH}-right {
|
||||
> .#{$pref}-h-actions {
|
||||
display: flex;
|
||||
column-gap: 20px;
|
||||
column-gap: ($internal-spacing + 4px);
|
||||
|
||||
> [class*='#{$prefHR}'] {
|
||||
> .#{$pref}-h-a-action {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-out;
|
||||
|
||||
@ -66,40 +59,41 @@ $panelPadding: 22px;
|
||||
|
||||
> .#{$pref}-separator {
|
||||
height: 1px;
|
||||
margin: 0 $panelPadding;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
margin: 0 $panel-padding;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
flex-shrink: 0;
|
||||
|
||||
body.dark & {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
> .#{$pref}-content {
|
||||
padding: 0 $panelPadding;
|
||||
@include no-scrollbar();
|
||||
padding: $internal-spacing $panel-padding 0 $panel-padding;
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
> .#{$prefC}-section {
|
||||
margin: 18px 0;
|
||||
> .#{$pref}-c-option {
|
||||
margin-bottom: ($internal-spacing + 4px);
|
||||
|
||||
&:nth-child(1):nth-last-child(1) {
|
||||
margin-bottom: 12px;
|
||||
&:nth-last-child(1) {
|
||||
margin-bottom: ($internal-spacing - 2px);
|
||||
}
|
||||
|
||||
> .#{$prefCS}-title {
|
||||
> .#{$pref}-c-o-title {
|
||||
margin-bottom: 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .#{$prefCS}-description {
|
||||
> .#{$pref}-c-o-description {
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.6;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
> .#{$prefCS}-input {
|
||||
> .#{$pref}-c-o-input {
|
||||
@include h-center();
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
export interface ExtraOptionsPanelInitData {
|
||||
header: {
|
||||
title: {
|
||||
/** 标题文字 */
|
||||
text: string
|
||||
/** 标题图标 */
|
||||
icon: string
|
||||
}
|
||||
actions: {
|
||||
/** 动作序号,从 0 开始增长,重复将会导致渲染异常 */
|
||||
id: number
|
||||
/** 动作标题 */
|
||||
title: string
|
||||
/** 动作图标 */
|
||||
icon: string
|
||||
/** 动作类名的后缀,前缀是 action- */
|
||||
actionClassNameSuffix: string
|
||||
}[]
|
||||
}
|
||||
content: {
|
||||
options: {
|
||||
/** 选项序号,从 0 开始增长,重复将会导致渲染异常 */
|
||||
id: number
|
||||
/** 选项标题 */
|
||||
title: string
|
||||
/** 选项介绍 */
|
||||
description: string
|
||||
/** 选项输入入口类名的后缀,前缀是 input- */
|
||||
inputClassNameSuffix: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
// 当使用 ExtraOptionsPanel 组件时,如果不传入 initData 所使用的默认数据
|
||||
export const defaultInitData: ExtraOptionsPanelInitData = {
|
||||
header: {
|
||||
title: {
|
||||
text: '默认标题',
|
||||
icon: 'mdi-format-font',
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: 0,
|
||||
title: '默认动作 0',
|
||||
icon: 'mdi-cog-sync-outline',
|
||||
actionClassNameSuffix: 'default-action-0',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '默认动作 1',
|
||||
icon: 'mdi-eye-outline',
|
||||
actionClassNameSuffix: 'default-action-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
content: {
|
||||
options: [
|
||||
{
|
||||
id: 0,
|
||||
title: '默认选项 0',
|
||||
description: '默认选项 0 的说明',
|
||||
inputClassNameSuffix: 'default-option-0',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '默认选项 1',
|
||||
description: '默认选项 1 的说明',
|
||||
inputClassNameSuffix: 'default-option-1',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@ -1,136 +0,0 @@
|
||||
<template>
|
||||
<VPopup
|
||||
ref="popup"
|
||||
v-model="popupOpen"
|
||||
class="custom-font-family-extra-options-panel extra-options-panel"
|
||||
fixed
|
||||
:lazy="false"
|
||||
:trigger-element="triggerElement"
|
||||
:class="{ peek: isPeeking }"
|
||||
>
|
||||
<div class="eop-header">
|
||||
<div class="eop-h-left">
|
||||
<VIcon class="eop-h-l-symbol" icon="mdi-format-font" :size="24"></VIcon>
|
||||
<div class="eop-h-l-title">字体设置</div>
|
||||
</div>
|
||||
|
||||
<div class="eop-h-right">
|
||||
<VIcon
|
||||
class="eop-h-r-reset"
|
||||
title="重置面板中的所有选项为默认值"
|
||||
icon="mdi-cog-sync-outline"
|
||||
:size="24"
|
||||
@click="confirmResetOptions()"
|
||||
></VIcon>
|
||||
<VIcon
|
||||
class="eop-h-r-peek"
|
||||
title="透视"
|
||||
icon="mdi-eye-outline"
|
||||
:size="24"
|
||||
@mouseover="mouseOnPeekIcon = true"
|
||||
@mouseout="mouseOnPeekIcon = false"
|
||||
></VIcon>
|
||||
<VIcon
|
||||
class="eop-h-r-close"
|
||||
title="关闭"
|
||||
icon="mdi-close"
|
||||
:size="24"
|
||||
@click="popupOpen = false"
|
||||
></VIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="eop-separator"></div>
|
||||
|
||||
<div class="eop-content">
|
||||
<div class="eop-c-section">
|
||||
<div class="eop-c-s-title">自定义字体</div>
|
||||
<div class="eop-c-s-description">输入字体,不同字体之间必须以英文逗号分隔</div>
|
||||
<div class="eop-c-s-input input-font-family">
|
||||
<TextArea v-model="inputFontFamily" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VPopup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { VPopup, VIcon, TextArea } from '@/ui'
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import { delay } from '@/core/utils'
|
||||
import { useScopedConsole } from '@/core/utils/log'
|
||||
import { fontFamilyDefaultValue } from '../font-family-default-value'
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
VPopup,
|
||||
VIcon,
|
||||
TextArea,
|
||||
},
|
||||
|
||||
props: {
|
||||
triggerElement: {
|
||||
type: HTMLElement,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
popupOpen: false,
|
||||
isPeeking: false,
|
||||
mouseOnPeekIcon: false,
|
||||
// 设置 inputFontFamily 初始值为组件 fontFamily 选项的值
|
||||
inputFontFamily: getComponentSettings('customFontFamily').options.fontFamily,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
// 监听 inputFontFamily 修改动作,实时修改组件 fontFamily 选项为 inputFontFamily 修改后的新值,并拥有 500ms 防抖
|
||||
inputFontFamily: lodash.debounce(value => {
|
||||
getComponentSettings('customFontFamily').options.fontFamily = value
|
||||
}, 500),
|
||||
|
||||
async mouseOnPeekIcon(value: boolean) {
|
||||
if (!value) {
|
||||
this.isPeeking = false
|
||||
return
|
||||
}
|
||||
if (value) {
|
||||
await delay(200)
|
||||
}
|
||||
if (this.mouseOnPeekIcon) {
|
||||
this.isPeeking = true
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
confirmResetOptions() {
|
||||
if (confirm('确定将面板中的所有选项重置为默认值吗?')) {
|
||||
this.resetOptions()
|
||||
}
|
||||
},
|
||||
|
||||
resetOptions() {
|
||||
getComponentSettings('customFontFamily').options.fontFamily = fontFamilyDefaultValue
|
||||
this.inputFontFamily = fontFamilyDefaultValue
|
||||
const console = useScopedConsole('自定义字体')
|
||||
console.log('已将字体设置面板中的所有选项重置为默认值')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './extra-options-panel';
|
||||
|
||||
.custom-font-family-extra-options-panel {
|
||||
@include extra-options-panel;
|
||||
|
||||
> .eop-content > .eop-c-section > .eop-c-s-input.input-font-family .be-text-area {
|
||||
min-height: 160px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,24 +1,26 @@
|
||||
import { mountVueComponent } from '@/core/utils'
|
||||
|
||||
let extraOptionsVm: Vue & {
|
||||
popupOpen: boolean
|
||||
triggerElement: HTMLElement
|
||||
let panelVm: Vue & {
|
||||
toggleDisplay: () => void
|
||||
}
|
||||
|
||||
export const setTriggerElement = (element: HTMLElement) => {
|
||||
extraOptionsVm.triggerElement = element
|
||||
const getPanelLoadState = () => {
|
||||
return Boolean(panelVm)
|
||||
}
|
||||
|
||||
export const getExtraOptionsLoadState = async () => {
|
||||
return Boolean(extraOptionsVm)
|
||||
const mountPanel = async () => {
|
||||
const panel = await import('./Panel.vue').then(m => m.default)
|
||||
panelVm = mountVueComponent(panel)
|
||||
document.body.insertAdjacentElement('beforeend', panelVm.$el)
|
||||
}
|
||||
|
||||
export const loadExtraOptions = async () => {
|
||||
const extraOptions = await import('./panel.vue').then(m => m.default)
|
||||
extraOptionsVm = mountVueComponent(extraOptions)
|
||||
document.body.insertAdjacentElement('beforeend', extraOptionsVm.$el)
|
||||
export const loadPanel = async () => {
|
||||
const isLoaded = getPanelLoadState()
|
||||
if (!isLoaded) {
|
||||
await mountPanel()
|
||||
}
|
||||
}
|
||||
|
||||
export const toggleExtraOptionsDisplay = async () => {
|
||||
extraOptionsVm.popupOpen = !extraOptionsVm.popupOpen
|
||||
export const togglePanelDisplay = async () => {
|
||||
panelVm.toggleDisplay()
|
||||
}
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export const fontFamilyDefaultValue =
|
||||
'Microsoft Yahei, Helvetica Neue, Helvetica, Arial, Hiragino Sans GB, Heiti SC, WenQuanYi Micro Hei'
|
||||
@ -0,0 +1,9 @@
|
||||
使用组件提供的字体设置覆盖原版的主站字体,并使主站字体可被自定义。
|
||||
|
||||
当组件被启用后,几乎所有的元素会立即应用组件提供的字体设置。
|
||||
|
||||
选项说明:
|
||||
- `禁用标题标点符号缩进`: 在新版视频页中,推荐视频栏中的视频标题,如果首个字符是特定的标点符号,则文本会缩入左侧。这个选项可以禁用这种样式
|
||||
- `更多选项`:
|
||||
- `自定义字体`: 设置自定义字体。写法请参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family)、默认设置与设置说明。
|
||||
- `覆盖选项`: 相当于一个白名单,使用了特殊字体的元素会被加入其中。默认情况下这些元素不会应用组件提供的字体设置,只有在启用对应的选项后才会应用。
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { addComponentListener } from '@/core/settings'
|
||||
import { fontFamilyDefaultValue } from './font-family-default-value'
|
||||
import { fontFamilyDefaultValue, coverOptionsName, coverOptionsDefaultValue } from './data'
|
||||
|
||||
const docElement = document.documentElement
|
||||
|
||||
@ -12,11 +12,6 @@ const name = camelName
|
||||
|
||||
const displayName = '自定义字体'
|
||||
|
||||
const description = {
|
||||
'zh-CN':
|
||||
'使用组件提供的字体设置覆盖原版的主站字体,并使主站字体可被自定义。字体设置写法请参考 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-family) 、默认设置与设置说明',
|
||||
}
|
||||
|
||||
const tags = [componentsTags.style, componentsTags.general]
|
||||
|
||||
const entry = () => {
|
||||
@ -34,34 +29,25 @@ const entry = () => {
|
||||
)
|
||||
|
||||
addComponentListener(
|
||||
`${name}.disableQuotationMarkTextIndent`,
|
||||
`${name}.disableTitlePunctuationTextIndent`,
|
||||
(value: boolean) => {
|
||||
docElement.setAttribute(
|
||||
`${kebabName}--options--disable-quotation-mark-text-indent`,
|
||||
`${kebabName}--options--disable-title-punctuation-text-indent`,
|
||||
`${value}`,
|
||||
)
|
||||
},
|
||||
true,
|
||||
)
|
||||
|
||||
const onOptionNames = [
|
||||
{ camel: 'onOrnament', kebab: 'on-ornament' },
|
||||
{ camel: 'onFansMedal', kebab: 'on-fans-medal' },
|
||||
{ camel: 'onDanmaku', kebab: 'on-danmaku' },
|
||||
{ camel: 'onIconFont', kebab: 'on-icon-font' },
|
||||
{ camel: 'onColumn', kebab: 'on-column' },
|
||||
{ camel: 'onScore', kebab: 'on-score' },
|
||||
]
|
||||
|
||||
onOptionNames.forEach(onOptionName => {
|
||||
for (const coverOptionName of coverOptionsName) {
|
||||
addComponentListener(
|
||||
`${name}.${onOptionName.camel}`,
|
||||
`${name}.${coverOptionName.camel}`,
|
||||
(value: boolean) => {
|
||||
docElement.setAttribute(`${kebabName}--options--${onOptionName.kebab}`, `${value}`)
|
||||
docElement.setAttribute(`${kebabName}--options--${coverOptionName.kebab}`, `${value}`)
|
||||
},
|
||||
true,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
@ -70,47 +56,35 @@ const options = {
|
||||
defaultValue: fontFamilyDefaultValue,
|
||||
hidden: true,
|
||||
},
|
||||
disableQuotationMarkTextIndent: {
|
||||
displayName: '禁用引号缩进',
|
||||
disableTitlePunctuationTextIndent: {
|
||||
displayName: '禁用标题标点符号缩进',
|
||||
defaultValue: true,
|
||||
},
|
||||
onOrnament: {
|
||||
displayName: '覆盖装扮字体',
|
||||
defaultValue: false,
|
||||
},
|
||||
onFansMedal: {
|
||||
displayName: '覆盖粉丝勋章字体',
|
||||
defaultValue: false,
|
||||
},
|
||||
onDanmaku: {
|
||||
displayName: '覆盖弹幕字体',
|
||||
defaultValue: false,
|
||||
},
|
||||
onIconFont: {
|
||||
displayName: '覆盖图标字体',
|
||||
defaultValue: false,
|
||||
},
|
||||
onColumn: {
|
||||
displayName: '覆盖专栏自定义字体',
|
||||
defaultValue: false,
|
||||
},
|
||||
onScore: {
|
||||
displayName: '覆盖评分字体',
|
||||
defaultValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
const extraOptions = () => import('./extra-options/entry.vue').then(m => m.default)
|
||||
const invokeCoverOptions = () => {
|
||||
for (const coverOptionName of coverOptionsName) {
|
||||
options[coverOptionName.camel] = {
|
||||
displayName: coverOptionName.display,
|
||||
defaultValue: coverOptionsDefaultValue[coverOptionName.camel],
|
||||
hidden: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invokeCoverOptions()
|
||||
|
||||
const extraOptions = () => import('./extra-options/Entry.vue').then(m => m.default)
|
||||
|
||||
const instantStyles = [
|
||||
{
|
||||
name: `${name}--style--setFontFamily`,
|
||||
name: `${kebabName}--style--set-font-family`, // style 标签 id
|
||||
style: () => import('./set-font-family.scss'),
|
||||
important: true,
|
||||
},
|
||||
{
|
||||
name: `${name}--style--disableQuotationMarkTextIndent`,
|
||||
style: () => import('./disable-quotation-mark-text-indent.scss'),
|
||||
name: `${kebabName}--style--disable-title-punctuation-text-indent`,
|
||||
style: () => import('./disable-title-punctuation-text-indent.scss'),
|
||||
important: true,
|
||||
},
|
||||
]
|
||||
@ -123,7 +97,6 @@ const author = {
|
||||
export const component = defineComponentMetadata({
|
||||
name,
|
||||
displayName,
|
||||
description,
|
||||
tags,
|
||||
entry,
|
||||
options,
|
||||
|
||||
@ -1,115 +1,117 @@
|
||||
$name: 'custom-font-family';
|
||||
$fontFamilyCss: var(--#{$name}--options--font-family) !important;
|
||||
$font-family-css: var(--#{$name}--options--font-family) !important;
|
||||
|
||||
// 收集 CSS 路径为单一字符串
|
||||
@function collectPaths($pathList) {
|
||||
$stringPath: '';
|
||||
@each $path in $pathList {
|
||||
$i: index($pathList, $path);
|
||||
@function collect-paths($path-list) {
|
||||
$string-path: '';
|
||||
@each $path in $path-list {
|
||||
$i: index($path-list, $path);
|
||||
@if $i!=1 {
|
||||
$stringPath: '#{$stringPath}, ';
|
||||
$string-path: '#{$string-path}, ';
|
||||
}
|
||||
$stringPath: '#{$stringPath}#{$path}';
|
||||
$string-path: '#{$string-path}#{$path}';
|
||||
}
|
||||
@return $stringPath;
|
||||
@return $string-path;
|
||||
}
|
||||
|
||||
@function getOrnamentPath() {
|
||||
$newVideo: '.reply-item .root-reply-container .content-warp .reply-decorate .user-sailing .user-sailing-text .sailing-text';
|
||||
@function get-ornament-path() {
|
||||
$new-video: '.reply-item .root-reply-container .content-warp .reply-decorate .user-sailing .user-sailing-text .sailing-text';
|
||||
$dynamic: '.bili-dyn-ornament__type--3 span';
|
||||
$dynamicCommentArea: '.bb-comment .sailing .sailing-info, .comment-bilibili-fold .sailing .sailing-info';
|
||||
@return collectPaths(($newVideo, $dynamic, $dynamicCommentArea));
|
||||
$dynamic-comment-area: '.bb-comment .sailing .sailing-info, .comment-bilibili-fold .sailing .sailing-info';
|
||||
@return collect-paths(($new-video, $dynamic, $dynamic-comment-area));
|
||||
}
|
||||
|
||||
@function getFansMedalPath() {
|
||||
$newVideo: '.badge-level';
|
||||
@return collectPaths(($newVideo));
|
||||
@function get-fans-medal-path() {
|
||||
$new-video: '.badge-level';
|
||||
@return collect-paths(($new-video));
|
||||
}
|
||||
|
||||
@function getDanmakuPath() {
|
||||
@function get-danmaku-path() {
|
||||
$main: '.bili-dm';
|
||||
@return collectPaths(($main));
|
||||
@return collect-paths(($main));
|
||||
}
|
||||
|
||||
@function getIconFontPath() {
|
||||
$mainA: '.iconfont';
|
||||
$mainB: '.icon-font';
|
||||
@function get-icon-font-path() {
|
||||
$main-a: '.iconfont';
|
||||
$main-b: '.icon-font';
|
||||
$note: '.read-icon';
|
||||
$noteEdit: '.bili-note-iconfont';
|
||||
$note-edit: '.bili-note-iconfont';
|
||||
$footer: '.bili-footer-font';
|
||||
$bangumiInfo: '[class^="icon-"]';
|
||||
$bangumi-info: '[class^="icon-"]';
|
||||
$history: '.bilifont';
|
||||
$message: '.bp-icon-font';
|
||||
$creativeCenterA: '.bcc-iconfont';
|
||||
$creativeCenterB: '.fontvt';
|
||||
@return collectPaths(
|
||||
$creative-center-a: '.bcc-iconfont';
|
||||
$creative-center-b: '.fontvt';
|
||||
$quick-favorite: 'span.quick-favorite.be-quick-favorite > i.quick-favorite-icon.icon'; // 组件-启用快速收藏的图标,参见 #4566
|
||||
@return collect-paths(
|
||||
(
|
||||
$mainA,
|
||||
$mainB,
|
||||
$main-a,
|
||||
$main-b,
|
||||
$note,
|
||||
$noteEdit,
|
||||
$note-edit,
|
||||
$footer,
|
||||
$bangumiInfo,
|
||||
$bangumi-info,
|
||||
$history,
|
||||
$message,
|
||||
$creativeCenterA,
|
||||
$creativeCenterB
|
||||
$creative-center-a,
|
||||
$creative-center-b,
|
||||
$quick-favorite
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@function getColumnPath() {
|
||||
$mainA: '.article-detail .article-container .article-container__content .article-content .read-article-holder';
|
||||
$mainB: '.article-detail .article-container .article-container__content .article-content .read-article-holder *';
|
||||
@return collectPaths(($mainA, $mainB));
|
||||
@function get-column-path() {
|
||||
$main-a: '.article-detail .article-container .article-container__content .article-content .read-article-holder';
|
||||
$main-b: '.article-detail .article-container .article-container__content .article-content .read-article-holder *';
|
||||
@return collect-paths(($main-a, $main-b));
|
||||
}
|
||||
|
||||
@function getScorePath() {
|
||||
@function get-score-path() {
|
||||
$documentary: '.season-cover .score';
|
||||
$big: '.season-cover .season-cover-score';
|
||||
@return collectPaths(($documentary, $big));
|
||||
@return collect-paths(($documentary, $big));
|
||||
}
|
||||
|
||||
// 装扮,在评论区、动态等界面显示
|
||||
$ornament: getOrnamentPath();
|
||||
$ornament: get-ornament-path();
|
||||
|
||||
// 粉丝勋章,在评论区界面显示
|
||||
$fansMedal: getFansMedalPath();
|
||||
$fans-medal: get-fans-medal-path();
|
||||
|
||||
// 弹幕,在播放器里显示
|
||||
$danmaku: getDanmakuPath();
|
||||
$danmaku: get-danmaku-path();
|
||||
|
||||
// 图标字体
|
||||
$iconFont: getIconFontPath();
|
||||
$icon-font: get-icon-font-path();
|
||||
|
||||
// 专栏阅读页可能出现的部分自定义字体
|
||||
$column: getColumnPath();
|
||||
$column: get-column-path();
|
||||
|
||||
// 分数,在部分剧集的右下角显示
|
||||
$score: getScorePath();
|
||||
$score: get-score-path();
|
||||
|
||||
// 收集所有路径
|
||||
$discardPaths: collectPaths(($ornament, $fansMedal, $danmaku, $iconFont, $column, $score));
|
||||
$discard-paths: collect-paths(($ornament, $fans-medal, $danmaku, $icon-font, $column, $score));
|
||||
|
||||
$onOptionNames: (
|
||||
'on-ornament',
|
||||
'on-fans-medal',
|
||||
'on-danmaku',
|
||||
'on-icon-font',
|
||||
'on-column',
|
||||
'on-score'
|
||||
$cover-option-names: (
|
||||
'cover-ornament',
|
||||
'cover-fans-medal',
|
||||
'cover-danmaku',
|
||||
'cover-icon-font',
|
||||
'cover-column',
|
||||
'cover-score'
|
||||
);
|
||||
$onOptionPathLists: ($ornament, $fansMedal, $danmaku, $iconFont, $column, $score);
|
||||
$onOptionGroups: zip($onOptionNames, $onOptionPathLists);
|
||||
$cover-option-path-lists: ($ornament, $fans-medal, $danmaku, $icon-font, $column, $score);
|
||||
$cover-option-groups: zip($cover-option-names, $cover-option-path-lists);
|
||||
|
||||
html[#{$name}--detect--is-input-empty='false'] {
|
||||
:not(#{$discardPaths}) {
|
||||
font-family: $fontFamilyCss;
|
||||
:not(#{$discard-paths}) {
|
||||
font-family: $font-family-css;
|
||||
}
|
||||
|
||||
@each $onOptionName, $onOptionPathList in $onOptionGroups {
|
||||
&[#{$name}--options--#{$onOptionName}='true'] :is(#{$onOptionPathList}) {
|
||||
font-family: $fontFamilyCss;
|
||||
@each $cover-option-name, $cover-option-path-list in $cover-option-groups {
|
||||
&[#{$name}--options--#{$cover-option-name}='true'] :is(#{$cover-option-path-list}) {
|
||||
font-family: $font-family-css;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +121,8 @@ body.fixed-navbar {
|
||||
top: calc(var(--navbar-height) + 8px) !important;
|
||||
}
|
||||
}
|
||||
.bili-feed4 .header-channel {
|
||||
.bili-feed4 .header-channel,
|
||||
.search-fixed-header {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -772,4 +772,26 @@
|
||||
|
||||
#{contains('live-info-content')} {
|
||||
background: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
#{contains('upower-pay-container')} {
|
||||
.header {
|
||||
@include background-color('0');
|
||||
}
|
||||
#{contains('content')} {
|
||||
@include background('2');
|
||||
#{contains('tab-group')} {
|
||||
#{contains('active')} {
|
||||
@include color('f');
|
||||
}
|
||||
}
|
||||
#{contains('month-charge-grade')} {
|
||||
background: #333;
|
||||
}
|
||||
#{contains('charge-grad-container')} {
|
||||
.charge-grad-item {
|
||||
background: linear-gradient(45deg, #333, #444);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,10 @@ const options = defineOptionsMetadata({
|
||||
displayName: '禁用UP主弹幕',
|
||||
defaultValue: true,
|
||||
},
|
||||
upSlogan: {
|
||||
displayName: '禁用带货弹幕',
|
||||
defaultValue: true,
|
||||
},
|
||||
vip: {
|
||||
displayName: '禁用大会员弹幕',
|
||||
defaultValue: true,
|
||||
|
||||
@ -37,3 +37,14 @@ body.disable-vip-danmaku-style {
|
||||
--textShadow: var(--danmaku-text-shadow);
|
||||
}
|
||||
}
|
||||
body.disable-upSlogan-danmaku-style {
|
||||
.bili-upslogan {
|
||||
padding: 0 !important;
|
||||
opacity: var(--opacity) !important;
|
||||
font-weight: var(--fontWeight) !important;
|
||||
background-color: transparent !important;
|
||||
img {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,14 +21,15 @@ export const setupFeedImageExporter: ComponentEntry<Options> = async ({
|
||||
text: '导出图片',
|
||||
action: async () => {
|
||||
const imageUrls: { url: string; extension: string }[] = []
|
||||
dqa(card.element, '.main-content .img-content, .bili-album__preview__picture__img').forEach(
|
||||
(img: HTMLImageElement | HTMLDivElement) => {
|
||||
const urlData = retrieveImageUrl(img)
|
||||
if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) {
|
||||
imageUrls.push(urlData)
|
||||
}
|
||||
},
|
||||
)
|
||||
dqa(
|
||||
card.element,
|
||||
'.main-content .img-content, .bili-album__preview__picture__img, .bili-album .preview__picture__img',
|
||||
).forEach((img: HTMLImageElement | HTMLDivElement) => {
|
||||
const urlData = retrieveImageUrl(img)
|
||||
if (urlData && !imageUrls.some(({ url }) => url === urlData.url)) {
|
||||
imageUrls.push(urlData)
|
||||
}
|
||||
})
|
||||
if (imageUrls.length === 0) {
|
||||
Toast.info('此条动态没有检测到任何图片.', '导出图片')
|
||||
return
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/* eslint-disable yoda */
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { CommentItem, CommentReplyItem } from '@/components/utils/comment-apis'
|
||||
|
||||
@ -109,14 +110,154 @@ const createSubReplyItem = function subReply(item, i) {
|
||||
return dom
|
||||
}
|
||||
|
||||
// 小黑屋专用带IP属地显示的评论创建
|
||||
const blackroomCreateListCon = function listCon(e, n, t) {
|
||||
const r = this._parentBlacklistDom(e, n, t)
|
||||
const i = [
|
||||
`<div class="con ${t === n ? 'no-border' : ''}">`,
|
||||
`<div class="user">${this._identity(e.mid, e.assist, e.member.fans_detail)}`,
|
||||
`<a data-usercard-mid="${e.mid}" href="//space.bilibili.com/${
|
||||
e.mid
|
||||
}" target="_blank" class="name ${this._createVipClass(
|
||||
e.member.vip.vipType,
|
||||
e.member.vip.vipStatus,
|
||||
e.member.vip.themeType,
|
||||
)}">${this._unhtmlFix(
|
||||
e.member.uname,
|
||||
)}</a><a class="level-link" href="//www.bilibili.com/blackboard/help.html#%E4%BC%9A%E5%91%98%E7%AD%89%E7%BA%A7%E7%9B%B8%E5%85%B3" target="_blank"><i class="level l${
|
||||
e.member.level_info.current_level
|
||||
}"></i></a>${this._createNameplate(e.member.nameplate)}${this._createUserSailing(
|
||||
(e.member && e.member.user_sailing) || {},
|
||||
)}</div>`,
|
||||
this._createMsgContent(e),
|
||||
'<div class="info">',
|
||||
e.floor ? `<span class="floor">#${e.floor}</span>` : '',
|
||||
this._createPlatformDom(e.content.plat),
|
||||
`<span class="time">${this._formateTime(e.ctime)}</span>`,
|
||||
e?.reply_control?.location
|
||||
? `<span style="margin-left:-15px;">${e?.reply_control?.location || ''}</span>`
|
||||
: '',
|
||||
e.lottery_id
|
||||
? ''
|
||||
: `<span class="like ${1 === e.action ? 'liked' : ''}"><i></i><span>${
|
||||
e.like || ''
|
||||
}</span></span>`,
|
||||
e.lottery_id ? '' : `<span class="hate ${2 === e.action ? 'hated' : ''}"><i></i></span>`,
|
||||
e.lottery_id ? '' : this._createReplyBtn(e.rcount),
|
||||
e.lottery_id && e.mid !== this.userStatus.mid
|
||||
? ''
|
||||
: `<div class="operation more-operation"><div class="spot"></div><div class="opera-list"><ul>${
|
||||
this._canSetTop(e)
|
||||
? `<li class="set-top">${e.isUpTop ? '取消置顶' : '设为置顶'}</li>`
|
||||
: ''
|
||||
}${this._canBlackList(e.mid) ? '<li class="blacklist">加入黑名单</li>' : ''}${
|
||||
this._canReport(e.mid) ? '<li class="report">举报</li>' : ''
|
||||
}${
|
||||
this._canDel(e.mid) && !e.isTop ? `<li class="del" data-mid="${e.mid}">删除</li>` : ''
|
||||
}</ul></div></div>`,
|
||||
this._createLotteryContent(e.content),
|
||||
this._createVoteContent(e.content),
|
||||
this._createTags(e),
|
||||
'</div>',
|
||||
'<div class="reply-box">',
|
||||
this._createSubReplyList(e.replies, e.rcount, !1, e.rpid, e.folder && e.folder.has_folded),
|
||||
'</div>',
|
||||
'<div class="paging-box">',
|
||||
'</div>',
|
||||
'</div>',
|
||||
].join('')
|
||||
return e.state === this.blacklistCode ? r : i
|
||||
}
|
||||
|
||||
// 小黑屋专用带IP属地显示的回复创建
|
||||
const blackroomCreateSubReplyItem = function subReply(e, n) {
|
||||
const t = [
|
||||
`<div class="reply-item reply-wrap" data-id="${e.rpid}" data-index="${n}">`,
|
||||
`<a href="//space.bilibili.com/${e.mid}" data-usercard-mid="${e.mid}" target="_blank" class="reply-face">`,
|
||||
`<img src="${this._trimHttpFix(
|
||||
this._webpFix(e.member.avatar, {
|
||||
w: 52,
|
||||
h: 52,
|
||||
}),
|
||||
)}" alt="">`,
|
||||
'</a>',
|
||||
'<div class="reply-con">',
|
||||
'<div class="user">',
|
||||
`<a href="//space.bilibili.com/${e.mid}" target="_blank" data-usercard-mid="${
|
||||
e.mid
|
||||
}" class="name ${this._createVipClass(
|
||||
e.member.vip.vipType,
|
||||
e.member.vip.vipStatus,
|
||||
e.member.vip.themeType,
|
||||
)}">${this._unhtmlFix(e.member.uname)}</a>`,
|
||||
`<a class="level-link" href="//www.bilibili.com/blackboard/help.html#%E4%BC%9A%E5%91%98%E7%AD%89%E7%BA%A7%E7%9B%B8%E5%85%B3" target="_blank"><i class="level l${e.member.level_info.current_level}"></i></a>`,
|
||||
this._createSubMsgContent(e),
|
||||
'</div>',
|
||||
'</div>',
|
||||
'<div class="info">',
|
||||
`<span class="time">${this._formateTime(e.ctime)}</span>`,
|
||||
e?.reply_control?.location
|
||||
? `<span style="margin-left:-15px;">${e?.reply_control?.location || ''}</span>`
|
||||
: '',
|
||||
`<span class="like ${1 === e.action ? 'liked' : ''}"><i></i><span>${
|
||||
e.like || ''
|
||||
}</span></span>`,
|
||||
`<span class="hate ${2 === e.action ? 'hated' : ''}"><i></i></span>`,
|
||||
'<span class="reply btn-hover">回复</span>',
|
||||
`<div class="operation btn-hover btn-hide-re"><div class="spot"></div><div class="opera-list"><ul>${
|
||||
this._canBlackList(e.mid) ? '<li class="blacklist">加入黑名单</li>' : ''
|
||||
}${this._canReport(e.mid) ? '<li class="report">举报</li>' : ''}${
|
||||
this._canDel(e.mid) ? `<li class="del" data-mid="${e.mid}">删除</li>` : ''
|
||||
}</ul></div></div>`,
|
||||
'</div>',
|
||||
'</div>',
|
||||
].join('')
|
||||
return t
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(() => {
|
||||
// 监听旧版评论区全局变量
|
||||
if (typeof unsafeWindow.bbComment !== 'undefined') {
|
||||
// 评论创建函数替换
|
||||
bbComment = unsafeWindow.bbComment
|
||||
bbComment.prototype._createListCon = createListCon
|
||||
bbComment.prototype._createSubReplyItem = createSubReplyItem
|
||||
if (unsafeWindow.location.href.split('/')[3] === 'blackroom') {
|
||||
// 小黑屋特判
|
||||
// 三个谜之函数的polyfill(不知为何无法直接调用,会报错)
|
||||
bbComment.prototype._unhtmlFix = function unhtml(e, n) {
|
||||
return e
|
||||
? // eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
e.replace(n || /[&<">'](?:(amp|lt|quot|gt|#39|nbsp|#\\d+);)?/g, function (e, n) {
|
||||
return n ? e : { '<': '', '&': '', '"': '', '>': '', "'": '' }[e]
|
||||
})
|
||||
: ''
|
||||
}
|
||||
bbComment.prototype._trimHttpFix = function trimHttp(e) {
|
||||
return e ? e.replace(/^http:/, '') : ''
|
||||
}
|
||||
bbComment.prototype._webpFix = function webp(e, n) {
|
||||
if (!e) {
|
||||
return e
|
||||
}
|
||||
const t = e.match(/(.*\\.(jpg|jpeg|gif|png|bmp))(\\?.*)?/)
|
||||
let r = -1 !== e.indexOf('/bfs/')
|
||||
if (!t || 'gif' === t[2] || 'bmp' === t[2] || !r) {
|
||||
return e
|
||||
}
|
||||
r = n.w
|
||||
e = n.h
|
||||
e = r && e ? `@${r}w_${e}h` : '@'
|
||||
n = t[3] || ''
|
||||
return this.isWebp ? `${t[1] + e}.webp${n}` : `${t[1] + e}.${t[2]}${n}`
|
||||
}
|
||||
|
||||
bbComment.prototype._createListCon = blackroomCreateListCon
|
||||
bbComment.prototype._createSubReplyItem = blackroomCreateSubReplyItem
|
||||
} else {
|
||||
// 其他情况旧版评论区替换
|
||||
bbComment.prototype._createListCon = createListCon
|
||||
bbComment.prototype._createSubReplyItem = createSubReplyItem
|
||||
}
|
||||
version = 1
|
||||
observer.disconnect()
|
||||
}
|
||||
|
||||
@ -3,23 +3,30 @@ import { getComponentSettings } from '@/core/settings'
|
||||
import { registerAndGetData } from '@/plugins/data'
|
||||
import { Options } from '.'
|
||||
import { KeyBindingAction, KeyBindingActionContext } from './bindings'
|
||||
import { simulateClick } from '@/core/utils'
|
||||
|
||||
export const keyboardEventToPointer = (event: KeyboardEvent): PointerEventInit => {
|
||||
return {
|
||||
...lodash.pick(event, 'ctrlKey', 'shiftKey', 'altKey', 'metaKey'),
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: unsafeWindow,
|
||||
}
|
||||
}
|
||||
export const clickElement = (target: string | HTMLElement, context: KeyBindingActionContext) => {
|
||||
const { event } = context
|
||||
const mouseEvent = new MouseEvent('click', {
|
||||
...lodash.pick(event, 'ctrlKey', 'shiftKey', 'altKey', 'metaKey'),
|
||||
})
|
||||
const eventParams = keyboardEventToPointer(event)
|
||||
if (typeof target === 'string') {
|
||||
const targetElement = dq(target) as HTMLElement
|
||||
if (!targetElement) {
|
||||
return false
|
||||
}
|
||||
targetElement.dispatchEvent(mouseEvent)
|
||||
simulateClick(targetElement, eventParams)
|
||||
} else {
|
||||
if (!target) {
|
||||
return false
|
||||
}
|
||||
target.dispatchEvent(mouseEvent)
|
||||
simulateClick(target, eventParams)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -82,7 +89,8 @@ export const builtInActions: Record<string, KeyBindingAction> = {
|
||||
volumeUp: {
|
||||
displayName: '增加音量',
|
||||
run: () => {
|
||||
const volume = playerAgent.changeVolume(10)
|
||||
const step = getComponentSettings<Options>('keymap').options.volumeStep
|
||||
const volume = playerAgent.changeVolume(step)
|
||||
if (lodash.isNil(volume)) {
|
||||
return volume
|
||||
}
|
||||
@ -93,7 +101,8 @@ export const builtInActions: Record<string, KeyBindingAction> = {
|
||||
volumeDown: {
|
||||
displayName: '降低音量',
|
||||
run: () => {
|
||||
const volume = playerAgent.changeVolume(-10)
|
||||
const step = getComponentSettings<Options>('keymap').options.volumeStep
|
||||
const volume = playerAgent.changeVolume(-step)
|
||||
if (lodash.isNil(volume)) {
|
||||
return volume
|
||||
}
|
||||
@ -127,7 +136,7 @@ export const builtInActions: Record<string, KeyBindingAction> = {
|
||||
coin: {
|
||||
displayName: '投币',
|
||||
run: useClickElement(
|
||||
'.video-toolbar .coin, .tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2), .video-toolbar-v1 .coin',
|
||||
'.video-toolbar .coin, .tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2), .video-toolbar-v1 .coin, .toolbar .coin',
|
||||
),
|
||||
},
|
||||
favorite: {
|
||||
@ -143,31 +152,32 @@ export const builtInActions: Record<string, KeyBindingAction> = {
|
||||
like: {
|
||||
displayName: '点赞',
|
||||
run: (() => {
|
||||
/** 长按`L`三连使用的记忆变量 */
|
||||
/** 长按 `L` 三连使用的记忆变量 */
|
||||
let likeClick = true
|
||||
return (context: KeyBindingActionContext) => {
|
||||
const { event } = context
|
||||
const likeButton = dq(
|
||||
'.video-toolbar .like, .tool-bar .like-info, .video-toolbar-v1 .like',
|
||||
'.video-toolbar .like, .tool-bar .like-info, .video-toolbar-v1 .like, .toolbar .like',
|
||||
) as HTMLSpanElement
|
||||
if (!likeButton) {
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
const fireEvent = (name: string, args: Event) => {
|
||||
const customEvent = new CustomEvent(name, args)
|
||||
likeButton.dispatchEvent(customEvent)
|
||||
const fireMouseEvent = (name: string, source: KeyboardEvent) => {
|
||||
const eventParams: MouseEventInit = keyboardEventToPointer(source)
|
||||
const mouseEvent = new MouseEvent(name, eventParams)
|
||||
likeButton.dispatchEvent(mouseEvent)
|
||||
}
|
||||
likeClick = true
|
||||
setTimeout(() => (likeClick = false), 200)
|
||||
fireEvent('mousedown', event)
|
||||
fireMouseEvent('mousedown', event)
|
||||
document.body.addEventListener(
|
||||
'keyup',
|
||||
e => {
|
||||
e.preventDefault()
|
||||
fireEvent('mouseup', e)
|
||||
fireMouseEvent('mouseup', e)
|
||||
if (likeClick) {
|
||||
fireEvent('click', e)
|
||||
fireMouseEvent('click', e)
|
||||
}
|
||||
},
|
||||
{ once: true },
|
||||
|
||||
@ -9,11 +9,18 @@ import { addComponentListener } from '@/core/settings'
|
||||
import { actions } from './actions'
|
||||
import { KeyBinding, KeyBindingConfig, loadKeyBindings } from './bindings'
|
||||
import { presetBase, presets } from './presets'
|
||||
import { getNumberValidator } from '@/core/utils'
|
||||
|
||||
const options = defineOptionsMetadata({
|
||||
longJumpSeconds: {
|
||||
defaultValue: 85,
|
||||
displayName: '长跳跃秒数',
|
||||
validator: getNumberValidator(1),
|
||||
},
|
||||
volumeStep: {
|
||||
defaultValue: 10,
|
||||
displayName: '音量调整幅度',
|
||||
validator: getNumberValidator(1, 100),
|
||||
},
|
||||
customKeyBindings: {
|
||||
defaultValue: {} as Record<string, string>,
|
||||
|
||||
@ -32,7 +32,8 @@ body.remove-game-match-module #reportFirst3,
|
||||
.video-page-game-card-small,
|
||||
.video-page-operator-card-small,
|
||||
[data-be-promotion-mark],
|
||||
body:not(.preserve-feed-goods) .dyn-goods {
|
||||
body:not(.preserve-feed-goods) .dyn-goods,
|
||||
.desktop-download-tip {
|
||||
display: none !important;
|
||||
}
|
||||
.recommend-list .rec-list > :not(.video-page-card) + .video-page-card {
|
||||
@ -83,28 +84,6 @@ body.dark .blocked-ads.new {
|
||||
body:not(.preserve-feed-goods) .bili-dyn-content__orig__additional:has(.dyn-goods) {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.bili-video-card__wrap {
|
||||
a[href*='cm.bilibili.com'] {
|
||||
pointer-events: none !important;
|
||||
cursor: default !important;
|
||||
|
||||
> * {
|
||||
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;
|
||||
}
|
||||
}
|
||||
.bili-video-card__wrap:has(a[href*='cm.bilibili.com']) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
52
registry/lib/components/utils/view-cover/Plugin.vue
Normal file
52
registry/lib/components/utils/view-cover/Plugin.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div class="download-cover-config download-video-config-section">
|
||||
<div class="download-video-config-item">
|
||||
<div class="download-video-config-title">封面:</div>
|
||||
<VDropdown v-model="type" :items="items">
|
||||
<template #item="{ item }">
|
||||
{{ item }}
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import { UnknownOptions } from '@/components/component'
|
||||
import { VDropdown } from '@/ui'
|
||||
import { CoverDownloadType } from './types'
|
||||
|
||||
interface Options extends UnknownOptions {
|
||||
CoverType: CoverDownloadType | '无'
|
||||
}
|
||||
const { options } = getComponentSettings<Options>('downloadVideo')
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
VDropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: options.CoverType ?? '无',
|
||||
items: ['无', 'jpg'],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enabled() {
|
||||
return this.type !== '无'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
type(newValue: CoverDownloadType) {
|
||||
options.CoverType = newValue
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.download-cover-config.download-video-config-section {
|
||||
.be-dropdown {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -9,12 +9,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { getVideoCoverUrlByAid } from '@/components/video/video-cover'
|
||||
import { getJson } from '@/core/ajax'
|
||||
import { videoChange } from '@/core/observer'
|
||||
import { select } from '@/core/spin-query'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { showImage, DefaultWidget } from '@/ui'
|
||||
import { VideoInfo } from '@/components/video/video-info'
|
||||
|
||||
export default Vue.extend({
|
||||
components: {
|
||||
@ -29,14 +28,7 @@ export default Vue.extend({
|
||||
if (!document.URL.includes('live.bilibili.com')) {
|
||||
videoChange(async () => {
|
||||
const { aid } = unsafeWindow
|
||||
const videoInfo = new VideoInfo(aid)
|
||||
try {
|
||||
await videoInfo.fetchInfo()
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
throw error
|
||||
}
|
||||
this.imageUrl = videoInfo.coverUrl.replace('http:', 'https:')
|
||||
this.imageUrl = await getVideoCoverUrlByAid(aid)
|
||||
})
|
||||
} else {
|
||||
const spaceElementSelector = '.header-info-ctnr .room-cover, .header-info-ctnr .avatar'
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import { defineComponentMetadata } from '@/components/define'
|
||||
import { getBlobByAid } from '@/components/video/video-cover'
|
||||
import { PackageEntry } from '@/core/download'
|
||||
import { videoAndBangumiUrls } from '@/core/utils/urls'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { DownloadVideoAssets } from '../../video/download/types'
|
||||
import { CoverDownloadType } from './types'
|
||||
|
||||
export const component = defineComponentMetadata({
|
||||
name: 'viewCover',
|
||||
@ -12,6 +17,49 @@ export const component = defineComponentMetadata({
|
||||
entry: none,
|
||||
reload: none,
|
||||
unload: none,
|
||||
plugin: {
|
||||
displayName: '下载视频 - 下载封面支持',
|
||||
setup: ({ addData }) => {
|
||||
addData('downloadVideo.assets', async (assets: DownloadVideoAssets[]) => {
|
||||
assets.push({
|
||||
name: 'downloadCover',
|
||||
displayName: '下载封面',
|
||||
getAssets: async (
|
||||
infos,
|
||||
instance: {
|
||||
type: CoverDownloadType
|
||||
enabled: boolean
|
||||
},
|
||||
) => {
|
||||
const { type, enabled } = instance
|
||||
if (!enabled) {
|
||||
return []
|
||||
}
|
||||
const toast = Toast.info('获取封面中...', '下载封面')
|
||||
let downloadedItemCount = 0
|
||||
const results = await Promise.allSettled(
|
||||
infos.map(async info => {
|
||||
const blob = await getBlobByAid(info.input.aid)
|
||||
downloadedItemCount++
|
||||
toast.message = `获取封面中... (${downloadedItemCount}/${infos.length})`
|
||||
return {
|
||||
name: `${info.input.title}.${type}`,
|
||||
data: blob,
|
||||
}
|
||||
}),
|
||||
)
|
||||
const success = results.filter(
|
||||
it => it.status === 'fulfilled',
|
||||
) as PromiseFulfilledResult<PackageEntry>[]
|
||||
const fail = results.filter(it => it.status === 'rejected') as PromiseRejectedResult[]
|
||||
toast.message = `获取完成. 成功 ${success.length} 个, 失败 ${fail.length} 个.`
|
||||
return success.map(it => it.value)
|
||||
},
|
||||
component: () => import('./Plugin.vue').then(m => m.default),
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
widget: {
|
||||
component: () => import('./ViewCover.vue').then(m => m.default),
|
||||
},
|
||||
|
||||
1
registry/lib/components/utils/view-cover/types.ts
Normal file
1
registry/lib/components/utils/view-cover/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type CoverDownloadType = 'jpg'
|
||||
@ -0,0 +1,3 @@
|
||||
将稍后再看的链接重定向为普通播放网址.
|
||||
- `重定向页面`: 对稍后再看列表页面里的链接重定向.
|
||||
- `重定向顶栏`: 对 `自定义顶栏` 里的稍后再看链接重定向.
|
||||
@ -51,9 +51,6 @@ const entry: ComponentEntry = async ({ settings }) => {
|
||||
export const component = defineComponentMetadata({
|
||||
name: 'watchlaterRedirect',
|
||||
displayName: '稍后再看重定向',
|
||||
description: {
|
||||
'zh-CN': '将稍后再看的链接重定向为普通播放网址.',
|
||||
},
|
||||
entry,
|
||||
options: {
|
||||
page: {
|
||||
|
||||
@ -5,6 +5,7 @@ import { getFriendlyTitle } from '@/core/utils/title'
|
||||
import { DanmakuConverterConfig, DanmakuConverter } from '../converter/danmaku-converter'
|
||||
import { DanmakuType } from '../converter/danmaku-type'
|
||||
import { XmlDanmaku } from '../converter/xml-danmaku'
|
||||
import { playerAgent } from '@/components/video/player-agent'
|
||||
|
||||
export class JsonDanmaku {
|
||||
// static SegmentSize = 6 * 60
|
||||
@ -97,9 +98,6 @@ export const getUserDanmakuConfig = async () => {
|
||||
const playerSettingsJson = localStorage.getItem('bilibili_player_settings')
|
||||
|
||||
if (playerSettingsJson) {
|
||||
const playerSettings = JSON.parse(playerSettingsJson)
|
||||
const getConfig = <T>(prop: string, defaultValue?: T): T =>
|
||||
lodash.get(playerSettings, `setting_config.${prop}`, defaultValue)
|
||||
// 屏蔽类型
|
||||
config.blockTypes = (() => {
|
||||
const result: (DanmakuType | 'color')[] = []
|
||||
@ -111,7 +109,7 @@ export const getUserDanmakuConfig = async () => {
|
||||
}
|
||||
|
||||
for (const [type, value] of Object.entries(blockValues)) {
|
||||
if (lodash.get(playerSettings, `block.type_${type}`, true) === false) {
|
||||
if (playerAgent.getPlayerConfig<boolean>(`block.type_${type}`, true) === false) {
|
||||
result.push(...(value as (DanmakuType | 'color')[]))
|
||||
}
|
||||
}
|
||||
@ -119,13 +117,17 @@ export const getUserDanmakuConfig = async () => {
|
||||
})()
|
||||
|
||||
// 加粗
|
||||
config.bold = getConfig('bold', false)
|
||||
config.bold = playerAgent.getPlayerConfig('bold', false)
|
||||
|
||||
// 透明度
|
||||
config.alpha = lodash.clamp(1 - parseFloat(getConfig('opacity', '0.4')), 0, 1)
|
||||
config.alpha = lodash.clamp(
|
||||
1 - parseFloat(playerAgent.getPlayerConfig('opacity', '0.4')),
|
||||
0,
|
||||
1,
|
||||
)
|
||||
|
||||
// 分辨率
|
||||
const resolutionFactor = 1.4 - 0.4 * getConfig('fontsize', 1)
|
||||
const resolutionFactor = 1.4 - 0.4 * playerAgent.getPlayerConfig('fontsize', 1)
|
||||
config.resolution = {
|
||||
x: Math.round(1920 * resolutionFactor),
|
||||
y: Math.round(1080 * resolutionFactor),
|
||||
@ -133,7 +135,7 @@ export const getUserDanmakuConfig = async () => {
|
||||
|
||||
// 弹幕持续时长
|
||||
config.duration = (() => {
|
||||
const scrollDuration = 18 - 3 * getConfig('speedplus', 0)
|
||||
const scrollDuration = 18 - 3 * playerAgent.getPlayerConfig('speedplus', 0)
|
||||
return (danmaku: { type: number }) => {
|
||||
switch (danmaku.type) {
|
||||
case 4:
|
||||
@ -146,23 +148,25 @@ export const getUserDanmakuConfig = async () => {
|
||||
})()
|
||||
|
||||
// 底部间距
|
||||
const bottomMargin = getConfig('danmakuArea', 0)
|
||||
const bottomMargin = playerAgent.getPlayerConfig('danmakuArea', 0)
|
||||
config.bottomMarginPercent = bottomMargin >= 100 ? 0 : bottomMargin / 100
|
||||
// 无显示区域限制时要检查是否开启防挡字幕
|
||||
if (config.bottomMarginPercent === 0 && getConfig('preventshade', false)) {
|
||||
if (config.bottomMarginPercent === 0 && playerAgent.getPlayerConfig('preventshade', false)) {
|
||||
config.bottomMarginPercent = 0.15
|
||||
}
|
||||
|
||||
// 用户屏蔽词
|
||||
const blockSettings = lodash.get(playerSettings, 'block.list', []) as {
|
||||
/** 类型 */
|
||||
t: 'keyword' | 'regexp' | 'user'
|
||||
/** 内容 */
|
||||
v: string
|
||||
/** 是否开启 */
|
||||
s: boolean
|
||||
id: number
|
||||
}[]
|
||||
const blockSettings = playerAgent.getPlayerConfig<
|
||||
{
|
||||
/** 类型 */
|
||||
t: 'keyword' | 'regexp' | 'user'
|
||||
/** 内容 */
|
||||
v: string
|
||||
/** 是否开启 */
|
||||
s: boolean
|
||||
id: number
|
||||
}[]
|
||||
>('block.list', [])
|
||||
config.blockFilter = danmaku => {
|
||||
for (const b of blockSettings) {
|
||||
if (!b.s) {
|
||||
|
||||
@ -95,7 +95,7 @@ import { allQualities, VideoQuality } from '@/components/video/video-quality'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { getFriendlyTitle } from '@/core/utils/title'
|
||||
import { bangumiBatchInput } from './inputs/bangumi/batch'
|
||||
import { videoBatchInput } from './inputs/video/batch'
|
||||
import { videoBatchInput, videoSeasonBatchInput } from './inputs/video/batch'
|
||||
import { videoSingleInput } from './inputs/video/input'
|
||||
import { videoDashAvc, videoDashHevc, videoDashAv1, videoAudioDash } from './apis/dash'
|
||||
import { videoFlv } from './apis/flv'
|
||||
@ -113,6 +113,7 @@ import {
|
||||
const [inputs] = registerAndGetData('downloadVideo.inputs', [
|
||||
videoSingleInput,
|
||||
videoBatchInput,
|
||||
videoSeasonBatchInput,
|
||||
bangumiBatchInput,
|
||||
] as DownloadVideoInput[])
|
||||
const [apis] = registerAndGetData('downloadVideo.apis', [
|
||||
|
||||
@ -33,6 +33,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="episodes-picker-items">
|
||||
<div v-if="episodeItems.length === 0" class="episodes-picker-empty">
|
||||
<VEmpty />
|
||||
</div>
|
||||
<div v-for="(item, index) of episodeItems" :key="item.key" class="episodes-picker-item">
|
||||
<CheckBox
|
||||
v-model="item.isChecked"
|
||||
@ -54,7 +57,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { VButton, VIcon, CheckBox } from '@/ui'
|
||||
import { VButton, VIcon, CheckBox, VEmpty } from '@/ui'
|
||||
import { EpisodeItem } from './episode-item'
|
||||
|
||||
export default Vue.extend({
|
||||
@ -62,6 +65,7 @@ export default Vue.extend({
|
||||
VButton,
|
||||
VIcon,
|
||||
CheckBox,
|
||||
VEmpty,
|
||||
},
|
||||
props: {
|
||||
api: {
|
||||
@ -171,5 +175,10 @@ export default Vue.extend({
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
&-empty {
|
||||
@include h-center();
|
||||
justify-content: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -5,9 +5,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { getVideoCoverUrlByAid } from '@/components/video/video-cover'
|
||||
import { videoChange } from '@/core/observer'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { VideoInfo } from '@/components/video/video-info'
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
@ -18,14 +17,7 @@ export default Vue.extend({
|
||||
created() {
|
||||
videoChange(async () => {
|
||||
const { aid } = unsafeWindow
|
||||
const videoInfo = new VideoInfo(aid)
|
||||
try {
|
||||
await videoInfo.fetchInfo()
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
throw error
|
||||
}
|
||||
this.imageUrl = videoInfo.coverUrl.replace('http:', 'https:')
|
||||
this.imageUrl = await getVideoCoverUrlByAid(aid)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@ -49,3 +49,52 @@ export const videoBatchInput: DownloadVideoInput = {
|
||||
})
|
||||
}),
|
||||
}
|
||||
export const videoSeasonBatchInput: DownloadVideoInput = {
|
||||
name: 'video.seasonBatch',
|
||||
displayName: '当前视频 (合集)',
|
||||
match: videoUrls,
|
||||
batch: true,
|
||||
getInputs: async instance => instance?.checkedInputItems ?? [],
|
||||
component: async () =>
|
||||
createEpisodesPicker(async instance => {
|
||||
const { aid, bvid } = unsafeWindow
|
||||
const api = `https://api.bilibili.com/x/web-interface/wbi/view/detail?bvid=${bvid}&aid=${aid}`
|
||||
const json = await getJsonWithCredentials(api)
|
||||
if (json.code !== 0) {
|
||||
logError(`获取视频合集列表失败, message = ${json.message}`)
|
||||
return []
|
||||
}
|
||||
const sections: { episodes: any[] }[] = lodash.get(json, 'data.View.ugc_season.sections', [])
|
||||
if (sections.length === 0) {
|
||||
return []
|
||||
}
|
||||
const totalEpisodesLength = lodash.sumBy(sections, it => it.episodes.length)
|
||||
return sections.flatMap((section, sectionIndex) => {
|
||||
const { episodes = [] } = section
|
||||
return episodes.map((episode, episodeIndex) => {
|
||||
const currentIndex =
|
||||
episodeIndex + lodash.sumBy(sections.slice(0, sectionIndex), it => it.episodes.length)
|
||||
const key = episode.cid
|
||||
const page = currentIndex + 1
|
||||
const title = `P${page} ${episode.title}`
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
isChecked: currentIndex < instance.maxCheckedItems,
|
||||
durationText: formatDuration(episode.arc.duration),
|
||||
inputItem: {
|
||||
allowQualityDrop: true,
|
||||
title: formatTitle(getGeneralSettings().batchFilenameFormat, false, {
|
||||
title: episode.page.part,
|
||||
cid: episode.cid,
|
||||
n: formatNumber(page, totalEpisodesLength),
|
||||
ep: episode.title,
|
||||
}),
|
||||
cid: episode.cid,
|
||||
aid: episode.aid,
|
||||
},
|
||||
} as EpisodeItem
|
||||
})
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
@ -51,5 +51,9 @@ export const component = defineComponentMetadata({
|
||||
defaultValue: true,
|
||||
displayName: '预告',
|
||||
},
|
||||
promotions: {
|
||||
defaultValue: true,
|
||||
displayName: '心动',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -50,5 +50,12 @@ body {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
// https://www.bilibili.com/video/BV1Qe411d7LM/?t=221
|
||||
&-promotions {
|
||||
.bili-cmtime,
|
||||
.bpx-player-cmtime {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,17 @@ import { Toast } from '@/core/toast'
|
||||
import { getFriendlyTitle } from '@/core/utils/title'
|
||||
import { SubtitleConverterConfig } from '../subtitle-converter'
|
||||
|
||||
export interface SubtitleSettings {
|
||||
bilingual: boolean
|
||||
color: string
|
||||
fade: boolean
|
||||
fontsize: string
|
||||
lan: string
|
||||
opacity: number
|
||||
position: string
|
||||
scale: boolean
|
||||
shadow: string
|
||||
}
|
||||
export interface SubtitleInfo {
|
||||
id: number
|
||||
id_str: string
|
||||
@ -20,14 +31,10 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
|
||||
'../subtitle-converter'
|
||||
)
|
||||
const { playerAgent } = await import('@/components/video/player-agent')
|
||||
const isBpxPlayer = dq('.bpx-player-video-wrap')
|
||||
const playerSettingsText = isBpxPlayer
|
||||
? localStorage.getItem('bpx_player_profile')
|
||||
: localStorage.getItem('bilibili_player_settings')
|
||||
if (!playerSettingsText) {
|
||||
const subtitleSettings = playerAgent.getPlayerConfig<null, SubtitleSettings>('subtitle', null)
|
||||
if (!subtitleSettings) {
|
||||
return [SubtitleConverter.defaultConfig, '']
|
||||
}
|
||||
const subtitleSettings = JSON.parse(playerSettingsText).subtitle
|
||||
const language = subtitleSettings.lan
|
||||
const title = getFriendlyTitle(true)
|
||||
const fontSizeMapping: { [key: number]: number } = {
|
||||
@ -38,11 +45,8 @@ export const getSubtitleConfig = async (): Promise<[SubtitleConverterConfig, str
|
||||
1.6: SubtitleSize.VeryLarge,
|
||||
}
|
||||
const size = fontSizeMapping[subtitleSettings.fontsize]
|
||||
const color =
|
||||
typeof subtitleSettings.color === 'number'
|
||||
? subtitleSettings.color.toString(16)
|
||||
: parseInt(subtitleSettings.color).toString(16)
|
||||
const opacity = subtitleSettings.backgroundopacity ?? subtitleSettings.opacity
|
||||
const color = parseInt(subtitleSettings.color).toString(16)
|
||||
const { opacity } = subtitleSettings
|
||||
|
||||
const positions = {
|
||||
bc: SubtitleLocation.BottomCenter,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
移除动态里的充电专属动态, 装有 `动态过滤器` 时生效.
|
||||
24
registry/lib/plugins/feeds/filter/hide-charge-feeds/index.ts
Normal file
24
registry/lib/plugins/feeds/filter/hide-charge-feeds/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { PluginMetadata } from '@/plugins/plugin'
|
||||
|
||||
export const plugin: PluginMetadata = {
|
||||
name: 'feedsFilter.pluginBlocks.chargeFeeds',
|
||||
displayName: '动态过滤器 - 移除充电专属动态',
|
||||
async setup() {
|
||||
const { forEachFeedsCard, getVueData } = await import('@/components/feeds/api')
|
||||
forEachFeedsCard({
|
||||
added: card => {
|
||||
const vueData = getVueData(card.element)
|
||||
const blockedType: number | null = lodash.get(
|
||||
vueData,
|
||||
'data.modules.module_dynamic.major.blocked.blocked_type',
|
||||
null,
|
||||
)
|
||||
const isBlockedByCharge = blockedType === 3
|
||||
if (!isBlockedByCharge) {
|
||||
return
|
||||
}
|
||||
card.element.classList.add('plugin-block')
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
提供一个 "空" 的输出选项, 只想获取下载视频的附带产物 (弹幕, 字幕等) 时可以使用此插件.
|
||||
17
registry/lib/plugins/video/download/empty-output/index.ts
Normal file
17
registry/lib/plugins/video/download/empty-output/index.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import { DownloadVideoOutput } from '../../../../components/video/download/types'
|
||||
|
||||
export const plugin: PluginMetadata = {
|
||||
name: 'downloadVideo.outputs.empty',
|
||||
displayName: '下载视频 - 空输出',
|
||||
setup: ({ addData }) => {
|
||||
addData('downloadVideo.outputs', (outputs: DownloadVideoOutput[]) => {
|
||||
outputs.push({
|
||||
name: 'empty',
|
||||
displayName: '空',
|
||||
description: '不输出视频本身, 仅获取下载视频的附带产物.',
|
||||
runAction: async () => lodash.noop(),
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
47
registry/lib/plugins/video/download/mpv-output-ex/Config.vue
Normal file
47
registry/lib/plugins/video/download/mpv-output-ex/Config.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="mpv-ex config">
|
||||
<div class="config-item">
|
||||
<span class="item-name">api_dev_key:</span>
|
||||
<TextBox v-model="api_dev_key" @blur="onChange" />
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="item-name">api_user_key:</span>
|
||||
<TextBox v-model="api_user_key" @blur="onChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { getComponentSettings } from '@/core/settings'
|
||||
import { TextBox } from '@/ui'
|
||||
|
||||
const { options } = getComponentSettings<ConfigDataType>('downloadVideo')
|
||||
|
||||
export default Vue.extend<ConfigDataType>({
|
||||
components: {
|
||||
TextBox,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
...lodash.pick(options, ['api_dev_key', 'api_user_key']),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange() {
|
||||
Object.assign(options, lodash.pick(this, ['api_dev_key', 'api_user_key']))
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import 'common';
|
||||
|
||||
.mpv-ex.config {
|
||||
align-self: stretch;
|
||||
@include v-stretch(12px);
|
||||
> .config-item {
|
||||
@include h-center(8px);
|
||||
> .item-name {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
71
registry/lib/plugins/video/download/mpv-output-ex/handler.ts
Normal file
71
registry/lib/plugins/video/download/mpv-output-ex/handler.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { monkey } from '@/core/ajax'
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { DownloadVideoOutput } from '../../../../components/video/download/types'
|
||||
|
||||
const getPastebinUrl = async (str: string, config: ConfigDataType) => {
|
||||
const sp = new URLSearchParams()
|
||||
Object.entries({
|
||||
api_dev_key: config.api_dev_key,
|
||||
api_user_key: config.api_user_key, // ! 必须绑定一个账号,否则链接可能被吞
|
||||
api_folder_key: 'm3u',
|
||||
api_option: 'paste',
|
||||
api_paste_code: str,
|
||||
api_paste_private: '1',
|
||||
api_paste_expire_date: '10M',
|
||||
}).forEach(([key, value]) => sp.append(key, value))
|
||||
const response = await monkey<string>({
|
||||
url: 'https://pastebin.com/api/api_post.php',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
data: sp.toString(),
|
||||
nocache: true,
|
||||
responseType: 'text',
|
||||
fetch: true,
|
||||
})
|
||||
if (/^Bad API request,/.test(response)) {
|
||||
throw response
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const MPV_Ex: DownloadVideoOutput<ConfigDataType> = {
|
||||
name: 'mpv-ex',
|
||||
displayName: 'MPV 输出支持加强版',
|
||||
description:
|
||||
'多文件时格式选择 flv,使用前请先阅读 <a href="https://github.com/Asukaaaaaa/tricks/blob/main/Bilibili-Evolved%20mpv-ex%20%E6%8F%92%E4%BB%B6.md" target="blank">README</a>',
|
||||
runAction: async (action, instance) => {
|
||||
let finalURL: string
|
||||
const mpv_protocol = 'mpv://--http-header-fields="referer:https://www.bilibili.com/"'
|
||||
if (action.isSingleVideo) {
|
||||
// 单文件直接生成 url
|
||||
const frag = action.infos[0].fragments
|
||||
if (frag.length === 1) {
|
||||
finalURL = `${mpv_protocol} ${frag[0].url}`
|
||||
} else if (frag.length === 2) {
|
||||
finalURL = `${mpv_protocol} ${frag[0].url} --audio-file=${frag[1].url}`
|
||||
}
|
||||
} else {
|
||||
// 多文件生成 .m3u 播放列表
|
||||
const m3u = action.infos.reduce((acc, v) => {
|
||||
return `${acc}#EXTINF:-1,${v.input.title}\n${v.fragments[0].url}\n` // ? 暂不支持外挂音轨,默认取第一个资源的链接
|
||||
}, '#EXTM3U\n')
|
||||
try {
|
||||
const playlist = (await getPastebinUrl(m3u, instance)).replace(
|
||||
/^(https:\/\/pastebin.com)\/(.*)/,
|
||||
'$1/raw/$2?.m3u',
|
||||
)
|
||||
finalURL = `${mpv_protocol} ${playlist}`
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log(finalURL)
|
||||
window.open(finalURL)
|
||||
},
|
||||
component: () => import('./Config.vue').then(m => m.default),
|
||||
}
|
||||
19
registry/lib/plugins/video/download/mpv-output-ex/index.ts
Normal file
19
registry/lib/plugins/video/download/mpv-output-ex/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import { DownloadVideoOutput } from '../../../../components/video/download/types'
|
||||
import { MPV_Ex } from './handler'
|
||||
|
||||
export const plugin: PluginMetadata = {
|
||||
name: 'downloadVideo.outputs.mpv-ex',
|
||||
displayName: '下载视频 - MPV 输出支持加强版',
|
||||
author: {
|
||||
name: 'asuaaa',
|
||||
link: 'https://github.com/Asukaaaaaa',
|
||||
},
|
||||
description:
|
||||
'为下载视频增加 MPV 输出,支持导出列表, 配置方式请参考 [README](https://github.com/Asukaaaaaa/tricks/blob/main/Bilibili-Evolved%20mpv-ex%20%E6%8F%92%E4%BB%B6.md)',
|
||||
setup: ({ addData }) => {
|
||||
addData('downloadVideo.outputs', (outputs: DownloadVideoOutput[]) => {
|
||||
outputs.push(MPV_Ex)
|
||||
})
|
||||
},
|
||||
}
|
||||
4
registry/lib/plugins/video/download/mpv-output-ex/type.d.ts
vendored
Normal file
4
registry/lib/plugins/video/download/mpv-output-ex/type.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
type ConfigDataType = {
|
||||
api_dev_key: string
|
||||
api_user_key: string
|
||||
}
|
||||
191
registry/lib/plugins/video/download/wasm-output/ffmpeg.ts
Normal file
191
registry/lib/plugins/video/download/wasm-output/ffmpeg.ts
Normal file
@ -0,0 +1,191 @@
|
||||
/*
|
||||
MIT License
|
||||
Copyright (c) 2019 Jerome Wu
|
||||
|
||||
Modified 2023 WakelessSloth56
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
const messageId = (() => {
|
||||
let messageID = 0
|
||||
return () => messageID++
|
||||
})()
|
||||
|
||||
enum FFMessageType {
|
||||
LOAD = 'LOAD',
|
||||
EXEC = 'EXEC',
|
||||
WRITE_FILE = 'WRITE_FILE',
|
||||
READ_FILE = 'READ_FILE',
|
||||
ERROR = 'ERROR',
|
||||
}
|
||||
|
||||
export class FFmpeg {
|
||||
#worker: Worker | null = null
|
||||
#resolves: Callbacks = {}
|
||||
#rejects: Callbacks = {}
|
||||
|
||||
public loaded = false
|
||||
|
||||
#registerHandlers = () => {
|
||||
if (this.#worker) {
|
||||
this.#worker.onmessage = ({ data: { id, type, data } }: FFMessageEventCallback) => {
|
||||
switch (type) {
|
||||
case FFMessageType.LOAD:
|
||||
this.loaded = true
|
||||
this.#resolves[id](data)
|
||||
break
|
||||
case FFMessageType.EXEC:
|
||||
case FFMessageType.WRITE_FILE:
|
||||
case FFMessageType.READ_FILE:
|
||||
this.#resolves[id](data)
|
||||
break
|
||||
case FFMessageType.ERROR:
|
||||
this.#rejects[id](data)
|
||||
break
|
||||
default:
|
||||
// https://github.com/the1812/Bilibili-Evolved/pull/4521#discussion_r1402041877
|
||||
break
|
||||
}
|
||||
delete this.#resolves[id]
|
||||
delete this.#rejects[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#send = (
|
||||
{ type, data }: Message,
|
||||
trans: Transferable[] = [],
|
||||
signal?: AbortSignal,
|
||||
): Promise<CallbackData> => {
|
||||
if (!this.#worker) {
|
||||
return Promise.reject(new Error('FFmpeg is not loaded'))
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = messageId()
|
||||
this.#worker && this.#worker.postMessage({ id, type, data }, trans)
|
||||
this.#resolves[id] = resolve
|
||||
this.#rejects[id] = reject
|
||||
|
||||
signal?.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
reject(new DOMException(`Message # ${id} was aborted`, 'AbortError'))
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public load = (config: FFMessageLoadConfig, signal?: AbortSignal) => {
|
||||
if (!this.#worker) {
|
||||
this.#worker = new Worker(config.workerLoadURL, { type: 'classic' })
|
||||
this.#registerHandlers()
|
||||
}
|
||||
return this.#send(
|
||||
{
|
||||
type: FFMessageType.LOAD,
|
||||
data: config,
|
||||
},
|
||||
undefined,
|
||||
signal,
|
||||
) as Promise<boolean>
|
||||
}
|
||||
|
||||
public exec = (args: string[], timeout = -1, signal?: AbortSignal) =>
|
||||
this.#send(
|
||||
{
|
||||
type: FFMessageType.EXEC,
|
||||
data: { args, timeout },
|
||||
},
|
||||
undefined,
|
||||
signal,
|
||||
) as unknown as Promise<number>
|
||||
|
||||
public terminate = () => {
|
||||
const ids = Object.keys(this.#rejects)
|
||||
for (const id of ids) {
|
||||
this.#rejects[id](new Error('FFmpeg terminated'))
|
||||
delete this.#rejects[id]
|
||||
delete this.#resolves[id]
|
||||
}
|
||||
|
||||
if (this.#worker) {
|
||||
this.#worker.terminate()
|
||||
this.#worker = null
|
||||
this.loaded = false
|
||||
}
|
||||
}
|
||||
|
||||
public writeFile = (path: string, data: Uint8Array, signal?: AbortSignal) => {
|
||||
const trans: Transferable[] = []
|
||||
trans.push(data.buffer)
|
||||
return this.#send(
|
||||
{
|
||||
type: FFMessageType.WRITE_FILE,
|
||||
data: { path, data },
|
||||
},
|
||||
trans,
|
||||
signal,
|
||||
) as Promise<boolean>
|
||||
}
|
||||
|
||||
public readFile = (path: string, signal?: AbortSignal) =>
|
||||
this.#send(
|
||||
{
|
||||
type: FFMessageType.READ_FILE,
|
||||
data: { path, encoding: 'binary' },
|
||||
},
|
||||
undefined,
|
||||
signal,
|
||||
) as Promise<Uint8Array>
|
||||
}
|
||||
|
||||
// ========================================================================== //
|
||||
|
||||
interface FFMessageLoadConfig {
|
||||
workerLoadURL: string
|
||||
coreURL: string
|
||||
wasmURL: string
|
||||
}
|
||||
|
||||
interface FFMessageExecData {
|
||||
args: string[]
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface FFMessageWriteFileData {
|
||||
path: string
|
||||
data: Uint8Array
|
||||
}
|
||||
|
||||
interface FFMessageReadFileData {
|
||||
path: string
|
||||
encoding: string
|
||||
}
|
||||
|
||||
type FFMessageData =
|
||||
| FFMessageLoadConfig
|
||||
| FFMessageExecData
|
||||
| FFMessageWriteFileData
|
||||
| FFMessageReadFileData
|
||||
|
||||
interface Message {
|
||||
type: string
|
||||
data?: FFMessageData
|
||||
}
|
||||
|
||||
type CallbackData = Uint8Array | string | boolean | Error | undefined
|
||||
|
||||
interface Callbacks {
|
||||
[id: number | string]: (data: CallbackData) => void
|
||||
}
|
||||
|
||||
interface FFMessageEventCallback {
|
||||
data: {
|
||||
id: number
|
||||
type: string
|
||||
data: CallbackData
|
||||
}
|
||||
}
|
||||
49
registry/lib/plugins/video/download/wasm-output/handler.ts
Normal file
49
registry/lib/plugins/video/download/wasm-output/handler.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { DownloadPackage } from '@/core/download'
|
||||
import { meta } from '@/core/meta'
|
||||
import { Toast } from '@/core/toast'
|
||||
import { FFmpeg } from './ffmpeg'
|
||||
import { httpGet, toBlobUrl, toastProgress } from './utils'
|
||||
|
||||
const ffmpeg = new FFmpeg()
|
||||
|
||||
async function load(toast: Toast) {
|
||||
await ffmpeg.load({
|
||||
workerLoadURL: await toBlobUrl(
|
||||
meta.compilationInfo.altCdn.library.ffmpeg.worker,
|
||||
'text/javascript',
|
||||
toastProgress(toast, '正在加载 FFmpeg Worker'),
|
||||
),
|
||||
coreURL: await toBlobUrl(
|
||||
meta.compilationInfo.altCdn.library.ffmpeg.core,
|
||||
'text/javascript',
|
||||
toastProgress(toast, '正在加载 FFmpeg Core'),
|
||||
),
|
||||
wasmURL: await toBlobUrl(
|
||||
meta.compilationInfo.altCdn.library.ffmpeg.wasm,
|
||||
'application/wasm',
|
||||
toastProgress(toast, '正在加载 FFmpeg WASM'),
|
||||
),
|
||||
})
|
||||
}
|
||||
export async function run(name: string, videoUrl: string, audioUrl: string, toast: Toast) {
|
||||
if (!ffmpeg.loaded) {
|
||||
await load(toast)
|
||||
}
|
||||
|
||||
ffmpeg.writeFile('video', await httpGet(videoUrl, toastProgress(toast, '正在下载视频流')))
|
||||
ffmpeg.writeFile('audio', await httpGet(audioUrl, toastProgress(toast, '正在下载音频流')))
|
||||
|
||||
toast.message = '混流中……'
|
||||
|
||||
await ffmpeg.exec(['-i', 'video', '-i', 'audio', '-c:v', 'copy', '-c:a', 'copy', 'output.mp4'])
|
||||
|
||||
const output = await ffmpeg.readFile('output.mp4')
|
||||
const outputBlob = new Blob([output], {
|
||||
type: 'video/mp4',
|
||||
})
|
||||
|
||||
toast.message = '完成!'
|
||||
toast.duration = 1500
|
||||
|
||||
await DownloadPackage.single(name, outputBlob)
|
||||
}
|
||||
40
registry/lib/plugins/video/download/wasm-output/index.ts
Normal file
40
registry/lib/plugins/video/download/wasm-output/index.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Toast } from '@/core/toast'
|
||||
import { PluginMetadata } from '@/plugins/plugin'
|
||||
import { DownloadVideoOutput } from '../../../../components/video/download/types'
|
||||
import { run } from './handler'
|
||||
|
||||
const title = 'WASM 混流输出'
|
||||
const desc = '使用 WASM 在浏览器中下载并合并音视频'
|
||||
|
||||
export const plugin: PluginMetadata = {
|
||||
name: 'downloadVideo.outputs.wasm',
|
||||
displayName: `下载视频 - ${title}`,
|
||||
description: desc,
|
||||
author: {
|
||||
name: 'WakelessSloth56',
|
||||
link: 'https://github.com/WakelessSloth56',
|
||||
},
|
||||
setup: ({ addData }) => {
|
||||
addData('downloadVideo.outputs', (outputs: DownloadVideoOutput[]) => {
|
||||
outputs.push({
|
||||
name: 'wasm',
|
||||
displayName: 'WASM',
|
||||
description: `${desc},运行过程中请勿关闭页面,初次使用或清除缓存后需要加载约 30 MB 的 WASM 文件`,
|
||||
runAction: async action => {
|
||||
const fragments = action.infos.flatMap(it => it.titledFragments)
|
||||
if (fragments.length !== 2) {
|
||||
Toast.error('仅支持 DASH 格式', title)
|
||||
} else {
|
||||
const toast = Toast.info('正在加载', title)
|
||||
try {
|
||||
await run(fragments[0].title, fragments[0].url, fragments[1].url, toast)
|
||||
} catch (error) {
|
||||
toast.close()
|
||||
Toast.error(String(error), title)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
54
registry/lib/plugins/video/download/wasm-output/utils.ts
Normal file
54
registry/lib/plugins/video/download/wasm-output/utils.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Toast } from '@/core/toast'
|
||||
import { formatFileSize, formatPercent } from '@/core/utils/formatters'
|
||||
|
||||
type OnProgress = (received: number, length: number) => void
|
||||
|
||||
export function toastProgress(toast: Toast, message: string): OnProgress {
|
||||
return (r, l) => {
|
||||
toast.message = `${message}: ${formatFileSize(r)}${
|
||||
l > 0 ? ` / ${formatFileSize(l)} @ ${formatPercent(r / l)}` : ''
|
||||
}`
|
||||
}
|
||||
}
|
||||
|
||||
export async function httpGet(url: string, onprogress: OnProgress) {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const reader = response.body.getReader()
|
||||
|
||||
// https://github.com/the1812/Bilibili-Evolved/pull/4521#discussion_r1402127375
|
||||
const length = response.headers.get('Content-Encoding')
|
||||
? -1
|
||||
: parseInt(response.headers.get('Content-Length'))
|
||||
|
||||
let received = 0
|
||||
const chunks = []
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
break
|
||||
}
|
||||
chunks.push(value)
|
||||
received += value.length
|
||||
onprogress(received, length)
|
||||
}
|
||||
|
||||
const chunksAll = new Uint8Array(received)
|
||||
let position = 0
|
||||
for (const chunk of chunks) {
|
||||
chunksAll.set(chunk, position)
|
||||
position += chunk.length
|
||||
}
|
||||
|
||||
return chunksAll
|
||||
}
|
||||
|
||||
export async function toBlobUrl(url: string, mimeType: string, onprogress: OnProgress) {
|
||||
const buffer = await httpGet(url, onprogress)
|
||||
const blob = new Blob([buffer], { type: mimeType })
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.8.7",
|
||||
"version": "2.8.8",
|
||||
"author": "Grant Howard, Coulomb-G",
|
||||
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
||||
"license": "MIT",
|
||||
|
||||
@ -16,6 +16,12 @@ export const compatibilityPatch = () => {
|
||||
'iframe',
|
||||
isIframe() && transparentFrames.some(matchUrlPattern),
|
||||
)
|
||||
|
||||
const { allVideoUrls } = await import('@/core/utils/urls')
|
||||
if (allVideoUrls.some(url => matchUrlPattern(url))) {
|
||||
const { playerPolyfill } = await import('@/components/video/player-adaptor')
|
||||
playerPolyfill()
|
||||
}
|
||||
})
|
||||
fullyLoaded(() => {
|
||||
select('meta[name=spm_prefix]').then(spm => {
|
||||
|
||||
@ -21,6 +21,7 @@ import * as videoInfo from './video/video-info'
|
||||
import * as videoQuality from './video/video-quality'
|
||||
import * as videoContextMenu from './video/video-context-menu'
|
||||
import * as videoControlBar from './video/video-control-bar'
|
||||
import * as videoCover from './video/video-cover'
|
||||
import * as watchlater from './video/watchlater'
|
||||
import * as liveControlBar from './live/live-control-bar'
|
||||
import * as liveSocket from './live/live-socket'
|
||||
@ -61,6 +62,7 @@ export const componentApis = {
|
||||
videoQuality,
|
||||
videoContextMenu,
|
||||
videoControlBar,
|
||||
videoCover,
|
||||
watchlater,
|
||||
xmlUtils,
|
||||
},
|
||||
|
||||
@ -13,7 +13,8 @@ import {
|
||||
UpdateCheckItem,
|
||||
CheckSingleTypeUpdateConfig,
|
||||
} from './utils'
|
||||
import { Options } from '.'
|
||||
import type { Options } from '.'
|
||||
import { isDataSaveMode } from '@/core/utils'
|
||||
|
||||
export const checkUpdate = async (config: CheckUpdateConfig) => {
|
||||
const {
|
||||
@ -23,9 +24,12 @@ export const checkUpdate = async (config: CheckUpdateConfig) => {
|
||||
force = false,
|
||||
maxCount = Infinity,
|
||||
} = config
|
||||
if (isDataSaveMode()) {
|
||||
return '当前为流量计费网络, 跳过更新检查.'
|
||||
}
|
||||
const now = Number(new Date())
|
||||
const { devMode } = getGeneralSettings()
|
||||
const { options } = getComponentSettings(name)
|
||||
const { options } = getComponentSettings<Options>(name)
|
||||
// Remove uninstalled items
|
||||
Object.keys(items)
|
||||
.filter(it => !existPredicate(it))
|
||||
|
||||
@ -166,7 +166,7 @@ export default Vue.extend({
|
||||
if (!matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
|
||||
return
|
||||
}
|
||||
select('#search-keyword').then((input: HTMLInputElement) => {
|
||||
select('#search-keyword, .search-input-el').then((input: HTMLInputElement) => {
|
||||
if (!input) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@ export const component = defineComponentMetadata({
|
||||
const { monkey } = await import('@/core/ajax')
|
||||
const { meta } = await import('@/core/meta')
|
||||
const { Toast } = await import('@/core/toast')
|
||||
const { isDataSaveMode } = await import('@/core/utils')
|
||||
if (isDataSaveMode()) {
|
||||
return
|
||||
}
|
||||
const now = Number(new Date())
|
||||
const duration = now - options.lastUpdateCheck
|
||||
if (duration < options.minimumDuration) {
|
||||
|
||||
@ -3,6 +3,7 @@ import { select } from '@/core/spin-query'
|
||||
import { playerReady, preventEvent } from '@/core/utils'
|
||||
import { createPlayerModeChangeEvent, PlayerMode } from './events'
|
||||
|
||||
/** 播放器模式标记同步 */
|
||||
const playerModePolyfill = async () => {
|
||||
await playerReady()
|
||||
const bpxContainer = (await select('.bpx-player-container')) as HTMLElement
|
||||
@ -35,37 +36,6 @@ const playerModePolyfill = async () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const idPolyfill = async () => {
|
||||
let pbp = await select(() => unsafeWindow.$pbp)
|
||||
if (!pbp) {
|
||||
console.warn('[bpx player polyfill] pbp not found')
|
||||
return
|
||||
}
|
||||
const loadPbpData = () => {
|
||||
const idData = {
|
||||
aid: pbp.options.aid.toString(),
|
||||
cid: pbp.options.cid.toString(),
|
||||
bvid: pbp.options.bvid,
|
||||
}
|
||||
if (Object.values(idData).some(it => !it || parseInt(it) <= 0)) {
|
||||
console.warn('[bpx player polyfill] invalid pbp data', pbp.options)
|
||||
}
|
||||
Object.assign(unsafeWindow, idData)
|
||||
}
|
||||
Object.defineProperty(unsafeWindow, '$pbp', {
|
||||
get() {
|
||||
return pbp
|
||||
},
|
||||
set(newValue) {
|
||||
pbp = newValue
|
||||
if (newValue === undefined) {
|
||||
return
|
||||
}
|
||||
Promise.resolve().then(() => loadPbpData())
|
||||
},
|
||||
})
|
||||
loadPbpData()
|
||||
}
|
||||
/** 移除 3.x 的双击全屏.
|
||||
* 如果以后视频区兼容了弹幕点赞层, 需要将双击全屏组件更换为阻止双击全屏, 并取消对此函数的使用.
|
||||
*/
|
||||
@ -76,12 +46,9 @@ const doubleClickPolyfill = async () => {
|
||||
}
|
||||
preventEvent(layer, 'dblclick')
|
||||
}
|
||||
|
||||
/** 番剧 & 视频播放器 (BPX) 通用 polyfill */
|
||||
export const bpxPlayerPolyfill = lodash.once(async () => {
|
||||
// 番剧 + 3.x 播放器通用
|
||||
playerModePolyfill()
|
||||
doubleClickPolyfill()
|
||||
if (!document.URL.startsWith('https://www.bilibili.com/bangumi/play/')) {
|
||||
return
|
||||
}
|
||||
idPolyfill()
|
||||
})
|
||||
|
||||
@ -1,38 +1,51 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import { allMutations } from '@/core/observer'
|
||||
import { select } from '@/core/spin-query'
|
||||
import { matchUrlPattern } from '@/core/utils'
|
||||
import { bangumiUrls } from '@/core/utils/urls'
|
||||
|
||||
const isIdValid = (id: string) => {
|
||||
if (id === '') {
|
||||
return false
|
||||
}
|
||||
const idNumber = parseInt(id)
|
||||
if (Number.isNaN(idNumber)) {
|
||||
return true
|
||||
}
|
||||
return idNumber > 0
|
||||
}
|
||||
const idPolyfill = async () => {
|
||||
const player = await select(() => unsafeWindow.player)
|
||||
if (!player?.__getUserParams()) {
|
||||
if (!player?.getManifest) {
|
||||
return
|
||||
}
|
||||
const { useScopedConsole } = await import('@/core/utils/log')
|
||||
const console = useScopedConsole('v4 player polyfill')
|
||||
allMutations(() => {
|
||||
const { input } = player.__getUserParams()
|
||||
if (!input) {
|
||||
console.warn('invalid getUserParams data')
|
||||
const manifest = player.getManifest()
|
||||
if (!manifest) {
|
||||
console.warn('invalid getManifest data')
|
||||
return
|
||||
}
|
||||
const idData = {
|
||||
aid: input.aid.toString(),
|
||||
cid: input.cid.toString(),
|
||||
bvid: input.bvid,
|
||||
aid: manifest.aid.toString(),
|
||||
cid: manifest.cid.toString(),
|
||||
bvid: manifest.bvid ?? '',
|
||||
}
|
||||
if (Object.values(idData).some(it => it === '' || parseInt(it) <= 0)) {
|
||||
console.warn('invalid input data')
|
||||
const isIdDataValid = (() => {
|
||||
const isBangumi = bangumiUrls.some(url => matchUrlPattern(url))
|
||||
if (isBangumi) {
|
||||
return isIdValid(idData.aid) && isIdValid(idData.cid)
|
||||
}
|
||||
return Object.values(idData).every(it => isIdValid(it))
|
||||
})()
|
||||
if (!isIdDataValid) {
|
||||
console.warn('invalid manifest data', idData)
|
||||
}
|
||||
Object.assign(unsafeWindow, idData)
|
||||
})
|
||||
}
|
||||
|
||||
export const v4PlayerPolyfill = lodash.once(() => {
|
||||
if (bangumiUrls.some(url => matchUrlPattern(url))) {
|
||||
return
|
||||
}
|
||||
if (unsafeWindow.aid || unsafeWindow.cid) {
|
||||
return
|
||||
}
|
||||
|
||||
31
src/components/video/player-agent/bangumi-player.ts
Normal file
31
src/components/video/player-agent/bangumi-player.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { PlayerAgent, selectorWrap } from './base'
|
||||
import { bpxSelectors } from './bpx'
|
||||
import { AgentType, PlayerQuery, ElementQuery } from './types'
|
||||
|
||||
export class BangumiPlayerAgent extends PlayerAgent {
|
||||
type: AgentType = 'bangumi'
|
||||
|
||||
query = selectorWrap({
|
||||
playerWrap: '#bilibili-player-wrap',
|
||||
bilibiliPlayer: '#bilibili-player',
|
||||
...bpxSelectors,
|
||||
}) as PlayerQuery<ElementQuery>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
seek(time: number) {
|
||||
const seekResult = super.seek(time)
|
||||
if (seekResult === null) {
|
||||
return seekResult
|
||||
}
|
||||
setTimeout(() => {
|
||||
const toastText = dq('.bpx-player-toast-row .bpx-player-toast-item .bpx-player-toast-text')
|
||||
if (toastText?.textContent?.startsWith('已为您定位至')) {
|
||||
toastText.textContent = '已为您定位至00:00'
|
||||
}
|
||||
})
|
||||
return seekResult
|
||||
}
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
/* eslint-disable class-methods-use-this */
|
||||
import { select } from '@/core/spin-query'
|
||||
import { raiseEvent } from '@/core/utils'
|
||||
import {
|
||||
@ -29,9 +30,23 @@ export const click = (target: ElementQuery) => {
|
||||
button?.click()
|
||||
return button
|
||||
}
|
||||
export abstract class PlayerAgent {
|
||||
|
||||
export enum PlayerAgentEventTypes {
|
||||
Play = 'play',
|
||||
}
|
||||
export abstract class PlayerAgent
|
||||
extends EventTarget
|
||||
implements EnumEventTarget<`${PlayerAgentEventTypes}`>
|
||||
{
|
||||
isBpxPlayer = true
|
||||
|
||||
abstract type: AgentType
|
||||
abstract query: PlayerQuery<ElementQuery>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
provideCustomQuery<CustomQueryType extends CustomQuery<string>>(
|
||||
config: CustomQueryProvider<CustomQueryType>,
|
||||
) {
|
||||
@ -71,32 +86,145 @@ export abstract class PlayerAgent {
|
||||
|
||||
/** true 开灯,false 关灯 */
|
||||
async toggleLight(on?: boolean) {
|
||||
const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement
|
||||
if (!this.nativeApi) {
|
||||
return null
|
||||
}
|
||||
const isCurrentLightOff = this.nativeApi.getLightOff()
|
||||
// 无指定参数, 直接 toggle
|
||||
if (on === undefined) {
|
||||
checkbox.click()
|
||||
return
|
||||
this.nativeApi.setLightOff(!isCurrentLightOff)
|
||||
return !isCurrentLightOff
|
||||
}
|
||||
// 关灯状态 && 要开灯 -> 开灯
|
||||
checkbox.checked && on && checkbox.click()
|
||||
// 开灯状态 && 要关灯 -> 关灯
|
||||
!checkbox.checked && !on && checkbox.click()
|
||||
if (on && isCurrentLightOff) {
|
||||
this.nativeApi.setLightOff(true)
|
||||
return true
|
||||
}
|
||||
if (!on && !isCurrentLightOff) {
|
||||
this.nativeApi.setLightOff(false)
|
||||
return false
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
getPlayerConfig(target: string) {
|
||||
return lodash.get(JSON.parse(localStorage.getItem('bilibili_player_settings')), target, false)
|
||||
getPlayerConfig<DefaultValueType = unknown, ValueType = DefaultValueType>(
|
||||
target: string,
|
||||
defaultValue?: DefaultValueType,
|
||||
): ValueType | DefaultValueType {
|
||||
const storageKey = this.isBpxPlayer ? 'bpx_player_profile' : 'bilibili_player_settings'
|
||||
return lodash.get(JSON.parse(localStorage.getItem(storageKey)), target, defaultValue)
|
||||
}
|
||||
|
||||
isAutoPlay() {
|
||||
return this.getPlayerConfig('video_status.autoplay')
|
||||
}
|
||||
|
||||
abstract isMute(): boolean
|
||||
// https://github.com/the1812/Bilibili-Evolved/discussions/4341
|
||||
get nativeApi() {
|
||||
return unsafeWindow.player || unsafeWindow.playerRaw
|
||||
}
|
||||
|
||||
get nanoApi() {
|
||||
return unsafeWindow.nano
|
||||
}
|
||||
|
||||
private eventHandlerMap = new Map<
|
||||
EventListenerOrEventListenerObject,
|
||||
{
|
||||
handler: () => void
|
||||
options: boolean | AddEventListenerOptions
|
||||
}
|
||||
>()
|
||||
|
||||
addEventListener(
|
||||
type: `${PlayerAgentEventTypes}`,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
): void {
|
||||
super.addEventListener(type, callback, options)
|
||||
switch (type) {
|
||||
default: {
|
||||
break
|
||||
}
|
||||
case PlayerAgentEventTypes.Play: {
|
||||
const handler = () => {
|
||||
this.dispatchEvent(new Event(PlayerAgentEventTypes.Play))
|
||||
}
|
||||
if (typeof options === 'object' && options.once) {
|
||||
this.nativeApi.once(this.nanoApi.EventType.Player_Initialized, handler)
|
||||
} else {
|
||||
this.nativeApi.on(this.nanoApi.EventType.Player_Initialized, handler)
|
||||
}
|
||||
this.eventHandlerMap.set(callback, {
|
||||
handler,
|
||||
options,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeEventListener(
|
||||
type: `${PlayerAgentEventTypes}`,
|
||||
callback: EventListenerOrEventListenerObject,
|
||||
options?: boolean | EventListenerOptions,
|
||||
): void {
|
||||
super.removeEventListener(type, callback, options)
|
||||
switch (type) {
|
||||
default: {
|
||||
break
|
||||
}
|
||||
case PlayerAgentEventTypes.Play: {
|
||||
const handlerData = this.eventHandlerMap.get(callback)
|
||||
if (!handlerData || lodash.isEqual(options, handlerData.options)) {
|
||||
break
|
||||
}
|
||||
this.nativeApi.off(this.nanoApi.EventType.Player_Initialized, handlerData.handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取是否静音 */
|
||||
isMute() {
|
||||
if (!this.nativeApi) {
|
||||
return null
|
||||
}
|
||||
if (this.nativeApi.isMuted) {
|
||||
return this.nativeApi.isMuted()
|
||||
}
|
||||
return this.nativeApi.isMute()
|
||||
}
|
||||
/** 更改音量 (%) */
|
||||
abstract changeVolume(change: number): number
|
||||
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)
|
||||
}
|
||||
/** 跳转到指定时间 */
|
||||
abstract seek(time: number): number
|
||||
seek(time: number) {
|
||||
if (!this.nativeApi) {
|
||||
return null
|
||||
}
|
||||
this.nativeApi.seek(time)
|
||||
return this.nativeApi.getCurrentTime() as number
|
||||
}
|
||||
/** 更改时间 */
|
||||
abstract changeTime(change: number): number
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
51
src/components/video/player-agent/bpx.ts
Normal file
51
src/components/video/player-agent/bpx.ts
Normal file
@ -0,0 +1,51 @@
|
||||
// spell-checker: disable
|
||||
export const bpxSelectors = {
|
||||
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-perch',
|
||||
},
|
||||
control: {
|
||||
element: '.bpx-player-control-entity',
|
||||
wrap: '.bpx-player-control-wrap',
|
||||
mask: '.bpx-player-control-mask',
|
||||
top: '.bpx-player-control-top',
|
||||
progress: '.bpx-player-progress',
|
||||
bottom: '.bpx-player-control-bottom',
|
||||
bottomLeft: '.bpx-player-control-bottom-left',
|
||||
bottomCenter: '.bpx-player-control-bottom-center',
|
||||
bottomRight: '.bpx-player-control-bottom-right',
|
||||
buttons: {
|
||||
start: '.bpx-player-ctrl-play',
|
||||
next: '.bpx-player-ctrl-btn-next',
|
||||
time: '.bpx-player-ctrl-time',
|
||||
quality: '.bpx-player-ctrl-quality',
|
||||
pageList: '.bpx-player-ctrl-eplist',
|
||||
speed: '.bpx-player-ctrl-playbackrate',
|
||||
subtitle: '.bpx-player-ctrl-subtitle > div > span',
|
||||
volume: '.bpx-player-ctrl-volume-icon',
|
||||
settings: '.bpx-player-ctrl-setting',
|
||||
pip: '.bpx-player-ctrl-pip',
|
||||
widescreen: '.bpx-player-ctrl-wide',
|
||||
webFullscreen: '.bpx-player-ctrl-web',
|
||||
fullscreen: '.bpx-player-ctrl-full',
|
||||
},
|
||||
settings: {
|
||||
wrap: '.bpx-player-ctrl-setting-box',
|
||||
lightOff: '.bpx-player-ctrl-setting-lightoff .bui-checkbox-input',
|
||||
},
|
||||
},
|
||||
toastWrap: '.bpx-player-dialog-wrap',
|
||||
danmakuTipLayer: '.bpx-player-dm-tip',
|
||||
danmakuSwitch: '.bpx-player-dm-switch input',
|
||||
}
|
||||
// spell-checker: enable
|
||||
@ -1,10 +1,10 @@
|
||||
import { bangumiUrls, matchCurrentPage } from '@/core/utils/urls'
|
||||
import { BangumiPlayerAgent } from './bangumi'
|
||||
import { VideoPlayerMixedAgent } from './video-player-mixed'
|
||||
import { BangumiPlayerAgent } from './bangumi-player'
|
||||
import { VideoPlayerBpxAgent } from './video-player'
|
||||
|
||||
export const playerAgent = (() => {
|
||||
if (matchCurrentPage(bangumiUrls)) {
|
||||
return new BangumiPlayerAgent()
|
||||
}
|
||||
return new VideoPlayerMixedAgent()
|
||||
return new VideoPlayerBpxAgent()
|
||||
})()
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -3,11 +3,9 @@ import { isBwpVideo } from '@/core/utils'
|
||||
import { PlayerAgent, selectorWrap } from './base'
|
||||
import { AgentType, PlayerQuery, ElementQuery } from './types'
|
||||
|
||||
/** @deprecated Use `VideoPlayerBpxAgent` instead */
|
||||
export class VideoPlayerV2Agent extends PlayerAgent {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
get nativeApi() {
|
||||
return unsafeWindow.player
|
||||
}
|
||||
isBpxPlayer = false
|
||||
type: AgentType = 'video'
|
||||
query = selectorWrap({
|
||||
playerWrap: '.player-wrap',
|
||||
@ -89,35 +87,12 @@ export class VideoPlayerV2Agent extends PlayerAgent {
|
||||
})()
|
||||
}
|
||||
|
||||
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
|
||||
const seekResult = super.seek(time)
|
||||
if (seekResult === null) {
|
||||
return seekResult
|
||||
}
|
||||
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)',
|
||||
)
|
||||
@ -125,17 +100,6 @@ export class VideoPlayerV2Agent extends PlayerAgent {
|
||||
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()
|
||||
return seekResult
|
||||
}
|
||||
}
|
||||
|
||||
43
src/components/video/player-agent/video-player.ts
Normal file
43
src/components/video/player-agent/video-player.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { isBwpVideo } from '@/core/utils'
|
||||
import { PlayerAgent, selectorWrap } from './base'
|
||||
import { PlayerQuery, ElementQuery, AgentType } from './types'
|
||||
import { select } from '@/core/spin-query'
|
||||
import { bpxSelectors } from './bpx'
|
||||
|
||||
export class VideoPlayerBpxAgent extends PlayerAgent {
|
||||
type: AgentType = 'video'
|
||||
|
||||
query = selectorWrap({
|
||||
playerWrap: '.player-wrap',
|
||||
bilibiliPlayer: '#bilibili-player',
|
||||
...bpxSelectors,
|
||||
}) 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
|
||||
})()
|
||||
}
|
||||
}
|
||||
22
src/components/video/video-cover.ts
Normal file
22
src/components/video/video-cover.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { logError } from '@/core/utils/log'
|
||||
import { VideoInfo } from './video-info'
|
||||
|
||||
/** 根据 aid 获取视频封面地址 */
|
||||
export const getVideoCoverUrlByAid = async (aid: string) => {
|
||||
const videoInfo = new VideoInfo(aid)
|
||||
try {
|
||||
await videoInfo.fetchInfo()
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
throw error
|
||||
}
|
||||
return videoInfo.coverUrl.replace('http:', 'https:')
|
||||
}
|
||||
|
||||
/** 根据 aid 下载视频封面为 Blob */
|
||||
export const getBlobByAid = async (aid: string) => {
|
||||
const url = await getVideoCoverUrlByAid(aid)
|
||||
const response = await fetch(url)
|
||||
const blob = await response.blob()
|
||||
return blob
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { JSZipLibrary } from './runtime-library'
|
||||
import { getGeneralSettings } from './settings'
|
||||
import { formatFilename } from './utils/formatters'
|
||||
import { useScopedConsole } from './utils/log'
|
||||
import { getRandomId } from './utils'
|
||||
|
||||
/** 表示`DownloadPackage`中的一个文件 */
|
||||
export interface PackageEntry {
|
||||
@ -36,6 +37,7 @@ export class DownloadPackage {
|
||||
}
|
||||
/** 获取打包后的Blob数据 */
|
||||
async blob(): Promise<Blob | null> {
|
||||
const console = useScopedConsole('文件打包')
|
||||
if (this.entries.length === 0) {
|
||||
return null
|
||||
}
|
||||
@ -45,7 +47,20 @@ export class DownloadPackage {
|
||||
}
|
||||
const JSZip = await JSZipLibrary
|
||||
const zip = new JSZip()
|
||||
const fileExists = (name: string) => zip.filter((_, file) => file.name === name).length > 0
|
||||
this.entries.forEach(({ name, data, options }) => {
|
||||
if (fileExists(name)) {
|
||||
let tempName = name
|
||||
while (fileExists(tempName)) {
|
||||
const extensionIndex = name.lastIndexOf('.')
|
||||
tempName = `${name.substring(0, extensionIndex)}.${getRandomId(8)}${name.substring(
|
||||
extensionIndex,
|
||||
)}`
|
||||
}
|
||||
console.warn(`文件名 "${name}" 和已有文件冲突, 已临时更换为 "${tempName}"`)
|
||||
zip.file(tempName, data, options)
|
||||
return
|
||||
}
|
||||
zip.file(name, data, options)
|
||||
})
|
||||
return zip.generateAsync({ type: 'blob' })
|
||||
|
||||
@ -241,7 +241,6 @@ export const urlChange = (callback: (url: string) => void, config?: AddEventList
|
||||
/** 等待 cid */
|
||||
const selectCid = lodash.once(() =>
|
||||
select(() => {
|
||||
import('@/components/video/player-adaptor').then(({ playerPolyfill }) => playerPolyfill())
|
||||
if (unsafeWindow.cid) {
|
||||
return unsafeWindow.cid
|
||||
}
|
||||
@ -264,8 +263,6 @@ export const videoChange = async (
|
||||
if (!matchCurrentPage(playerUrls)) {
|
||||
return false
|
||||
}
|
||||
const { playerPolyfill } = await import('@/components/video/player-adaptor')
|
||||
playerPolyfill()
|
||||
const cid = await selectCid()
|
||||
if (cid === null) {
|
||||
return false
|
||||
|
||||
@ -37,7 +37,7 @@ export const addImportantStyle = (text: string, name?: string) =>
|
||||
export const removeStyle = (...names: string[]) => {
|
||||
names.forEach(name => {
|
||||
const id = getDefaultStyleID(name)
|
||||
dqa(`#${id}`).forEach(it => it.remove())
|
||||
document.getElementById(id)?.remove()
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/** 请求有UA限制的网址时, 可以使用此内置UA (取自Firefox) */
|
||||
/** 请求有 UA 限制的网址时, 可以使用此内置 UA (取自Firefox) */
|
||||
export const UserAgent =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
|
||||
/** 空白透明图像的URL */
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0'
|
||||
/** 空白透明图像的 URL */
|
||||
export const EmptyImageUrl =
|
||||
'data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="0" height="0"></svg>'
|
||||
|
||||
@ -205,8 +205,7 @@ export const isNotHtml = () => document.contentType !== 'text/html'
|
||||
* @param eventName 事件名称
|
||||
*/
|
||||
export const raiseEvent = (element: HTMLElement, eventName: string) => {
|
||||
const event = document.createEvent('HTMLEvents')
|
||||
event.initEvent(eventName, true, true)
|
||||
const event = new Event(eventName)
|
||||
element.dispatchEvent(event)
|
||||
}
|
||||
/** 根据图片URL生成 `srcset`, 范围从 `@1x` 至 `@4x`, 每 `0.25x` 产生一个 `src`
|
||||
@ -540,15 +539,21 @@ export const retrieveImageUrl = (element: HTMLElement) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return null
|
||||
}
|
||||
let url: string
|
||||
if (element.hasAttribute('data-src')) {
|
||||
url = element.getAttribute('data-src')
|
||||
} else if (element instanceof HTMLImageElement) {
|
||||
url = element.src
|
||||
} else if (dq(element, 'picture img')) {
|
||||
const image = dq(element, 'picture img') as HTMLImageElement
|
||||
url = image.src
|
||||
} else {
|
||||
const url = (() => {
|
||||
if (element.hasAttribute('data-src')) {
|
||||
return element.getAttribute('data-src')
|
||||
}
|
||||
if (element instanceof HTMLImageElement) {
|
||||
return element.src
|
||||
}
|
||||
if (element instanceof HTMLPictureElement && dq(element, 'img')) {
|
||||
const image = dq(element, 'img') as HTMLImageElement
|
||||
return image.src
|
||||
}
|
||||
if (dq(element, 'picture img')) {
|
||||
const image = dq(element, 'picture img') as HTMLImageElement
|
||||
return image.src
|
||||
}
|
||||
const { backgroundImage } = element.style
|
||||
if (!backgroundImage) {
|
||||
return null
|
||||
@ -557,8 +562,9 @@ export const retrieveImageUrl = (element: HTMLElement) => {
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
url = match[1]
|
||||
}
|
||||
return match[1]
|
||||
})()
|
||||
|
||||
const thumbMatch = url.match(/^(.+)(\..+?)(@.+)$/)
|
||||
if (thumbMatch) {
|
||||
return {
|
||||
@ -672,3 +678,26 @@ export const todo = (...args: unknown[]): never => {
|
||||
export const unreachable = (): never => {
|
||||
throw new Error(`unreachable`)
|
||||
}
|
||||
|
||||
/** 是否为流量计费网络 (不支持的浏览器仍按 false 算) */
|
||||
export const isDataSaveMode = () => {
|
||||
return navigator.connection?.saveData ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟一次点击 (依次触发 `pointerdown`, `mousedown`, `pointerup`, `mouseup`, `click` 事件)
|
||||
* @param target 点击的目标元素
|
||||
* @param eventParams 事件参数
|
||||
*/
|
||||
export const simulateClick = (target: EventTarget, eventParams?: PointerEventInit) => {
|
||||
const mouseDownEvent = new MouseEvent('mousedown', eventParams)
|
||||
const mouseUpEvent = new MouseEvent('mouseup', eventParams)
|
||||
const pointerDownEvent = new PointerEvent('pointerdown', eventParams)
|
||||
const pointerUpEvent = new PointerEvent('pointerup', eventParams)
|
||||
const clickEventEvent = new MouseEvent('click', eventParams)
|
||||
target.dispatchEvent(pointerDownEvent)
|
||||
target.dispatchEvent(mouseDownEvent)
|
||||
target.dispatchEvent(pointerUpEvent)
|
||||
target.dispatchEvent(mouseUpEvent)
|
||||
target.dispatchEvent(clickEventEvent)
|
||||
}
|
||||
|
||||
@ -37,39 +37,64 @@ export const formatTitle = (
|
||||
extraVariables: StringMap = {},
|
||||
) => {
|
||||
const now = new Date()
|
||||
const getLegacyTitle = () => {
|
||||
return (
|
||||
document.title
|
||||
.replace(
|
||||
/第[0-9]*[零一二三四五六七八九十百千]*[集话]-[^-]+-[^-]+-[^-]+在线观看-bilibili-哔哩哔哩$/,
|
||||
'',
|
||||
)
|
||||
.replace(/-[^-]+-[^-]+-[^-]+在线观看-bilibili-哔哩哔哩$/, '')
|
||||
.replace(/-[^-]+-[^-]+在线观看-bilibili-哔哩哔哩$/, '')
|
||||
.replace(/:([^:]+?)_.+?_bilibili_哔哩哔哩$/, '')
|
||||
.replace(/_哔哩哔哩_bilibili$/, '')
|
||||
.replace(/ - 哔哩哔哩$/, '')
|
||||
// b站不再有干杯了吗...
|
||||
.replace(/_哔哩哔哩 \(゜-゜\)つロ 干杯~-bilibili$/, '')
|
||||
.replace(/(.*?) - (.*?) - 哔哩哔哩直播,二次元弹幕直播平台$/, '$1')
|
||||
.trim()
|
||||
)
|
||||
}
|
||||
const builtInVariables: StringMap = {
|
||||
title: document.title
|
||||
.replace(
|
||||
/第[0-9]*[零一二三四五六七八九十百千]*[集话]-[^-]+-[^-]+-[^-]+在线观看-bilibili-哔哩哔哩$/,
|
||||
'',
|
||||
)
|
||||
.replace(/-[^-]+-[^-]+-[^-]+在线观看-bilibili-哔哩哔哩$/, '')
|
||||
.replace(/-[^-]+-[^-]+在线观看-bilibili-哔哩哔哩$/, '')
|
||||
.replace(/:([^:]+?)_.+?_bilibili_哔哩哔哩$/, '')
|
||||
.replace(/_哔哩哔哩_bilibili$/, '')
|
||||
.replace(/ - 哔哩哔哩$/, '')
|
||||
// b站不再有干杯了吗...
|
||||
.replace(/_哔哩哔哩 \(゜-゜\)つロ 干杯~-bilibili$/, '')
|
||||
.replace(/(.*?) - (.*?) - 哔哩哔哩直播,二次元弹幕直播平台$/, '$1')
|
||||
.trim(),
|
||||
title: (() => {
|
||||
const videoPageTitle = dq('.video-info-container .video-title')
|
||||
if (videoPageTitle !== null) {
|
||||
return videoPageTitle.getAttribute('title')
|
||||
}
|
||||
const bangumiPageTitle = dq('[class*="mediainfo_mediaTitle"]')
|
||||
if (bangumiPageTitle !== null) {
|
||||
return bangumiPageTitle.getAttribute('title')
|
||||
}
|
||||
const livePageTitle = dq('.header-info-ctnr .live-title .text')
|
||||
if (livePageTitle !== null) {
|
||||
return livePageTitle.getAttribute('title')
|
||||
}
|
||||
return getLegacyTitle()
|
||||
})(),
|
||||
ep: (() => {
|
||||
if (!includesPageTitle) {
|
||||
return undefined
|
||||
}
|
||||
const bangumiPage = dq('#eplist_module li.cursor .ep-title') as HTMLElement
|
||||
if (bangumiPage !== null) {
|
||||
return bangumiPage.innerText
|
||||
const bangumiPageEp = dq('#eplist_module li.cursor .ep-title') as HTMLElement
|
||||
if (bangumiPageEp !== null) {
|
||||
return bangumiPageEp.innerText
|
||||
}
|
||||
const pageLink = dq('#multi_page .cur-list>ul li.on a')
|
||||
if (pageLink !== null) {
|
||||
return pageLink.getAttribute('title')
|
||||
const videoPageEp = dq(
|
||||
'#multi_page .cur-list>ul li.on a, .video-episode-card__info-playing .video-episode-card__info-title',
|
||||
)
|
||||
if (videoPageEp !== null) {
|
||||
return videoPageEp.getAttribute('title')
|
||||
}
|
||||
const watchlaterPageEp = dq('.multip-list-item.multip-list-item-active')
|
||||
if (watchlaterPageEp !== null) {
|
||||
return watchlaterPageEp.getAttribute('title')
|
||||
}
|
||||
return undefined
|
||||
})(),
|
||||
aid: unsafeWindow.aid,
|
||||
bvid: unsafeWindow.bvid,
|
||||
cid: unsafeWindow.cid,
|
||||
lid: document.URL.replace(/https:\/\/live\.bilibili\.com\/(blanc\/)?(\d)+/, '$2'),
|
||||
lid: document.URL.replace(/https:\/\/live\.bilibili\.com\/(blanc\/)?([\d]+)/, '$2'),
|
||||
// 年月日这方法名真够乱的
|
||||
y: now.getFullYear().toString(),
|
||||
M: (now.getMonth() + 1).toString().padStart(2, '0'), // zero-based
|
||||
|
||||
24
src/global.d.ts
vendored
24
src/global.d.ts
vendored
@ -9,6 +9,30 @@ declare global {
|
||||
const lodash: LoDashStatic
|
||||
const Vue: typeof import('vue/types/umd')
|
||||
|
||||
type EnumEventTarget<EventTypes extends string> = EventTarget & {
|
||||
addEventListener(
|
||||
type: EventTypes,
|
||||
callback: EventListenerOrEventListenerObject | null,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void
|
||||
removeEventListener(
|
||||
type: EventTypes,
|
||||
callback: EventListenerOrEventListenerObject | null,
|
||||
options?: EventListenerOptions | boolean,
|
||||
): void
|
||||
}
|
||||
interface NetworkInformation extends EnumEventTarget<'change'> {
|
||||
downlink: number
|
||||
downlinkMax: number
|
||||
effectiveType: string
|
||||
rtt: number
|
||||
saveData: boolean
|
||||
type: string
|
||||
}
|
||||
interface Navigator {
|
||||
connection?: NetworkInformation
|
||||
}
|
||||
|
||||
interface GitInfo {
|
||||
commitHash: string
|
||||
branch: string
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { jsDelivr } from './jsdelivr'
|
||||
import { CdnConfig } from './types'
|
||||
|
||||
const owner = 'the1812'
|
||||
@ -15,6 +16,8 @@ export const github: CdnConfig = {
|
||||
sortable: `https://${host}/SortableJS/Sortable/1.14.0/Sortable.min.js`,
|
||||
mdi: `https://${owner}.github.io/Bilibili-Evolved/static/mdi/mdi.css`,
|
||||
streamsaver: `https://${host}/jimmywarting/StreamSaver.js/2.0.6/StreamSaver.js`,
|
||||
// https://github.com/the1812/Bilibili-Evolved/pull/4521#discussion_r1402084486
|
||||
ffmpeg: jsDelivr.library.ffmpeg,
|
||||
},
|
||||
smallLogo: `https://${host}/${owner}/Bilibili-Evolved/preview/images/logo-small.png`,
|
||||
logo: `https://${host}/${owner}/Bilibili-Evolved/preview/images/logo.png`,
|
||||
|
||||
@ -15,6 +15,11 @@ export const jsDelivr: CdnConfig = {
|
||||
sortable: `https://${host}/npm/sortablejs@1.14.0/Sortable.min.js`,
|
||||
mdi: `https://${host}/gh/${owner}/Bilibili-Evolved@master/docs/static/mdi/mdi.css`,
|
||||
streamsaver: `https://${host}/npm/streamsaver@2.0.6/StreamSaver.min.js`,
|
||||
ffmpeg: {
|
||||
worker: `https://${host}/npm/@ffmpeg/ffmpeg@0.12.4/dist/umd/814.ffmpeg.js`,
|
||||
core: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.js`,
|
||||
wasm: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.wasm`,
|
||||
},
|
||||
},
|
||||
smallLogo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo-small.png`,
|
||||
logo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo.png`,
|
||||
|
||||
@ -11,6 +11,11 @@ export interface CdnConfig {
|
||||
sortable: string
|
||||
mdi: string
|
||||
streamsaver: string
|
||||
ffmpeg: {
|
||||
worker: string
|
||||
core: string
|
||||
wasm: string
|
||||
}
|
||||
}
|
||||
smallLogo: string
|
||||
logo: string
|
||||
|
||||
Loading…
Reference in New Issue
Block a user