mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge remote-tracking branch 'main/preview-features' into update-to-vue3
This commit is contained in:
commit
da48523257
4
.vscode/tasks.json
vendored
4
.vscode/tasks.json
vendored
@ -68,14 +68,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/ './**/*.@(js|ts|vue)'",
|
"command": "pnpm eslint . --cache --cache-location node_modules/.cache/eslint/ --ext .ts,.vue",
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"label": "生产:代码检查 prod:lint"
|
"label": "生产:代码检查 prod:lint"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pnpm eslint --cache --cache-location node_modules/.cache/eslint/ --fix './**/*.@(js|ts|vue)'",
|
"command": "pnpm eslint . --cache --cache-location node_modules/.cache/eslint/ --fix --ext .ts,.vue",
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"problemMatcher": [],
|
"problemMatcher": [],
|
||||||
"label": "生产:代码修复 prod:lint-fix"
|
"label": "生产:代码修复 prod:lint-fix"
|
||||||
|
|||||||
88
CHANGELOG.md
88
CHANGELOG.md
@ -1,5 +1,93 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v2.6.3-preview
|
||||||
|
`2023-02-09`
|
||||||
|
|
||||||
|
包含 v2.6.3 的所有更新内容.
|
||||||
|
|
||||||
|
- 启用新的代码加载方式. (PR #3938 by [timongh](https://github.com/timongh))
|
||||||
|
|
||||||
|
## v2.6.3
|
||||||
|
`2023-02-20`
|
||||||
|
|
||||||
|
- 修复 `隐藏推荐直播` 失效. (#3993)
|
||||||
|
- 修复 `夜间模式` 下鼠标移至特定页排行榜时的显示问题. (#1052, PR #4001 by [Tinhone](https://github.com/Tinhone))
|
||||||
|
|
||||||
|
## v2.6.2-preview
|
||||||
|
`2023-02-09`
|
||||||
|
|
||||||
|
包含 v2.6.2 的所有更新内容.
|
||||||
|
|
||||||
|
- 修复搜索历史无法删除. (#3893)
|
||||||
|
|
||||||
|
## v2.6.2
|
||||||
|
`2023-02-09`
|
||||||
|
|
||||||
|
- 修复 `播放前显示封面` 未正确清除封面. (#3974, PR #3976 by [imshixin](https://github.com/imshixin))
|
||||||
|
- 删除 NavbarWatchlater.vue 中的未定义属性. (#3949)
|
||||||
|
|
||||||
|
## v2.6.1-preview
|
||||||
|
`2023-02-07`
|
||||||
|
|
||||||
|
包含 v2.6.1 的所有更新内容.
|
||||||
|
|
||||||
|
- 提高了 `自定义顶栏` 搜索栏中搜索 av 号 / BV 号结果的优先级, 输入编号后直接回车就可以前往视频.
|
||||||
|
- 添加跨域存储功能 (`crossOriginLocalStorage`), 支持搜索历史跨域同步. (#3893)
|
||||||
|
- 新增功能 `弹幕转义`. (#3470)
|
||||||
|
> 将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
||||||
|
|
||||||
|
## v2.6.1
|
||||||
|
`2023-02-07`
|
||||||
|
|
||||||
|
✨新增
|
||||||
|
- 优化 `简化评论区` 的时间样式. (#3541, PR #3791 by [Tinhone](https://github.com/Tinhone))
|
||||||
|
- `下载视频`:
|
||||||
|
- 支持选择备用下载地址. (PR #3798 by [Seaward233](https://github.com/Seaward233))
|
||||||
|
- 新增 `StreamSaver` 输出方式.
|
||||||
|
- 新增 `Motrix` 输出方式. (PR #3908 by [FoundTheWOUT](https://github.com/FoundTheWOUT))
|
||||||
|
- `删除广告` 支持屏蔽动态的商品推荐. (#3778)
|
||||||
|
- 优化 `自定义顶栏` 的弹窗逻辑, *有任意元素聚焦时* -> *仅在输入框聚焦时*显示弹窗. (#3756)
|
||||||
|
- `展开弹幕列表` 新增 `有选集时不自动展开` 选项. (#1734)
|
||||||
|
- 新增功能 `禁止滚轮调音量`. (#3489)
|
||||||
|
> 在网页全屏 / 全屏模式下, 禁止鼠标滚轮控制播放器的音量.
|
||||||
|
|
||||||
|
- 新增功能 `批量导入合集`. (PR #3884 by [swhoro](https://github.com/swhoro))
|
||||||
|
> 在合集页面制作一个批量导入按钮,可以新增所有合集内视频至同名收藏夹
|
||||||
|
|
||||||
|
- `扩展倍速` 添加 `隐藏移除图标` 和 `隐藏新增图标` 选项. (PR #3864 by [Tinhone](https://github.com/Tinhone))
|
||||||
|
- 新增插件 `快捷键扩展 - 开关 CC 字幕`, 默认为 <kbd>Shift</kbd> + <kbd>C</kbd>. (#3407)
|
||||||
|
- 新增插件 `快捷键扩展 - 开关弹幕列表`, 默认为 <kbd>Shift</kbd> + <kbd>D</kbd>. (#3613)
|
||||||
|
- `新版本提示` 支持在安装前查看更新说明.
|
||||||
|
- 功能面板为空时增加引导提示. (#3932, PR #3936 by [Ethkuil](https://github.com/Ethkuil))
|
||||||
|
- `下载字幕` 支持在番剧页面使用.
|
||||||
|
- 新增功能 `禁用评论区搜索词`. (#3648)
|
||||||
|
> 禁用评论区的搜索词链接.
|
||||||
|
|
||||||
|
🐛修复
|
||||||
|
- `下载视频` 的 `flv` 格式恢复 1080P 支持. (一共就 360P / 1080P, 其他清晰度还是没有, 需要的话请使用 dash 格式)
|
||||||
|
- 修复评论区功能在新版播放页上的支持, 包括 `复制评论链接` / `简化评论区` 等. (#3807)
|
||||||
|
- 修复 `清爽首页` 的排行榜第三项数据不正确. (#3911, PR #3912 by [Colerar](https://github.com/Colerar))
|
||||||
|
- 修复 `简化直播间` 对左下角红包的简化样式. (#3243)
|
||||||
|
- MDI 图标库内联至本体中, 避免阻塞页面加载. (#3964, #3888)
|
||||||
|
- 修复 `侧栏垂直偏移` 在部分直播间失效的问题. (#3934)
|
||||||
|
- 修复 `自定义顶栏` 的收藏夹在加载完毕后仍显示加载中. (#3940)
|
||||||
|
- 修复 `展开弹幕列表` 检测到的弹幕量不正确. (获取页面的已装载弹幕量太坑了, 改为调 API 获取总弹幕量来判断了) (#3958)
|
||||||
|
- 修复 `启用弹幕空降` 失效. (#3187)
|
||||||
|
- 修复 `高分辨率图片` 在个人空间投稿页失效. (#3894)
|
||||||
|
- 修复 `复制评论链接` 在个人空间复制出的链接不正确. (#3821)
|
||||||
|
- 修复 ASS 弹幕中的颜色标记不正确. (#3960)
|
||||||
|
|
||||||
|
☕开发者相关
|
||||||
|
- 完成 define API + SwitchOptions API 的改造 (受影响组件为 `简化首页` 和 `简化直播间`), 感谢 [timongh](https://github.com/timongh) 的长期贡献.
|
||||||
|
- 重新拆分了一下 PlayerAgent 的文件.
|
||||||
|
- 修复 CI 构建脚本未提交新增文件. (#3903)
|
||||||
|
- 新增 Dialog API, 可用简单封装以弹窗形式展示的内容.
|
||||||
|
- 开源许可有所变更, 对再分发有一些限制, 详见 [LICENSE](./LICENCE.md).
|
||||||
|
- 删除了 `<template>` 元素多余的 class. (#3951)
|
||||||
|
- 修复 VSCode Task 中的 ESLint 命令和 package.json 中不一致. (#3950)
|
||||||
|
- TypeScript 升级至 4.9 版本.
|
||||||
|
|
||||||
|
|
||||||
## v2.6.0-preview
|
## v2.6.0-preview
|
||||||
`2023-01-01`
|
`2023-01-01`
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,13 @@ https://afdian.net/@the1812?tab=sponsor
|
|||||||
|
|
||||||
| 时间 | 用户名 | 单号后4位 | 金额 |
|
| 时间 | 用户名 | 单号后4位 | 金额 |
|
||||||
| ------------------- | ---------------- | --------- | ------- |
|
| ------------------- | ---------------- | --------- | ------- |
|
||||||
|
| 2023.02.18 22:49:21 | \*😕\* | 9195 | ¥5.00 |
|
||||||
|
| 2023.02.14 20:40:45 | 匿名 | 9914 | ¥5.00 |
|
||||||
|
| 2023.02.13 12:33:00 | A*l | 2182 | ¥66.00 |
|
||||||
|
| 2023.02.08 16:25:37 | 匿名 | 2858 | ¥10.00 |
|
||||||
|
| 2023.02.08 12:06:19 | 匿名 | 8525 | ¥5.00 |
|
||||||
|
| 2023.02.03 23:32:39 | J*i | 6870 | ¥10.00 |
|
||||||
|
| 2023.01.02 13:13:53 | P* | 0764 | ¥5.00 |
|
||||||
| 2022.12.31 16:59:32 | *梦 | 6802 | ¥5.00 |
|
| 2022.12.31 16:59:32 | *梦 | 6802 | ¥5.00 |
|
||||||
| 2022.12.26 22:36:34 | b*8 | 3849 | ¥2.50 |
|
| 2022.12.26 22:36:34 | b*8 | 3849 | ¥2.50 |
|
||||||
| 2022.12.16 12:27:57 | *訪 | 6143 | ¥3.79 |
|
| 2022.12.16 12:27:57 | *訪 | 6143 | ¥3.79 |
|
||||||
|
|||||||
@ -415,6 +415,14 @@
|
|||||||
"fullRelativePath": "../../registry/dist/components/utils/comments/copy-link.js",
|
"fullRelativePath": "../../registry/dist/components/utils/comments/copy-link.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/utils/comments/copy-link.js"
|
"fullAbsolutePath": "registry/dist/components/utils/comments/copy-link.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "component",
|
||||||
|
"name": "disableCommentsSearchLink",
|
||||||
|
"displayName": "禁用评论区搜索词",
|
||||||
|
"description": "禁用评论区的搜索词链接.",
|
||||||
|
"fullRelativePath": "../../registry/dist/components/utils/comments/disable-search-link.js",
|
||||||
|
"fullAbsolutePath": "registry/dist/components/utils/comments/disable-search-link.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "devClient",
|
"name": "devClient",
|
||||||
@ -547,10 +555,18 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "expandDanmakuList",
|
"name": "expandDanmakuList",
|
||||||
"displayName": "展开弹幕列表",
|
"displayName": "展开弹幕列表",
|
||||||
"description": "每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿.",
|
"description": "每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿. (`最大弹幕数量` 是和视频的总弹幕量对比, 不一定和播放器实际装载的弹幕量相等)\r\n",
|
||||||
"fullRelativePath": "../../registry/dist/components/video/danmaku/expand.js",
|
"fullRelativePath": "../../registry/dist/components/video/danmaku/expand.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/video/danmaku/expand.js"
|
"fullAbsolutePath": "registry/dist/components/video/danmaku/expand.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "component",
|
||||||
|
"name": "unescapeDanmaku",
|
||||||
|
"displayName": "弹幕转义",
|
||||||
|
"description": "将弹幕中的 `\\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.\r\n",
|
||||||
|
"fullRelativePath": "../../registry/dist/components/video/danmaku/unescape.js",
|
||||||
|
"fullAbsolutePath": "registry/dist/components/video/danmaku/unescape.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "videoDefaultLocation",
|
"name": "videoDefaultLocation",
|
||||||
@ -856,6 +872,14 @@
|
|||||||
"fullRelativePath": "../../registry/dist/plugins/video/download/manual-input.js",
|
"fullRelativePath": "../../registry/dist/plugins/video/download/manual-input.js",
|
||||||
"fullAbsolutePath": "registry/dist/plugins/video/download/manual-input.js"
|
"fullAbsolutePath": "registry/dist/plugins/video/download/manual-input.js"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "plugin",
|
||||||
|
"name": "downloadVideo.outputs.motrix",
|
||||||
|
"displayName": "下载视频 - Motrix 输出支持",
|
||||||
|
"description": "为下载视频增加 Motrix 输出支持.",
|
||||||
|
"fullRelativePath": "../../registry/dist/plugins/video/download/motrix-output.js",
|
||||||
|
"fullAbsolutePath": "registry/dist/plugins/video/download/motrix-output.js"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "plugin",
|
"type": "plugin",
|
||||||
"name": "downloadVideo.outputs.mpv-playlist",
|
"name": "downloadVideo.outputs.mpv-playlist",
|
||||||
|
|||||||
@ -505,6 +505,15 @@ by [@snowraincloud](https://github.com/snowraincloud)
|
|||||||
|
|
||||||
开启后, 可在每条评论的菜单中选择复制链接.
|
开启后, 可在每条评论的菜单中选择复制链接.
|
||||||
|
|
||||||
|
### [禁用评论区搜索词](../../registry/dist/components/utils/comments/disable-search-link.js)
|
||||||
|
`disableCommentsSearchLink`
|
||||||
|
|
||||||
|
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/comments/disable-search-link.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/utils/comments/disable-search-link.js)
|
||||||
|
|
||||||
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/comments/disable-search-link.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/comments/disable-search-link.js)
|
||||||
|
|
||||||
|
禁用评论区的搜索词链接.
|
||||||
|
|
||||||
### [DevClient](../../registry/dist/components/utils/dev-client.js)
|
### [DevClient](../../registry/dist/components/utils/dev-client.js)
|
||||||
`devClient`
|
`devClient`
|
||||||
|
|
||||||
@ -687,7 +696,16 @@ by [@kdxcxs](https://github.com/kdxcxs)
|
|||||||
|
|
||||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/expand.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/expand.js)
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/expand.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/expand.js)
|
||||||
|
|
||||||
每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿.
|
每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿. (`最大弹幕数量` 是和视频的总弹幕量对比, 不一定和播放器实际装载的弹幕量相等)
|
||||||
|
|
||||||
|
### [弹幕转义](../../registry/dist/components/video/danmaku/unescape.js)
|
||||||
|
`unescapeDanmaku`
|
||||||
|
|
||||||
|
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/video/danmaku/unescape.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/video/danmaku/unescape.js)
|
||||||
|
|
||||||
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/unescape.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/unescape.js)
|
||||||
|
|
||||||
|
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
||||||
|
|
||||||
### [视频页默认定位](../../registry/dist/components/video/default-location.js)
|
### [视频页默认定位](../../registry/dist/components/video/default-location.js)
|
||||||
`videoDefaultLocation`
|
`videoDefaultLocation`
|
||||||
@ -1114,6 +1132,15 @@ by FoundTheWOUT
|
|||||||
|
|
||||||
为下载视频增加手动输入支持.
|
为下载视频增加手动输入支持.
|
||||||
|
|
||||||
|
### [下载视频 - Motrix 输出支持](../../registry/dist/plugins/video/download/motrix-output.js)
|
||||||
|
`downloadVideo.outputs.motrix`
|
||||||
|
|
||||||
|
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/video/download/motrix-output.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/video/download/motrix-output.js)
|
||||||
|
|
||||||
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/video/download/motrix-output.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/video/download/motrix-output.js)
|
||||||
|
|
||||||
|
为下载视频增加 Motrix 输出支持.
|
||||||
|
|
||||||
### [下载视频 - MPV 播放支持(列表)](../../registry/dist/plugins/video/download/mpv-output-playlist.js)
|
### [下载视频 - MPV 播放支持(列表)](../../registry/dist/plugins/video/download/mpv-output-playlist.js)
|
||||||
`downloadVideo.outputs.mpv-playlist`
|
`downloadVideo.outputs.mpv-playlist`
|
||||||
|
|
||||||
|
|||||||
24
package.json
24
package.json
@ -13,14 +13,14 @@
|
|||||||
"type": "tsc -p tsconfig.type-check.json --noEmit"
|
"type": "tsc -p tsconfig.type-check.json --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.17.9",
|
"@babel/core": "7.20.12",
|
||||||
"@babel/parser": "^7.18.5",
|
"@babel/parser": "^7.20.15",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||||
"@babel/preset-env": "^7.9.6",
|
"@babel/preset-env": "^7.20.2",
|
||||||
"@babel/preset-typescript": "^7.9.0",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@babel/types": "^7.18.4",
|
"@babel/types": "^7.20.7",
|
||||||
"@types/async-exit-hook": "^2.0.0",
|
"@types/async-exit-hook": "^2.0.0",
|
||||||
"@types/babel__core": "^7.1.19",
|
"@types/babel__core": "^7.20.0",
|
||||||
"@types/color": "^3.0.1",
|
"@types/color": "^3.0.1",
|
||||||
"@types/glob": "^7.2.0",
|
"@types/glob": "^7.2.0",
|
||||||
"@types/lodash": "^4.14.172",
|
"@types/lodash": "^4.14.172",
|
||||||
@ -31,8 +31,8 @@
|
|||||||
"@types/streamsaver": "^2.0.1",
|
"@types/streamsaver": "^2.0.1",
|
||||||
"@types/webpack-env": "^1.16.4",
|
"@types/webpack-env": "^1.16.4",
|
||||||
"@types/ws": "^8.2.3",
|
"@types/ws": "^8.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.1",
|
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||||
"@typescript-eslint/parser": "^5.9.1",
|
"@typescript-eslint/parser": "^5.50.0",
|
||||||
"@vue/compiler-sfc": "^3.2.47",
|
"@vue/compiler-sfc": "^3.2.47",
|
||||||
"@vue/tsconfig": "^0.1.3",
|
"@vue/tsconfig": "^0.1.3",
|
||||||
"async-exit-hook": "^2.0.1",
|
"async-exit-hook": "^2.0.1",
|
||||||
@ -42,7 +42,7 @@
|
|||||||
"css-loader": "^5.0.0",
|
"css-loader": "^5.0.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-airbnb-base": "^14.1.0",
|
"eslint-config-airbnb-base": "^14.1.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-plugin-import": "^2.20.1",
|
"eslint-plugin-import": "^2.20.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "7.1.0",
|
"eslint-plugin-vue": "7.1.0",
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"postcss": "^8.1.0",
|
"postcss": "^8.1.0",
|
||||||
"postcss-loader": "^4.0.4",
|
"postcss-loader": "^4.0.4",
|
||||||
"postcss-preset-env": "^7.5.0",
|
"postcss-preset-env": "^7.5.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.3",
|
||||||
"sass": "^1.25.0",
|
"sass": "^1.25.0",
|
||||||
"serve": "^13.0.2",
|
"serve": "^13.0.2",
|
||||||
"serve-handler": "^6.1.3",
|
"serve-handler": "^6.1.3",
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"terser-webpack-plugin": "^5.0.1",
|
"terser-webpack-plugin": "^5.0.1",
|
||||||
"to-string-loader": "^1.2.0",
|
"to-string-loader": "^1.2.0",
|
||||||
"ts-node": "^10.7.0",
|
"ts-node": "^10.7.0",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.9.5",
|
||||||
"vue-loader": "^17.0.0",
|
"vue-loader": "^17.0.0",
|
||||||
"vue-tsc": "^1.0.24",
|
"vue-tsc": "^1.0.24",
|
||||||
"webpack": "^5.31.2",
|
"webpack": "^5.31.2",
|
||||||
|
|||||||
1839
pnpm-lock.yaml
generated
1839
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -256,7 +256,7 @@ const ThisComponent = defineComponent({
|
|||||||
this.page++
|
this.page++
|
||||||
const cards = await this.getCards()
|
const cards = await this.getCards()
|
||||||
this.cards.push(...cards)
|
this.cards.push(...cards)
|
||||||
this.hasMorePage = cards.length === 0 || this.cards.length < this.folder.count
|
this.hasMorePage = cards.length !== 0 || this.cards.length < this.folder.count
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error)
|
logError(error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="live-info">
|
<div class="live-info">
|
||||||
<div class="live-title" :title="c.title">{{ c.title }}</div>
|
<div class="live-title" :title="c.title">{{ c.title }}</div>
|
||||||
<div class="live-name" :title="c.name">{{ c.upName }}</div>
|
<div class="live-name" :title="c.upName">{{ c.upName }}</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
|
|||||||
@ -1,3 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavbarIframeConfig } from './iframe'
|
||||||
|
import type { CustomNavbarItem } from '../custom-navbar-item'
|
||||||
|
import { usePopup } from '../mixins'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
item: CustomNavbarItem & NavbarIframeConfig
|
||||||
|
container: HTMLElement
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { el } = usePopup(props)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<iframe
|
<iframe
|
||||||
ref="el"
|
ref="el"
|
||||||
@ -7,14 +20,3 @@
|
|||||||
:height="item.height"
|
:height="item.height"
|
||||||
></iframe>
|
></iframe>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue'
|
|
||||||
import { popupProps, usePopup } from '../mixins'
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'IframePopup',
|
|
||||||
props: popupProps,
|
|
||||||
setup: usePopup,
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent } from 'vue'
|
||||||
import type { CustomNavbarItemInit } from '../custom-navbar-item'
|
import type { CustomNavbarItemInit } from '../custom-navbar-item'
|
||||||
|
|
||||||
interface NavbarIframeConfig {
|
export interface NavbarIframeConfig {
|
||||||
src: string
|
src: string
|
||||||
href: string
|
href: string
|
||||||
width: number
|
width: number
|
||||||
|
|||||||
@ -165,13 +165,6 @@ const ThisComponent = defineComponent({
|
|||||||
async remove(aid: number, index: number) {
|
async remove(aid: number, index: number) {
|
||||||
this.cards.splice(index, 1)
|
this.cards.splice(index, 1)
|
||||||
await this.toggleWatchlater(aid)
|
await this.toggleWatchlater(aid)
|
||||||
this.lastRemovedAid = aid
|
|
||||||
},
|
|
||||||
async undo() {
|
|
||||||
const aid = this.lastRemovedAid
|
|
||||||
if (aid !== 0) {
|
|
||||||
await this.toggleWatchlater(aid)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
updateFilteredCards: lodash.debounce(updateFilteredCards, 100) as unknown as () => void,
|
updateFilteredCards: lodash.debounce(updateFilteredCards, 100) as unknown as () => void,
|
||||||
},
|
},
|
||||||
@ -373,16 +366,5 @@ export default ThisComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.undo {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 16px;
|
|
||||||
left: 50%;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-50%) translateY(8px);
|
|
||||||
&.show {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(-50%) translateY(0px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1436,7 +1436,6 @@ li.timeline:hover,
|
|||||||
border-color: #99a2aa !important;
|
border-color: #99a2aa !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ri-title,
|
|
||||||
.read-push,
|
.read-push,
|
||||||
.link-more,
|
.link-more,
|
||||||
.dropdown-item,
|
.dropdown-item,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#live_recommand_report, // b站以前写的recommand, 后来改对了
|
#live_recommand_report, // b站以前写的recommand, 后来改对了
|
||||||
#live_recommend_report {
|
#live_recommend_report,
|
||||||
|
.video-container-v1 .pop-live-small-mode {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ export const component = defineComponentMetadata({
|
|||||||
addComponentListener(
|
addComponentListener(
|
||||||
`${metadata.name}.offset`,
|
`${metadata.name}.offset`,
|
||||||
(value: number) => {
|
(value: number) => {
|
||||||
document.body.style.setProperty('--be-sidebar-offset', `${value}%`)
|
document.documentElement.style.setProperty('--be-sidebar-offset', `${value}%`)
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
.be-settings > .sidebar {
|
.be-settings > .sidebar {
|
||||||
top: calc(50% + var(--be-sidebar-offset)) !important;
|
top: calc(50% + var(--be-sidebar-offset, 0)) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
断开连接
|
断开连接
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</template>
|
</template>
|
||||||
<template v-else class="status">
|
<template v-else>
|
||||||
<div class="status-dot disconnected" />
|
<div class="status-dot disconnected" />
|
||||||
<div class="status-text">未连接</div>
|
<div class="status-text">未连接</div>
|
||||||
<AsyncButton title="连接" :wait-on-click="connect">
|
<AsyncButton title="连接" :wait-on-click="connect">
|
||||||
@ -36,8 +36,6 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
client: null as DevClient | null,
|
client: null as DevClient | null,
|
||||||
// sessions: [],
|
|
||||||
// devRecords: options.devRecords,
|
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -46,12 +44,10 @@ export default defineComponent({
|
|||||||
this.client = devClient
|
this.client = devClient
|
||||||
this.updateConnectionStatus()
|
this.updateConnectionStatus()
|
||||||
devClient.addEventListener(DevClientEvents.ServerChange, this.updateConnectionStatus)
|
devClient.addEventListener(DevClientEvents.ServerChange, this.updateConnectionStatus)
|
||||||
// devClient.addEventListener(DevClientEvents.SessionsUpdate, this.updateSessionsStatus)
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
const devClient = this.client as DevClient
|
const devClient = this.client as DevClient
|
||||||
devClient.removeEventListener(DevClientEvents.ServerChange, this.updateConnectionStatus)
|
devClient.removeEventListener(DevClientEvents.ServerChange, this.updateConnectionStatus)
|
||||||
// devClient.removeEventListener(DevClientEvents.SessionsUpdate, this.updateSessionsStatus)
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async connect() {
|
async connect() {
|
||||||
@ -63,9 +59,6 @@ export default defineComponent({
|
|||||||
updateConnectionStatus() {
|
updateConnectionStatus() {
|
||||||
this.isConnected = this.client.isConnected
|
this.isConnected = this.client.isConnected
|
||||||
},
|
},
|
||||||
updateSessionsStatus() {
|
|
||||||
this.sessions = [...this.client.sessions]
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -30,6 +30,10 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
|
|||||||
if (excludeSelectors.some(it => element.matches(it))) {
|
if (excludeSelectors.some(it => element.matches(it))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 带 , 的 srcset 不处理
|
||||||
|
if (value.includes(',')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const match = value.match(resizeRegex)
|
const match = value.match(resizeRegex)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return
|
return
|
||||||
@ -58,6 +62,10 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
|
|||||||
e => e.getAttribute('src'),
|
e => e.getAttribute('src'),
|
||||||
(e, v) => e.setAttribute('src', v),
|
(e, v) => e.setAttribute('src', v),
|
||||||
)
|
)
|
||||||
|
replaceSource(
|
||||||
|
e => e.getAttribute('srcset'),
|
||||||
|
(e, v) => e.setAttribute('srcset', v),
|
||||||
|
)
|
||||||
replaceSource(
|
replaceSource(
|
||||||
e => e.style.backgroundImage,
|
e => e.style.backgroundImage,
|
||||||
(e, v) => (e.style.backgroundImage = v),
|
(e, v) => (e.style.backgroundImage = v),
|
||||||
|
|||||||
@ -19,7 +19,7 @@ const importSeries = async (sid, uid, csrf) => {
|
|||||||
|
|
||||||
// 创建收藏夹,获取新收藏夹id
|
// 创建收藏夹,获取新收藏夹id
|
||||||
let favId = 0
|
let favId = 0
|
||||||
while (true) {
|
for (;;) {
|
||||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
|
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@ -43,7 +43,7 @@ const importSeries = async (sid, uid, csrf) => {
|
|||||||
for (let i = 0; i < seriesVideos.length; i++) {
|
for (let i = 0; i < seriesVideos.length; i++) {
|
||||||
// 做个延迟,防止太快而遭服务器拒绝
|
// 做个延迟,防止太快而遭服务器拒绝
|
||||||
await delay(500)
|
await delay(500)
|
||||||
while (true) {
|
for (;;) {
|
||||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
|
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@ -72,7 +72,7 @@ const importSeries = async (sid, uid, csrf) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const importCollection = async (sid, csrf) => {
|
const importCollection = async (sid, csrf) => {
|
||||||
while (true) {
|
for (;;) {
|
||||||
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
|
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
.bili-dm,
|
||||||
.b-danmaku {
|
.b-danmaku {
|
||||||
&.airborne {
|
&.airborne {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export const component = defineComponentMetadata({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
if (!target.classList.contains('b-danmaku')) {
|
if (!['b-danmaku', 'bili-dm'].some(token => target.classList.contains(token))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const time = getAirborneTime(target.textContent)
|
const time = getAirborneTime(target.textContent)
|
||||||
@ -60,11 +60,11 @@ export const component = defineComponentMetadata({
|
|||||||
unsafeWindow.player.seek(time, false)
|
unsafeWindow.player.seek(time, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const addAirborneStyle = (danmaku: DanmakuRecord) => {
|
const detectAirborne = (danmaku: DanmakuRecord) => {
|
||||||
const canAirborne = !Number.isNaN(getAirborneTime(danmaku.text))
|
const canAirborne = !Number.isNaN(getAirborneTime(danmaku.text))
|
||||||
danmaku.element.classList.toggle('airborne', canAirborne)
|
danmaku.element.classList.toggle('airborne', canAirborne)
|
||||||
}
|
}
|
||||||
forEachVideoDanmaku({ added: addAirborneStyle })
|
forEachVideoDanmaku({ added: detectAirborne })
|
||||||
videoChange(async () => {
|
videoChange(async () => {
|
||||||
const wrapper = (await playerAgent.query.video.wrap()) as HTMLElement
|
const wrapper = (await playerAgent.query.video.wrap()) as HTMLElement
|
||||||
if (wrapper.classList.contains('airborne-enabled')) {
|
if (wrapper.classList.contains('airborne-enabled')) {
|
||||||
|
|||||||
@ -285,3 +285,34 @@ const decode = lodash.curry(async (type: string, blob: Blob) => {
|
|||||||
})
|
})
|
||||||
export const decodeDanmakuSegment = decode('DmSegMobileReply')
|
export const decodeDanmakuSegment = decode('DmSegMobileReply')
|
||||||
export const decodeDanmakuView = decode('DmWebViewReply')
|
export const decodeDanmakuView = decode('DmWebViewReply')
|
||||||
|
|
||||||
|
// 这里为了兼容 pakku, 只能用 fetch https://github.com/xmcp/pakku.js/issues/153
|
||||||
|
const fetchBlob = async (url: string) => {
|
||||||
|
const response = await fetch(url)
|
||||||
|
return response.blob()
|
||||||
|
}
|
||||||
|
export const getDanmakuView = async (aid: string | number, cid: string | number) => {
|
||||||
|
const viewBlob = await fetchBlob(
|
||||||
|
`https://api.bilibili.com/x/v2/dm/web/view?type=1&oid=${cid}&pid=${aid}`,
|
||||||
|
)
|
||||||
|
if (!viewBlob) {
|
||||||
|
throw new Error('获取弹幕信息失败')
|
||||||
|
}
|
||||||
|
const view = await decodeDanmakuView(viewBlob)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
export const getDanmakuSegment = async (
|
||||||
|
aid: string | number,
|
||||||
|
cid: string | number,
|
||||||
|
index: number,
|
||||||
|
) => {
|
||||||
|
const blob = await fetchBlob(
|
||||||
|
`https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid=${cid}&pid=${aid}&segment_index=${
|
||||||
|
index + 1
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
if (!blob) {
|
||||||
|
throw new Error(`弹幕片段${index + 1}下载失败`)
|
||||||
|
}
|
||||||
|
return decodeDanmakuSegment(blob)
|
||||||
|
}
|
||||||
|
|||||||
@ -43,19 +43,8 @@ export class JsonDanmaku {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
async fetchInfo() {
|
async fetchInfo() {
|
||||||
const { decodeDanmakuSegment, decodeDanmakuView } = await import('../converter/danmaku-segment')
|
const { getDanmakuSegment, getDanmakuView } = await import('../converter/danmaku-segment')
|
||||||
// 这里为了兼容 pakku, 只能用 fetch https://github.com/xmcp/pakku.js/issues/153
|
const view = await getDanmakuView(this.aid, this.cid)
|
||||||
const fetchBlob = async (url: string) => {
|
|
||||||
const response = await fetch(url)
|
|
||||||
return response.blob()
|
|
||||||
}
|
|
||||||
const viewBlob = await fetchBlob(
|
|
||||||
`https://api.bilibili.com/x/v2/dm/web/view?type=1&oid=${this.cid}&pid=${this.aid}`,
|
|
||||||
)
|
|
||||||
if (!viewBlob) {
|
|
||||||
throw new Error('获取弹幕信息失败')
|
|
||||||
}
|
|
||||||
const view = await decodeDanmakuView(viewBlob)
|
|
||||||
const { total } = view.dmSge
|
const { total } = view.dmSge
|
||||||
if (total === undefined) {
|
if (total === undefined) {
|
||||||
throw new Error(`获取弹幕分页数失败: ${JSON.stringify(lodash.omit(view, 'flag'))}`)
|
throw new Error(`获取弹幕分页数失败: ${JSON.stringify(lodash.omit(view, 'flag'))}`)
|
||||||
@ -63,18 +52,14 @@ export class JsonDanmaku {
|
|||||||
console.log('segment count =', total)
|
console.log('segment count =', total)
|
||||||
const segments = await Promise.all(
|
const segments = await Promise.all(
|
||||||
new Array(total).fill(0).map(async (_, index) => {
|
new Array(total).fill(0).map(async (_, index) => {
|
||||||
const blob = await fetchBlob(
|
try {
|
||||||
`https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid=${this.cid}&pid=${
|
const result = await getDanmakuSegment(this.aid, this.cid, index)
|
||||||
this.aid
|
console.log(`received blob for segment ${index + 1}`, result)
|
||||||
}&segment_index=${index + 1}`,
|
|
||||||
)
|
|
||||||
if (!blob) {
|
|
||||||
logError(new Error(`弹幕片段${index + 1}下载失败`))
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
console.log(`received blob for segment ${index + 1}`, blob)
|
|
||||||
const result = await decodeDanmakuSegment(blob)
|
|
||||||
return result.elems ?? []
|
return result.elems ?? []
|
||||||
|
} catch (error) {
|
||||||
|
logError(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
this.jsonDanmakus = segments.flat().sort(ascendingSort(it => it.progress ?? 0))
|
this.jsonDanmakus = segments.flat().sort(ascendingSort(it => it.progress ?? 0))
|
||||||
|
|||||||
1
registry/lib/components/video/danmaku/expand/index.md
Normal file
1
registry/lib/components/video/danmaku/expand/index.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿. (`最大弹幕数量` 是和视频的总弹幕量对比, 不一定和播放器实际装载的弹幕量相等)
|
||||||
@ -1,47 +1,20 @@
|
|||||||
import { defineComponentMetadata } from '@/components/define'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import type { ComponentEntry } from '@/components/types'
|
import type { ComponentEntry } from '@/components/types'
|
||||||
import { videoChange } from '@/core/observer'
|
import { videoChange } from '@/core/observer'
|
||||||
import { select, sq } from '@/core/spin-query'
|
import { select } from '@/core/spin-query'
|
||||||
import { matchUrlPattern } from '@/core/utils'
|
import { matchUrlPattern } from '@/core/utils'
|
||||||
import { useScopedConsole } from '@/core/utils/log'
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
import { mediaListUrls, videoAndBangumiUrls } from '@/core/utils/urls'
|
import { mediaListUrls, videoAndBangumiUrls } from '@/core/utils/urls'
|
||||||
|
|
||||||
const parseCountMatch = (countMatch: RegExpMatchArray) => {
|
|
||||||
if (!countMatch) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const count = parseInt(countMatch[0])
|
|
||||||
if (Number.isNaN(count)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
// 2.x: n
|
|
||||||
const getDanmakuCountV2 = async () => {
|
|
||||||
const countElement = await select('.bilibili-player-video-info-danmaku-number')
|
|
||||||
const countText = countElement?.textContent ?? ''
|
|
||||||
const countMatch = countText.match(/\d+/)
|
|
||||||
return parseCountMatch(countMatch)
|
|
||||||
}
|
|
||||||
// 3.x: 已装填 n 条弹幕
|
|
||||||
const getDanmakuCountV3 = async () => {
|
|
||||||
const countElement = await select('.bpx-player-video-info-dm')
|
|
||||||
await sq(
|
|
||||||
() => dq('.bpx-player-video-info-online b'),
|
|
||||||
it => it && it.textContent !== '-',
|
|
||||||
)
|
|
||||||
const countText = countElement?.textContent ?? ''
|
|
||||||
const countMatch = countText.match(/\d+/)
|
|
||||||
return parseCountMatch(countMatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry: ComponentEntry = async ({ settings: { options } }) => {
|
const entry: ComponentEntry = async ({ settings: { options } }) => {
|
||||||
const console = useScopedConsole('展开弹幕列表')
|
const console = useScopedConsole('展开弹幕列表')
|
||||||
videoChange(async () => {
|
const { getDanmakuView } = await import('../converter/danmaku-segment')
|
||||||
|
videoChange(async ({ aid, cid }) => {
|
||||||
if (mediaListUrls.some(url => matchUrlPattern(url)) && options.ignoreMediaList) {
|
if (mediaListUrls.some(url => matchUrlPattern(url)) && options.ignoreMediaList) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const danmakuCount = await Promise.race([getDanmakuCountV2(), getDanmakuCountV3()])
|
const view = await getDanmakuView(aid, cid)
|
||||||
|
const danmakuCount = view.count
|
||||||
console.log(`当前弹幕量: ${danmakuCount}`)
|
console.log(`当前弹幕量: ${danmakuCount}`)
|
||||||
if (danmakuCount !== null && danmakuCount > options.maxDanmakuCount) {
|
if (danmakuCount !== null && danmakuCount > options.maxDanmakuCount) {
|
||||||
console.log(`超过了最大弹幕数量 ${options.maxDanmakuCount}, 跳过展开`)
|
console.log(`超过了最大弹幕数量 ${options.maxDanmakuCount}, 跳过展开`)
|
||||||
@ -64,10 +37,6 @@ export const component = defineComponentMetadata({
|
|||||||
displayName: '展开弹幕列表',
|
displayName: '展开弹幕列表',
|
||||||
entry,
|
entry,
|
||||||
tags: [componentsTags.video],
|
tags: [componentsTags.video],
|
||||||
description: {
|
|
||||||
'zh-CN':
|
|
||||||
'每次加载视频时自动展开弹幕列表, 如果弹幕装载量超过 `最大弹幕数量`, 则不展开以免对页面造成卡顿.',
|
|
||||||
},
|
|
||||||
options: {
|
options: {
|
||||||
ignoreMediaList: {
|
ignoreMediaList: {
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
|
|||||||
1
registry/lib/components/video/danmaku/unescape/index.md
Normal file
1
registry/lib/components/video/danmaku/unescape/index.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
||||||
19
registry/lib/components/video/danmaku/unescape/index.ts
Normal file
19
registry/lib/components/video/danmaku/unescape/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
import { forEachVideoDanmaku } from '@/components/video/video-danmaku'
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'unescapeDanmaku',
|
||||||
|
displayName: '弹幕转义',
|
||||||
|
tags: [componentsTags.video],
|
||||||
|
entry: () => {
|
||||||
|
const newLineRegex = /\\n/g
|
||||||
|
forEachVideoDanmaku({
|
||||||
|
added: danmaku => {
|
||||||
|
if (newLineRegex.test(danmaku.text)) {
|
||||||
|
const newText = danmaku.text.replace(newLineRegex, '\n')
|
||||||
|
danmaku.element.textContent = newText
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@ -448,6 +448,7 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
|
|||||||
setTimeout(() => this.forceUpdateStyle(value))
|
setTimeout(() => this.forceUpdateStyle(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected readonly filterNativeSpeed =
|
protected readonly filterNativeSpeed =
|
||||||
() =>
|
() =>
|
||||||
({ subscribe, next }: PublishContext<number>) => {
|
({ subscribe, next }: PublishContext<number>) => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { playerUrls } from '@/core/utils/urls'
|
|||||||
|
|
||||||
const entry = async () => {
|
const entry = async () => {
|
||||||
let lastAid: string
|
let lastAid: string
|
||||||
const removeCover = () => document.body.style.removeProperty('--cover-url')
|
const removeCover = () => document.documentElement.style.removeProperty('--cover-url')
|
||||||
const videoPrototype = (await isBwpVideo()) ? BwpElement.prototype : HTMLVideoElement.prototype
|
const videoPrototype = (await isBwpVideo()) ? BwpElement.prototype : HTMLVideoElement.prototype
|
||||||
// bpx player 改了 video.play(), hook 直接挂 HTMLVideoElement.prototype 上没效果 (#3698)
|
// bpx player 改了 video.play(), hook 直接挂 HTMLVideoElement.prototype 上没效果 (#3698)
|
||||||
const bpxPlayer = dq('.bpx-player-video-wrap')
|
const bpxPlayer = dq('.bpx-player-video-wrap')
|
||||||
@ -41,7 +41,7 @@ const entry = async () => {
|
|||||||
const { VideoInfo } = await import('@/components/video/video-info')
|
const { VideoInfo } = await import('@/components/video/video-info')
|
||||||
const info = new VideoInfo(aid)
|
const info = new VideoInfo(aid)
|
||||||
await info.fetchInfo()
|
await info.fetchInfo()
|
||||||
document.body.style.setProperty('--cover-url', `url('${info.coverUrl}')`)
|
document.documentElement.style.setProperty('--cover-url', `url('${info.coverUrl}')`)
|
||||||
}
|
}
|
||||||
videoChange(showCover)
|
videoChange(showCover)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.6.0",
|
"version": "2.6.3",
|
||||||
"author": "Grant Howard, Coulomb-G",
|
"author": "Grant Howard, Coulomb-G",
|
||||||
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
62
src/components/bisector/DialogContent.vue
Normal file
62
src/components/bisector/DialogContent.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bisector-dialog-content-container">
|
||||||
|
<div class="bisector-dialog-content-text">
|
||||||
|
组件二等分进行中,预计剩余
|
||||||
|
<b style="color: var(--theme-color)">{{ rouge }} </b>轮次,请确认当前情况:
|
||||||
|
</div>
|
||||||
|
<div class="bisector-dialog-button-group">
|
||||||
|
<VButton @click="$emit('good')">正常</VButton>
|
||||||
|
<VButton @click="$emit('bad')">不正常</VButton>
|
||||||
|
<VButton type="primary" @click="$emit('abort')">终止</VButton>
|
||||||
|
</div>
|
||||||
|
<div class="bisector-dialog-content-text weak">
|
||||||
|
*可以点击×号关闭对话框,刷新页面后,会再次询问
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { VButton } from '@/ui'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { VButton },
|
||||||
|
props: {
|
||||||
|
rouge: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
abort: null as () => void,
|
||||||
|
good: null as () => void,
|
||||||
|
bad: null as () => void,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.bisector-dialog-content-container {
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
.bisector-dialog-content-text {
|
||||||
|
&.weak {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bisector-dialog-content-text,
|
||||||
|
.bisector-dialog-button-group {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bisector-dialog-button-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
.be-button:last-child {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
43
src/components/bisector/DialogTitle.vue
Normal file
43
src/components/bisector/DialogTitle.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
组件二等分
|
||||||
|
<div
|
||||||
|
class="peek"
|
||||||
|
title="透视"
|
||||||
|
style="margin-left: auto"
|
||||||
|
@mouseover="peek = true"
|
||||||
|
@mouseout="peek = false"
|
||||||
|
>
|
||||||
|
<VIcon icon="eye" :size="18" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { VIcon } from '@/ui'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: { VIcon },
|
||||||
|
data() {
|
||||||
|
return { peek: false }
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
peek(value) {
|
||||||
|
this.$el.closest('.be-dialog').style.opacity = value ? '0.1' : '1'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import 'common';
|
||||||
|
|
||||||
|
div {
|
||||||
|
@include h-center(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peek {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
src/components/bisector/ResultToastContent.vue
Normal file
65
src/components/bisector/ResultToastContent.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>可能出问题的组件是:{{ displayName }}({{ name }})</div>
|
||||||
|
<div>
|
||||||
|
<span class="link" @click="restore">恢复原状({{ countdown }} 秒)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from 'vue'
|
||||||
|
|
||||||
|
import type { UserComponent } from './api'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
userComponent: {
|
||||||
|
type: Object as PropType<UserComponent>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
restore: null as () => void,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
countdown: 30,
|
||||||
|
interval: undefined as number | undefined,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayName() {
|
||||||
|
return this.userComponent.metadata?.displayName
|
||||||
|
},
|
||||||
|
name() {
|
||||||
|
return this.userComponent.metadata?.name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
countdown(value) {
|
||||||
|
if (value === 0) {
|
||||||
|
this.restore()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
if (this.countdown > 0) {
|
||||||
|
this.countdown--
|
||||||
|
} else {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
}
|
||||||
|
}, 1e3)
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
restore() {
|
||||||
|
clearInterval(this.interval)
|
||||||
|
this.$emit('restore')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
196
src/components/bisector/api.ts
Normal file
196
src/components/bisector/api.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import type { DialogInstance } from '@/core/dialog'
|
||||||
|
import { showDialog } from '@/core/dialog'
|
||||||
|
import type { Settings } from '@/core/settings/types'
|
||||||
|
import { Toast } from '@/core/toast'
|
||||||
|
import { getRandomId, delay, createVueApp } from '@/core/utils'
|
||||||
|
import type { useScopedConsole } from '@/core/utils/log'
|
||||||
|
import type { RecordValue } from '../types'
|
||||||
|
import type { BisectNext, BisectReturn } from './bisect'
|
||||||
|
import { bisect } from './bisect'
|
||||||
|
import type { BisectorOptions } from './options'
|
||||||
|
import ResultToastContent from './ResultToastContent.vue'
|
||||||
|
|
||||||
|
export type UserComponent = RecordValue<Settings['userComponents']>
|
||||||
|
|
||||||
|
let bisectorOptions: BisectorOptions
|
||||||
|
let scopedConsole: ReturnType<typeof useScopedConsole>
|
||||||
|
let bisectorGenerator: Generator<BisectNext<UserComponent>, BisectReturn<UserComponent>>
|
||||||
|
let groupedComponents: Awaited<ReturnType<typeof classifyComponents>>
|
||||||
|
let dialog: DialogInstance
|
||||||
|
|
||||||
|
export const setOptions = (options: BisectorOptions) => {
|
||||||
|
if (!bisectorOptions) {
|
||||||
|
bisectorOptions = options
|
||||||
|
} else {
|
||||||
|
Object.assign(bisectorOptions, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setConsole = (console: ReturnType<typeof useScopedConsole>) => {
|
||||||
|
scopedConsole = console
|
||||||
|
}
|
||||||
|
|
||||||
|
const classifyComponents = async () => {
|
||||||
|
const { settings } = await import('@/core/settings')
|
||||||
|
|
||||||
|
const { userComponents } = settings
|
||||||
|
const configurableUserComponents = lodash.pickBy(
|
||||||
|
userComponents,
|
||||||
|
v => v.metadata.configurable || true,
|
||||||
|
)
|
||||||
|
const { targetComponents, keepDisabledComponents, keepEnabledComponents } = lodash.transform(
|
||||||
|
configurableUserComponents,
|
||||||
|
(result, v, k) => {
|
||||||
|
if (bisectorOptions.keepEnabledComponents?.includes(k)) {
|
||||||
|
result.keepEnabledComponents.push(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bisectorOptions.keepDisabledComponents?.includes(k)) {
|
||||||
|
result.keepDisabledComponents.push(v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.targetComponents.push(v)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targetComponents: [] as UserComponent[],
|
||||||
|
keepDisabledComponents: [] as UserComponent[],
|
||||||
|
keepEnabledComponents: [] as UserComponent[],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
userComponents,
|
||||||
|
configurableUserComponents,
|
||||||
|
targetComponents,
|
||||||
|
keepDisabledComponents,
|
||||||
|
keepEnabledComponents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setComponentsEnabled = (components: UserComponent[], enabled: boolean) =>
|
||||||
|
components.forEach(component => (component.settings.enabled = enabled))
|
||||||
|
|
||||||
|
const getComponentNames = (components: UserComponent[]) =>
|
||||||
|
components
|
||||||
|
.map(component => `${component.metadata.displayName}(${component.metadata.name})`)
|
||||||
|
.join(', ') || '无'
|
||||||
|
|
||||||
|
export const isRecover = () => !lodash.isEmpty(bisectorOptions.bisectInitialState)
|
||||||
|
|
||||||
|
export const stop = async () => {
|
||||||
|
scopedConsole?.log('stop - 准备停止组件二等分')
|
||||||
|
dialog?.close()
|
||||||
|
const { configurableUserComponents } = await classifyComponents()
|
||||||
|
const unmatchedComponents: UserComponent[] = []
|
||||||
|
for (const [componentName, componentSettings] of Object.entries(configurableUserComponents)) {
|
||||||
|
const originalStatus = bisectorOptions.originalComponentEnableState?.[componentName]
|
||||||
|
if (originalStatus == null) {
|
||||||
|
unmatchedComponents.push(componentSettings)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
componentSettings.settings.enabled = originalStatus
|
||||||
|
}
|
||||||
|
if (unmatchedComponents.length) {
|
||||||
|
const msg = `部分组件未能还原状态:${getComponentNames(unmatchedComponents)}`
|
||||||
|
scopedConsole?.warn(`stop - ${msg}`)
|
||||||
|
Toast.error(msg, '组件二等分')
|
||||||
|
await delay(3e3)
|
||||||
|
}
|
||||||
|
scopedConsole?.log('stop - 清理状态')
|
||||||
|
bisectorGenerator = null
|
||||||
|
bisectorOptions.bisectInitialState = {}
|
||||||
|
bisectorOptions.originalComponentEnableState = {}
|
||||||
|
scopedConsole?.log('stop - 重载页面')
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const next = async (seeingBad?: boolean, autoReload?: boolean) => {
|
||||||
|
dialog?.close()
|
||||||
|
scopedConsole?.log(
|
||||||
|
`next - 当前工作状态:${
|
||||||
|
// eslint-disable-next-line no-nested-ternary
|
||||||
|
seeingBad == null ? '未知' : seeingBad ? '异常' : '正常'
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
const { done, value } = bisectorGenerator.next(seeingBad)
|
||||||
|
if (done) {
|
||||||
|
const { low, high } = value
|
||||||
|
bisectorOptions.bisectInitialState = { low, high }
|
||||||
|
const elementId = `bisector-result-toast-content-${getRandomId()}`
|
||||||
|
Toast.info(/* html */ `<div id="${elementId}"></div>`, '组件二等分结果')
|
||||||
|
await delay()
|
||||||
|
const vueApp = createVueApp(ResultToastContent, {
|
||||||
|
userComponent: value.target,
|
||||||
|
onRestore: stop,
|
||||||
|
})
|
||||||
|
vueApp.mount(`#${elementId}`)
|
||||||
|
} else {
|
||||||
|
const { slice, low, high } = value as BisectNext<UserComponent>
|
||||||
|
const needEnabled = slice
|
||||||
|
const needDisabled = lodash.difference(groupedComponents.targetComponents, slice)
|
||||||
|
bisectorOptions.bisectInitialState = { low, high }
|
||||||
|
scopedConsole?.log(`next - 关闭组件:${getComponentNames(needDisabled)}`)
|
||||||
|
scopedConsole?.log(`next - 开启组件:${getComponentNames(needEnabled)}`)
|
||||||
|
setComponentsEnabled(needDisabled, false)
|
||||||
|
setComponentsEnabled(needEnabled, true)
|
||||||
|
if (autoReload) {
|
||||||
|
scopedConsole?.log('next - 重载页面')
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { done, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const recover = async () => {
|
||||||
|
scopedConsole?.log('recover - 准备恢复组件二等分')
|
||||||
|
groupedComponents = await classifyComponents()
|
||||||
|
const { targetComponents, keepDisabledComponents, keepEnabledComponents } = groupedComponents
|
||||||
|
setComponentsEnabled(keepEnabledComponents, true)
|
||||||
|
setComponentsEnabled(keepDisabledComponents, false)
|
||||||
|
scopedConsole?.log(`recover - 保持关闭组件:${getComponentNames(keepDisabledComponents)}`)
|
||||||
|
scopedConsole?.log(`recover - 保持开启组件:${getComponentNames(keepEnabledComponents)}`)
|
||||||
|
scopedConsole?.log(`recover - 全部目标组件:${getComponentNames(targetComponents)}`)
|
||||||
|
bisectorGenerator = bisect(targetComponents, bisectorOptions.bisectInitialState)
|
||||||
|
const { done, value } = await next()
|
||||||
|
if (!done) {
|
||||||
|
const { rouge } = value as BisectNext<UserComponent>
|
||||||
|
dialog = showDialog({
|
||||||
|
title: () => import('./DialogTitle.vue'),
|
||||||
|
content: () => import('./DialogContent.vue'),
|
||||||
|
contentProps: {
|
||||||
|
rouge,
|
||||||
|
onGood: () => next(false, true),
|
||||||
|
onBad: () => next(true, true),
|
||||||
|
onAbort: () => stop(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const start = async () => {
|
||||||
|
if (isRecover()) {
|
||||||
|
await recover()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scopedConsole?.log('start - 准备开始组件二等分')
|
||||||
|
groupedComponents = await classifyComponents()
|
||||||
|
const {
|
||||||
|
configurableUserComponents,
|
||||||
|
targetComponents,
|
||||||
|
keepDisabledComponents,
|
||||||
|
keepEnabledComponents,
|
||||||
|
} = groupedComponents
|
||||||
|
scopedConsole?.log('start - 保存组件初始启用状态')
|
||||||
|
bisectorOptions.originalComponentEnableState = lodash.mapValues(
|
||||||
|
configurableUserComponents,
|
||||||
|
v => v.settings.enabled,
|
||||||
|
)
|
||||||
|
setComponentsEnabled(targetComponents, true)
|
||||||
|
setComponentsEnabled(keepEnabledComponents, true)
|
||||||
|
setComponentsEnabled(keepDisabledComponents, false)
|
||||||
|
scopedConsole?.log(`start - 保持关闭组件:${getComponentNames(keepDisabledComponents)}`)
|
||||||
|
scopedConsole?.log(`start - 保持开启组件:${getComponentNames(keepEnabledComponents)}`)
|
||||||
|
scopedConsole?.log(`start - 启用全部目标组件:${getComponentNames(targetComponents)}`)
|
||||||
|
bisectorGenerator = bisect(targetComponents, bisectorOptions.bisectInitialState)
|
||||||
|
await next(undefined, true)
|
||||||
|
}
|
||||||
92
src/components/bisector/bisect.ts
Normal file
92
src/components/bisector/bisect.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/* eslint-disable no-bitwise */
|
||||||
|
|
||||||
|
export interface BisectNext<O> {
|
||||||
|
low: number
|
||||||
|
high: number
|
||||||
|
mid: number
|
||||||
|
slice: O[]
|
||||||
|
rouge: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BisectReturn<O> extends BisectNext<O> {
|
||||||
|
target: O
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitialState {
|
||||||
|
low?: number
|
||||||
|
high?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* bisectLeft<O>(
|
||||||
|
data: readonly O[],
|
||||||
|
initialState?: InitialState,
|
||||||
|
): Generator<BisectNext<O>, BisectReturn<O>> {
|
||||||
|
let low = initialState?.low ?? 0
|
||||||
|
let high = initialState?.high ?? data.length
|
||||||
|
let mid: number
|
||||||
|
|
||||||
|
while (low + 1 < high) {
|
||||||
|
mid = (low + high) >>> 1
|
||||||
|
|
||||||
|
const seeingBad = yield {
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
mid,
|
||||||
|
slice: data.slice(low, mid),
|
||||||
|
rouge: Math.trunc(Math.log2(high - low)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seeingBad) {
|
||||||
|
high = mid
|
||||||
|
} else {
|
||||||
|
low = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
mid,
|
||||||
|
slice: data.slice(low, mid),
|
||||||
|
rouge: Math.trunc(Math.log2(high - low)),
|
||||||
|
target: data[low],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* bisectRight<O>(
|
||||||
|
data: readonly O[],
|
||||||
|
initialState?: InitialState,
|
||||||
|
): Generator<BisectNext<O>, BisectReturn<O>> {
|
||||||
|
let low = initialState?.low ?? 0
|
||||||
|
let high = initialState?.high ?? data.length
|
||||||
|
let mid = (low + high) >>> 1
|
||||||
|
|
||||||
|
while (low + 1 < high) {
|
||||||
|
mid = (low + high) >>> 1
|
||||||
|
|
||||||
|
const seeingBad = yield {
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
mid,
|
||||||
|
slice: data.slice(mid, high),
|
||||||
|
rouge: Math.trunc(Math.log2(high - low)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seeingBad) {
|
||||||
|
low = mid
|
||||||
|
} else {
|
||||||
|
high = mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
low,
|
||||||
|
high,
|
||||||
|
mid,
|
||||||
|
slice: data.slice(low, mid),
|
||||||
|
rouge: Math.trunc(Math.log2(high - low)),
|
||||||
|
target: data[low],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bisect = bisectLeft
|
||||||
45
src/components/bisector/index.ts
Normal file
45
src/components/bisector/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
|
import { bisectorOptionsMetadata } from './options'
|
||||||
|
import { LifeCycleEventTypes } from '@/core/life-cycle'
|
||||||
|
import { componentsTags } from '@/components/types'
|
||||||
|
import * as bisector from './api'
|
||||||
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
|
import type { LaunchBarActionProvider } from '../launch-bar/launch-bar-action'
|
||||||
|
|
||||||
|
export const component = defineComponentMetadata({
|
||||||
|
name: 'bisector',
|
||||||
|
displayName: '组件二等分',
|
||||||
|
tags: [componentsTags.general, componentsTags.utils],
|
||||||
|
hidden: true,
|
||||||
|
configurable: false,
|
||||||
|
entry: async ({ settings: { options } }) => {
|
||||||
|
bisector.setOptions(options)
|
||||||
|
bisector.setConsole(useScopedConsole('组件二等分'))
|
||||||
|
unsafeWindow.addEventListener(LifeCycleEventTypes.ComponentsLoaded, () => {
|
||||||
|
if (bisector.isRecover()) {
|
||||||
|
bisector.recover()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
options: bisectorOptionsMetadata,
|
||||||
|
plugin: {
|
||||||
|
displayName: '组件二等分 - 功能扩展',
|
||||||
|
setup: ({ addData }) => {
|
||||||
|
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
|
||||||
|
providers.push({
|
||||||
|
name: 'bisector-start',
|
||||||
|
getActions: async () => [
|
||||||
|
{
|
||||||
|
name: '开始 / 继续组件二等分',
|
||||||
|
description: 'Start/Continue component bisection',
|
||||||
|
icon: 'mdi-view-split-horizontal',
|
||||||
|
action: async () => {
|
||||||
|
await bisector.start()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
31
src/components/bisector/options.ts
Normal file
31
src/components/bisector/options.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { defineOptionsMetadata, type OptionsOfMetadata } from '@/components/define'
|
||||||
|
import { getComponentSettings } from '@/core/settings'
|
||||||
|
import type { InitialState } from './bisect'
|
||||||
|
|
||||||
|
export const bisectorOptionsMetadata = defineOptionsMetadata({
|
||||||
|
// 原始的组件启用状态
|
||||||
|
originalComponentEnableState: {
|
||||||
|
defaultValue: {} as Record<string, boolean>,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
// 保持禁用状态的组件内部名称数组
|
||||||
|
keepDisabledComponents: {
|
||||||
|
defaultValue: [] as string[],
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
// 保持启用状态的组件内部名称数组
|
||||||
|
keepEnabledComponents: {
|
||||||
|
defaultValue: [] as string[],
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
// bisect 生成器的初始状态
|
||||||
|
bisectInitialState: {
|
||||||
|
defaultValue: {} as Partial<InitialState>,
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getBisectorOptions = () =>
|
||||||
|
getComponentSettings<OptionsOfMetadata<typeof bisectorOptionsMetadata>>('bisector').options
|
||||||
|
|
||||||
|
export type BisectorOptions = ReturnType<typeof getBisectorOptions>
|
||||||
@ -2,6 +2,7 @@ import { component as AutoUpdate } from './auto-update'
|
|||||||
import { component as I18n } from './i18n'
|
import { component as I18n } from './i18n'
|
||||||
import { component as LaunchBar } from './launch-bar'
|
import { component as LaunchBar } from './launch-bar'
|
||||||
import { component as NotifyNewVersion } from './notify-new-version'
|
import { component as NotifyNewVersion } from './notify-new-version'
|
||||||
|
import { component as Bisector } from './bisector'
|
||||||
import { component as SettingsPanel } from './settings-panel'
|
import { component as SettingsPanel } from './settings-panel'
|
||||||
import type { ComponentMetadata } from './types'
|
import type { ComponentMetadata } from './types'
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export const getBuiltInComponents = (): ComponentMetadata[] => [
|
|||||||
I18n,
|
I18n,
|
||||||
AutoUpdate,
|
AutoUpdate,
|
||||||
NotifyNewVersion,
|
NotifyNewVersion,
|
||||||
|
Bisector,
|
||||||
]
|
]
|
||||||
|
|
||||||
export const isBuiltInComponent = (name: string) =>
|
export const isBuiltInComponent = (name: string) =>
|
||||||
|
|||||||
@ -144,19 +144,27 @@ export const loadComponent = async (component: ComponentMetadata) => {
|
|||||||
/** 加载所有用户组件的定义 (不运行) */
|
/** 加载所有用户组件的定义 (不运行) */
|
||||||
export const loadAllUserComponents = async () => {
|
export const loadAllUserComponents = async () => {
|
||||||
const { settings } = await import('@/core/settings')
|
const { settings } = await import('@/core/settings')
|
||||||
const { loadFeaturesFromCodes, FeatureKind } = await import(
|
const { loadFeatureCode } = await import('@/core/external-input/load-feature-code')
|
||||||
'@/core/external-input/load-features-from-codes'
|
|
||||||
)
|
|
||||||
const loadUserComponent = (component: ComponentMetadata) => {
|
const loadUserComponent = (component: ComponentMetadata) => {
|
||||||
components.push(component)
|
components.push(component)
|
||||||
componentsMap[component.name] = component
|
componentsMap[component.name] = component
|
||||||
}
|
}
|
||||||
const userComponents = await loadFeaturesFromCodes(
|
|
||||||
FeatureKind.Component,
|
for (const [name, setting] of Object.entries(settings.userComponents)) {
|
||||||
Object.keys(settings.userComponents),
|
const { code } = setting
|
||||||
Object.values(settings.userComponents).map(it => it.code),
|
let metadata: ComponentMetadata
|
||||||
)
|
try {
|
||||||
userComponents.forEach(loadUserComponent)
|
metadata = loadFeatureCode(code) as ComponentMetadata
|
||||||
|
} catch (e) {
|
||||||
|
console.error('从代码加载用户组件失败。代码可能有语法错误或代码执行时有抛出值。', {
|
||||||
|
componentName: name,
|
||||||
|
error: e,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
loadUserComponent(metadata)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/** 载入所有组件 */
|
/** 载入所有组件 */
|
||||||
export const loadAllComponents = async () => {
|
export const loadAllComponents = async () => {
|
||||||
|
|||||||
@ -89,6 +89,7 @@ const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
|
|||||||
combineText(getText(modules.module_dynamic, cardType), getText(repostDynamicModule, cardType))
|
combineText(getText(modules.module_dynamic, cardType), getText(repostDynamicModule, cardType))
|
||||||
}
|
}
|
||||||
card.text = await card.getText()
|
card.text = await card.getText()
|
||||||
|
card.element.setAttribute('data-did', card.id)
|
||||||
// 等待第一次 Vue 渲染完成
|
// 等待第一次 Vue 渲染完成
|
||||||
await selectAll(() => element.querySelectorAll('.bili-dyn-item *'), { queryInterval: 50 })
|
await selectAll(() => element.querySelectorAll('.bili-dyn-item *'), { queryInterval: 50 })
|
||||||
return card
|
return card
|
||||||
|
|||||||
@ -13,11 +13,15 @@ export class Translator {
|
|||||||
static map: Map<string, any>
|
static map: Map<string, any>
|
||||||
static regex: [RegExp, string][]
|
static regex: [RegExp, string][]
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected getValue = (node: Node) => node.nodeValue
|
protected getValue = (node: Node) => node.nodeValue
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected setValue = (node: Node, value: string) => {
|
protected setValue = (node: Node, value: string) => {
|
||||||
node.nodeValue = value
|
node.nodeValue = value
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
protected getElement = (node: Node) => node as Element
|
protected getElement = (node: Node) => node as Element
|
||||||
translate(node: Node) {
|
translate(node: Node) {
|
||||||
let value = this.getValue(node)
|
let value = this.getValue(node)
|
||||||
@ -100,17 +104,23 @@ export class Translator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class TextNodeTranslator extends Translator {
|
export class TextNodeTranslator extends Translator {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
|
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getElement = (node: Node) => node.parentElement
|
getElement = (node: Node) => node.parentElement
|
||||||
}
|
}
|
||||||
export class TitleTranslator extends Translator {
|
export class TitleTranslator extends Translator {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getValue = (node: Node) => (node as Element).getAttribute('title')
|
getValue = (node: Node) => (node as Element).getAttribute('title')
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
setValue = (node: Node, value: string) => {
|
setValue = (node: Node, value: string) => {
|
||||||
;(node as Element).setAttribute('title', value)
|
;(node as Element).setAttribute('title', value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class PlaceholderTranslator extends Translator {
|
export class PlaceholderTranslator extends Translator {
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
setValue = (node: Node, value: string) => {
|
setValue = (node: Node, value: string) => {
|
||||||
;(node as Element).setAttribute('placeholder', value)
|
;(node as Element).setAttribute('placeholder', value)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,16 +62,16 @@ export default defineComponent({
|
|||||||
'next-item': null as (e: KeyboardEvent) => true,
|
'next-item': null as (e: KeyboardEvent) => true,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
performAction(event: Event) {
|
performAction(event: KeyboardEvent | MouseEvent) {
|
||||||
this.action.action()
|
this.action.action()
|
||||||
this.$emit('action', event as KeyboardEvent | MouseEvent)
|
this.$emit('action', event)
|
||||||
},
|
},
|
||||||
performDelete(event: Event) {
|
performDelete(event: KeyboardEvent | MouseEvent) {
|
||||||
if (!this.action.deleteAction) {
|
if (!this.action.deleteAction) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.action.deleteAction()
|
this.action.deleteAction()
|
||||||
this.$emit('delete-item', event as KeyboardEvent | MouseEvent)
|
this.$emit('delete-item', event)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -78,18 +78,25 @@ import { historyProvider } from './history-provider'
|
|||||||
import type { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
|
import type { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
|
||||||
import { LaunchBarActionProviders } from './launch-bar-action'
|
import { LaunchBarActionProviders } from './launch-bar-action'
|
||||||
import { search, searchProvider } from './search-provider'
|
import { search, searchProvider } from './search-provider'
|
||||||
|
import { ascendingSort } from '@/core/utils/sort'
|
||||||
|
|
||||||
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
|
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
|
||||||
searchProvider,
|
searchProvider,
|
||||||
historyProvider,
|
historyProvider,
|
||||||
]) as [LaunchBarActionProvider[]]
|
]) as [LaunchBarActionProvider[]]
|
||||||
|
|
||||||
|
interface LaunchBarActionData extends LaunchBarAction {
|
||||||
|
key: string
|
||||||
|
provider: LaunchBarActionProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortActions = (actions: LaunchBarActionData[]): LaunchBarActionData[] => {
|
||||||
|
return [...actions].sort(ascendingSort(it => it.order ?? Infinity))
|
||||||
|
}
|
||||||
const generateKeys = (
|
const generateKeys = (
|
||||||
provider: LaunchBarActionProvider,
|
provider: LaunchBarActionProvider,
|
||||||
actions: LaunchBarAction[],
|
actions: LaunchBarAction[],
|
||||||
): ({
|
): LaunchBarActionData[] =>
|
||||||
key: string
|
|
||||||
provider: LaunchBarActionProvider
|
|
||||||
} & LaunchBarAction)[] =>
|
|
||||||
actions.map(a => {
|
actions.map(a => {
|
||||||
const key = `${provider.name}.${a.name}`
|
const key = `${provider.name}.${a.name}`
|
||||||
return {
|
return {
|
||||||
@ -114,13 +121,15 @@ async function getOnlineActions(this: InstanceType<typeof ThisComponent>) {
|
|||||||
})
|
})
|
||||||
const fuseResult = fuse.search(this.keyword)
|
const fuseResult = fuse.search(this.keyword)
|
||||||
console.log(fuseResult)
|
console.log(fuseResult)
|
||||||
this.actions = fuseResult.map(it => it.item).slice(0, 12)
|
this.actions = sortActions(fuseResult.map(it => it.item).slice(0, 12))
|
||||||
this.noActions = this.actions.length === 0
|
this.noActions = this.actions.length === 0
|
||||||
}
|
}
|
||||||
async function getActions(this: InstanceType<typeof ThisComponent>) {
|
async function getActions(this: InstanceType<typeof ThisComponent>) {
|
||||||
this.noActions = false
|
this.noActions = false
|
||||||
if (this.isHistory) {
|
if (this.isHistory) {
|
||||||
this.actions = generateKeys(historyProvider, await historyProvider.getActions(this.keyword))
|
this.actions = sortActions(
|
||||||
|
generateKeys(historyProvider, await historyProvider.getActions(this.keyword)),
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.actions = []
|
this.actions = []
|
||||||
|
|||||||
@ -1,22 +1,22 @@
|
|||||||
// import { formatDate } from '@/core/utils/formatters'
|
import { crossOriginLocalStorage } from '@/core/local-storage'
|
||||||
import type { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
|
import type { LaunchBarAction, LaunchBarActionProvider } from './launch-bar-action'
|
||||||
|
|
||||||
export interface HistoryItem {
|
export interface HistoryItem {
|
||||||
value: string
|
value: string
|
||||||
isHistory: number
|
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}
|
}
|
||||||
const SearchHistoryKey = 'search_history'
|
const SearchHistoryKey = 'search_history:search_history'
|
||||||
const SearchHistoryMaxItems = 12
|
const SearchHistoryMaxItems = 20
|
||||||
export const getHistoryItems = (key = SearchHistoryKey) => {
|
export const getHistoryItems = async (key = SearchHistoryKey) => {
|
||||||
const historyText = localStorage.getItem(key)
|
const historyText = await crossOriginLocalStorage.getItem(key)
|
||||||
const historyItems: HistoryItem[] = historyText ? JSON.parse(historyText) : []
|
const historyItems: HistoryItem[] = historyText ? JSON.parse(historyText) : []
|
||||||
return historyItems
|
return historyItems
|
||||||
}
|
}
|
||||||
export const clearHistoryItems = (key = SearchHistoryKey) => localStorage.setItem(key, '[]')
|
export const clearHistoryItems = (key = SearchHistoryKey) =>
|
||||||
export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
|
crossOriginLocalStorage.setItem(key, '[]')
|
||||||
|
export const addHistoryItem = async (keyword: string, key = SearchHistoryKey) => {
|
||||||
console.log('add', keyword)
|
console.log('add', keyword)
|
||||||
localStorage.setItem(
|
await crossOriginLocalStorage.setItem(
|
||||||
key,
|
key,
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
lodash
|
lodash
|
||||||
@ -25,10 +25,9 @@ export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
value: keyword,
|
value: keyword,
|
||||||
isHistory: 1,
|
|
||||||
timestamp: Number(new Date()),
|
timestamp: Number(new Date()),
|
||||||
},
|
},
|
||||||
...getHistoryItems(),
|
...(await getHistoryItems()),
|
||||||
],
|
],
|
||||||
h => h.value,
|
h => h.value,
|
||||||
),
|
),
|
||||||
@ -39,13 +38,13 @@ export const addHistoryItem = (keyword: string, key = SearchHistoryKey) => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export const deleteHistoryItem = (keyword: string, key = SearchHistoryKey) => {
|
export const deleteHistoryItem = async (keyword: string, key = SearchHistoryKey) => {
|
||||||
const items = getHistoryItems()
|
const items = await getHistoryItems()
|
||||||
const index = items.findIndex(it => it.value === keyword)
|
const index = items.findIndex(it => it.value === keyword)
|
||||||
console.log('delete', keyword, index)
|
console.log('delete', keyword, index)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
items.splice(index, 1)
|
items.splice(index, 1)
|
||||||
localStorage.setItem(key, JSON.stringify(items))
|
await crossOriginLocalStorage.setItem(key, JSON.stringify(items))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const historyProvider: LaunchBarActionProvider = {
|
export const historyProvider: LaunchBarActionProvider = {
|
||||||
@ -57,11 +56,12 @@ export const historyProvider: LaunchBarActionProvider = {
|
|||||||
icon: 'mdi-trash-can-outline',
|
icon: 'mdi-trash-can-outline',
|
||||||
description: 'Clear History',
|
description: 'Clear History',
|
||||||
explicitSelect: true,
|
explicitSelect: true,
|
||||||
action: () => {
|
action: async () => {
|
||||||
clearHistoryItems()
|
await clearHistoryItems()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const items = getHistoryItems().map(
|
const historyItems = await getHistoryItems()
|
||||||
|
const items = historyItems.map(
|
||||||
it =>
|
it =>
|
||||||
({
|
({
|
||||||
name: it.value,
|
name: it.value,
|
||||||
@ -71,8 +71,8 @@ export const historyProvider: LaunchBarActionProvider = {
|
|||||||
action: () => {
|
action: () => {
|
||||||
search(it.value)
|
search(it.value)
|
||||||
},
|
},
|
||||||
deleteAction: () => {
|
deleteAction: async () => {
|
||||||
deleteHistoryItem(it.value)
|
await deleteHistoryItem(it.value)
|
||||||
},
|
},
|
||||||
} as LaunchBarAction),
|
} as LaunchBarAction),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -21,6 +21,8 @@ export interface LaunchBarAction {
|
|||||||
deleteAction?: Executable
|
deleteAction?: Executable
|
||||||
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
|
/** 显式选中模式: 开启后可以禁止在列表第一项时直接由 Enter 触发 */
|
||||||
explicitSelect?: boolean
|
explicitSelect?: boolean
|
||||||
|
/** 手动指定在搜索结果中的顺序, 数字越小越排前面 */
|
||||||
|
order?: number
|
||||||
}
|
}
|
||||||
/** LaunchBar 动作提供者的插件key, 可注入其他提供者 */
|
/** LaunchBar 动作提供者的插件key, 可注入其他提供者 */
|
||||||
export const LaunchBarActionProviders = 'launchBar.actions'
|
export const LaunchBarActionProviders = 'launchBar.actions'
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const searchProvider: LaunchBarActionProvider = {
|
|||||||
content: defineAsyncComponent(
|
content: defineAsyncComponent(
|
||||||
async () => () =>
|
async () => () =>
|
||||||
h('div', {
|
h('div', {
|
||||||
innerHTML: /* html */ `<em class="suggest-highlight">${input}</em>`,
|
innerHTML: /* html */ `<span>${input}</span>`,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
action: () => search(input),
|
action: () => search(input),
|
||||||
|
|||||||
@ -218,3 +218,6 @@ export interface ComponentMetadata<O extends UnknownOptions = UnknownOptions>
|
|||||||
|
|
||||||
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
|
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
|
||||||
export type UserComponentMetadata = Omit<ComponentMetadata, keyof FunctionalMetadata>
|
export type UserComponentMetadata = Omit<ComponentMetadata, keyof FunctionalMetadata>
|
||||||
|
|
||||||
|
/** 推断 Record 的 Value 类型 */
|
||||||
|
export type RecordValue<R> = R extends Record<any, infer V> ? V : never
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { componentToSettings } from '@/core/settings'
|
|||||||
import { isBuiltInComponent } from './built-in-components'
|
import { isBuiltInComponent } from './built-in-components'
|
||||||
import type { ComponentMetadata } from './component'
|
import type { ComponentMetadata } from './component'
|
||||||
import { componentsMap } from './component'
|
import { componentsMap } from './component'
|
||||||
|
import * as bisector from './bisector/api'
|
||||||
|
import type { BisectorOptions } from './bisector/options'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安装自定义组件
|
* 安装自定义组件
|
||||||
@ -10,10 +12,12 @@ import { componentsMap } from './component'
|
|||||||
*/
|
*/
|
||||||
export const installComponent = async (code: string) => {
|
export const installComponent = async (code: string) => {
|
||||||
const { components } = await import('./component')
|
const { components } = await import('./component')
|
||||||
const { parseExternalInput } = await import('../core/external-input')
|
const { loadFeatureCode } = await import('@/core/external-input')
|
||||||
const component = await parseExternalInput<ComponentMetadata>(code)
|
let component: ComponentMetadata
|
||||||
if (component === null) {
|
try {
|
||||||
throw new Error('无效的组件代码')
|
component = loadFeatureCode(code) as ComponentMetadata
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('无效的组件代码', { cause: e })
|
||||||
}
|
}
|
||||||
const { settings } = await import('@/core/settings')
|
const { settings } = await import('@/core/settings')
|
||||||
if (isBuiltInComponent(component.name)) {
|
if (isBuiltInComponent(component.name)) {
|
||||||
@ -132,3 +136,16 @@ export const toggleComponent = async (nameOrDisplayName: string) => {
|
|||||||
const { displayName } = userComponent.metadata
|
const { displayName } = userComponent.metadata
|
||||||
return `已${enabled ? '开启' : '关闭'}组件'${displayName}', 可能需要刷新后才能生效`
|
return `已${enabled ? '开启' : '关闭'}组件'${displayName}', 可能需要刷新后才能生效`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二等分自定义组件的开关状态
|
||||||
|
*
|
||||||
|
* @param options 二等分选项
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const bisectComponent = async (options?: BisectorOptions) => {
|
||||||
|
if (options) {
|
||||||
|
bisector.setOptions(options)
|
||||||
|
}
|
||||||
|
return bisector
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,8 @@ const parseHexColor = (hexColor: string) => {
|
|||||||
}
|
}
|
||||||
export const convertHexColorForDialogue = (hexColor: string) => {
|
export const convertHexColorForDialogue = (hexColor: string) => {
|
||||||
const { red, green, blue } = parseHexColor(hexColor)
|
const { red, green, blue } = parseHexColor(hexColor)
|
||||||
return `\\c&H${blue}${green}${red}&`.toUpperCase()
|
const hex = `${blue}${green}${red}`.toUpperCase()
|
||||||
|
return `\\c&H${hex}&`
|
||||||
}
|
}
|
||||||
export const convertHexColorForStyle = (hexColor: string, opacity = 1) => {
|
export const convertHexColorForStyle = (hexColor: string, opacity = 1) => {
|
||||||
const { red, green, blue } = parseHexColor(hexColor)
|
const { red, green, blue } = parseHexColor(hexColor)
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { hasVideo } from '@/core/spin-query'
|
|||||||
|
|
||||||
import { playerAgent } from './player-agent'
|
import { playerAgent } from './player-agent'
|
||||||
|
|
||||||
let danmakuContainerObserver: MutationObserver
|
|
||||||
export interface DanmakuRecord {
|
export interface DanmakuRecord {
|
||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
reuse: boolean
|
reuse: boolean
|
||||||
@ -13,17 +12,14 @@ export interface DanmakuRecordCallback {
|
|||||||
added?: (danmaku: DanmakuRecord) => void
|
added?: (danmaku: DanmakuRecord) => void
|
||||||
removed?: (danmaku: DanmakuRecord) => void
|
removed?: (danmaku: DanmakuRecord) => void
|
||||||
}
|
}
|
||||||
const recordedDanmakus: DanmakuRecord[] = []
|
|
||||||
const parseDanmakuRecord = (element: HTMLElement) => ({
|
const parseDanmakuRecord = (element: HTMLElement) => ({
|
||||||
element,
|
element,
|
||||||
reuse: false,
|
reuse: false,
|
||||||
text: element.textContent || '',
|
text: element.textContent || '',
|
||||||
})
|
})
|
||||||
const startRecording = (container: HTMLElement, callback: DanmakuRecordCallback) => {
|
const startRecording = (container: HTMLElement, callback: DanmakuRecordCallback) => {
|
||||||
if (danmakuContainerObserver) {
|
const recordedDanmakus: DanmakuRecord[] = []
|
||||||
danmakuContainerObserver.disconnect()
|
childListSubtree(container, records => {
|
||||||
}
|
|
||||||
;[danmakuContainerObserver] = childListSubtree(container, records => {
|
|
||||||
records.forEach(record => {
|
records.forEach(record => {
|
||||||
record.addedNodes.forEach(node => {
|
record.addedNodes.forEach(node => {
|
||||||
if (node.nodeType === Node.TEXT_NODE) {
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { componentApis } from '@/components/api'
|
|||||||
import * as ajax from '@/core/ajax'
|
import * as ajax from '@/core/ajax'
|
||||||
import * as cdnTypes from '@/core/cdn-types'
|
import * as cdnTypes from '@/core/cdn-types'
|
||||||
import * as download from '@/core/download'
|
import * as download from '@/core/download'
|
||||||
|
import * as dialog from '@/core/dialog'
|
||||||
import * as externalInput from '@/core/external-input'
|
import * as externalInput from '@/core/external-input'
|
||||||
import * as filePicker from '@/core/file-picker'
|
import * as filePicker from '@/core/file-picker'
|
||||||
import * as horizontalScroll from '@/core/horizontal-scroll'
|
import * as horizontalScroll from '@/core/horizontal-scroll'
|
||||||
import * as installFeature from '@/core/install-feature'
|
import * as installFeature from '@/core/install-feature'
|
||||||
import * as lifeCycle from '@/core/life-cycle'
|
import * as lifeCycle from '@/core/life-cycle'
|
||||||
import * as loadingMode from '@/core/loading-mode'
|
import * as loadingMode from '@/core/loading-mode'
|
||||||
|
import * as localStorage from '@/core/local-storage'
|
||||||
import * as meta from '@/core/meta'
|
import * as meta from '@/core/meta'
|
||||||
import * as observer from '@/core/observer'
|
import * as observer from '@/core/observer'
|
||||||
import * as reorder from '@/core/reorder'
|
import * as reorder from '@/core/reorder'
|
||||||
@ -39,12 +41,14 @@ export const coreApis = {
|
|||||||
ajax,
|
ajax,
|
||||||
cdnTypes,
|
cdnTypes,
|
||||||
download,
|
download,
|
||||||
|
dialog,
|
||||||
externalInput,
|
externalInput,
|
||||||
filePicker,
|
filePicker,
|
||||||
installFeature,
|
installFeature,
|
||||||
horizontalScroll,
|
horizontalScroll,
|
||||||
lifeCycle,
|
lifeCycle,
|
||||||
loadingMode,
|
loadingMode,
|
||||||
|
localStorage,
|
||||||
meta,
|
meta,
|
||||||
observer,
|
observer,
|
||||||
reorder,
|
reorder,
|
||||||
@ -78,12 +82,14 @@ export const externalApis = {
|
|||||||
ajax,
|
ajax,
|
||||||
...cdnTypes,
|
...cdnTypes,
|
||||||
...download,
|
...download,
|
||||||
|
...dialog,
|
||||||
...externalInput,
|
...externalInput,
|
||||||
...filePicker,
|
...filePicker,
|
||||||
...installFeature,
|
...installFeature,
|
||||||
...horizontalScroll,
|
...horizontalScroll,
|
||||||
lifeCycle,
|
lifeCycle,
|
||||||
...loadingMode,
|
...loadingMode,
|
||||||
|
...localStorage,
|
||||||
...meta,
|
...meta,
|
||||||
observer,
|
observer,
|
||||||
...reorder,
|
...reorder,
|
||||||
|
|||||||
@ -12,10 +12,10 @@ export interface DialogInputs {
|
|||||||
}
|
}
|
||||||
export interface DialogInstance extends Required<DialogInputs> {
|
export interface DialogInstance extends Required<DialogInputs> {
|
||||||
open: boolean
|
open: boolean
|
||||||
close: Promise<void>
|
close(): Promise<void>
|
||||||
closeListeners: (() => void)[]
|
closeListeners: (() => void)[]
|
||||||
}
|
}
|
||||||
export const showDialog = (inputs: DialogInputs) => {
|
export const showDialog = (inputs: DialogInputs): DialogInstance => {
|
||||||
const { icon, title, zIndex, content, contentProps } = inputs
|
const { icon, title, zIndex, content, contentProps } = inputs
|
||||||
let dialogEl: HTMLDivElement | undefined
|
let dialogEl: HTMLDivElement | undefined
|
||||||
let dialogApp: App<HTMLDivElement> | undefined
|
let dialogApp: App<HTMLDivElement> | undefined
|
||||||
@ -46,5 +46,5 @@ export const showDialog = (inputs: DialogInputs) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
vm.open = true
|
vm.open = true
|
||||||
})
|
})
|
||||||
return vm
|
return vm as DialogInstance
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { pickFile } from '@/core/file-picker'
|
import { pickFile } from '@/core/file-picker'
|
||||||
|
|
||||||
|
export * from './load-feature-code'
|
||||||
|
|
||||||
/** 外部输入包装类型, 详见`parseExternalInput`的文档 */
|
/** 外部输入包装类型, 详见`parseExternalInput`的文档 */
|
||||||
export type ExternalInput<T> = undefined | string | T
|
export type ExternalInput<T> = undefined | string | T
|
||||||
// const arrayReturn = <InputType extends any[], OutputType>(
|
// const arrayReturn = <InputType extends any[], OutputType>(
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import type { LoadFeatureCodeResult } from '@/core/external-input/load-feature-code'
|
|
||||||
import { loadFeatureCode } from '@/core/external-input/load-feature-code'
|
|
||||||
|
|
||||||
type LdRes<X> = LoadFeatureCodeResult<X>
|
|
||||||
type SettledRes<T> = PromiseSettledResult<T>
|
|
||||||
type FilledRes<T> = PromiseFulfilledResult<T>
|
|
||||||
|
|
||||||
const unwrapSettledResult = <T>(r: SettledRes<T>): T => (r as FilledRes<T>).value
|
|
||||||
|
|
||||||
const mapSettledArray = <T>(arr: SettledRes<T>[]): T[] => arr.map(unwrapSettledResult)
|
|
||||||
|
|
||||||
const mapSettleResult = <T>(p: Promise<SettledRes<T>[]>): Promise<T[]> => p.then(mapSettledArray)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量加载组件或插件的代码字符串,获取其导出 feature
|
|
||||||
*
|
|
||||||
* @param codes 代码字符串数组
|
|
||||||
* @returns 不会失败的 `Promise`。其结果为一个数组,其中每个元素都是代表代码执行结果的对象
|
|
||||||
*/
|
|
||||||
export const loadFeatureCodeAllSettled = <X>(
|
|
||||||
codes: string[],
|
|
||||||
): Promise<LoadFeatureCodeResult<X>[]> =>
|
|
||||||
lodash(codes)
|
|
||||||
.map<Promise<LdRes<X>>>(loadFeatureCode)
|
|
||||||
.thru<Promise<SettledRes<LdRes<X>>[]>>(arr => Promise.allSettled(arr))
|
|
||||||
.thru(mapSettleResult)
|
|
||||||
.value()
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
import type { FeatureBase } from '@/components/types'
|
|
||||||
import type {
|
|
||||||
LoadFeatureCodeResult,
|
|
||||||
LoadFeatureCodeResultError,
|
|
||||||
LoadFeatureCodeResultOk,
|
|
||||||
} from '@/core/external-input/load-feature-code'
|
|
||||||
import { loadFeatureCode } from '@/core/external-input/load-feature-code'
|
|
||||||
|
|
||||||
interface ResultInstance {
|
|
||||||
readonly isOk: <X extends FeatureBase>(
|
|
||||||
this: LoadFeatureCodeAllResult<X>,
|
|
||||||
) => this is LoadFeatureCodeAllResultOk<X>
|
|
||||||
|
|
||||||
readonly isError: (
|
|
||||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
|
||||||
) => this is LoadFeatureCodeAllResultError
|
|
||||||
|
|
||||||
readonly isNoExport: (
|
|
||||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
|
||||||
) => this is LoadFeatureCodeAllResultNoExport
|
|
||||||
|
|
||||||
readonly isCodeThrew: (
|
|
||||||
this: LoadFeatureCodeAllResult<FeatureBase>,
|
|
||||||
) => this is LoadFeatureCodeAllResultCodeThrew
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 成功从代码中获取 features
|
|
||||||
*
|
|
||||||
* @namespace
|
|
||||||
* @property features 从代码中获取的导出值
|
|
||||||
*/
|
|
||||||
interface LoadFeatureCodeAllResultOk<X extends FeatureBase> extends ResultInstance {
|
|
||||||
readonly tag: 'Ok'
|
|
||||||
readonly features: X[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 代码没有导出任何值 */
|
|
||||||
interface LoadFeatureCodeAllResultNoExport extends ResultInstance {
|
|
||||||
readonly tag: 'NoExport'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行代码过程中产生了抛出值。
|
|
||||||
*
|
|
||||||
* @namespace
|
|
||||||
* @property thrown 抛出的值
|
|
||||||
*/
|
|
||||||
interface LoadFeatureCodeAllResultCodeThrew extends ResultInstance {
|
|
||||||
readonly tag: 'CodeThrew'
|
|
||||||
readonly thrown: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoadFeatureCodeAllResultError =
|
|
||||||
| LoadFeatureCodeAllResultNoExport
|
|
||||||
| LoadFeatureCodeAllResultCodeThrew
|
|
||||||
type LoadFeatureCodeAllResult<X extends FeatureBase> =
|
|
||||||
| LoadFeatureCodeAllResultOk<X>
|
|
||||||
| LoadFeatureCodeAllResultError
|
|
||||||
|
|
||||||
const resultProto: ResultInstance = {
|
|
||||||
isOk() {
|
|
||||||
return this.tag === 'Ok'
|
|
||||||
},
|
|
||||||
isError() {
|
|
||||||
return this.tag !== 'Ok'
|
|
||||||
},
|
|
||||||
isNoExport() {
|
|
||||||
return this.tag === 'NoExport'
|
|
||||||
},
|
|
||||||
isCodeThrew() {
|
|
||||||
return this.tag === 'CodeThrew'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const okResult = <X extends FeatureBase>(features: X[]): LoadFeatureCodeAllResultOk<X> =>
|
|
||||||
lodash.create(resultProto, {
|
|
||||||
tag: 'Ok' as const,
|
|
||||||
features,
|
|
||||||
})
|
|
||||||
|
|
||||||
const noExportResult = lodash.create(resultProto, {
|
|
||||||
tag: 'NoExport' as const,
|
|
||||||
})
|
|
||||||
|
|
||||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeAllResultCodeThrew =>
|
|
||||||
lodash.create(resultProto, {
|
|
||||||
tag: 'CodeThrew' as const,
|
|
||||||
thrown,
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
type Task<Ok, Err = never> = Promise<Ok>
|
|
||||||
|
|
||||||
type LdRes<X> = LoadFeatureCodeResult<X>
|
|
||||||
type LdOk<X> = LoadFeatureCodeResultOk<X>
|
|
||||||
type LdErr = LoadFeatureCodeResultError
|
|
||||||
|
|
||||||
type LdAllRes<X> = LoadFeatureCodeAllResult<X>
|
|
||||||
type LdAllOk<X> = LoadFeatureCodeAllResultOk<X>
|
|
||||||
type LdAllErr = LoadFeatureCodeAllResultError
|
|
||||||
|
|
||||||
type LoadCodesTask<X> = Task<LdOk<X>[], [number, LdErr]>
|
|
||||||
|
|
||||||
// covert `Task<LdRes<X>>` to `Task<LdOk<X>, LdErr>`
|
|
||||||
const rejectErrorResult = <X>(t: Task<LdRes<X>>): Task<LdOk<X>, LdErr> =>
|
|
||||||
t.then(r => (r.isOk() ? r : Promise.reject(r)))
|
|
||||||
|
|
||||||
// load feature code, and return `Task<LdOk<X>, LdErr>`
|
|
||||||
const loadCode = <X>(code: string): Task<LdOk<X>, LdErr> => rejectErrorResult(loadFeatureCode(code))
|
|
||||||
|
|
||||||
// covert `Task`'s `Err` type from `T` to `[number, T]`
|
|
||||||
const addIndexToRejected = <N extends number, O, E>(
|
|
||||||
t: Task<O, E>,
|
|
||||||
i: N,
|
|
||||||
// eslint-disable-next-line prefer-promise-reject-errors
|
|
||||||
): Task<O, [N, E]> => t.catch(e => Promise.reject([i, e]))
|
|
||||||
|
|
||||||
// create `LdAllOk` from an array of `LdOk`
|
|
||||||
const createOkResult = <X>(arr: LdOk<X>[]): LdAllOk<X> => okResult(arr.map(r => r.feature))
|
|
||||||
|
|
||||||
// create `LdAllErr` from `[number, LdErr]`
|
|
||||||
const createErrResult = (t: [number, LdErr]): LdAllErr =>
|
|
||||||
t[1].isCodeThrew() ? codeThrewResult(t[1].thrown) : noExportResult
|
|
||||||
|
|
||||||
// load all feature codes, and return `LoadCodesTask`
|
|
||||||
const loadCodes = <X>(codes: string[]): LoadCodesTask<X> =>
|
|
||||||
lodash(codes)
|
|
||||||
.map<Task<LdOk<X>, LdErr>>(loadCode)
|
|
||||||
.map(addIndexToRejected)
|
|
||||||
.thru<LoadCodesTask<X>>(arr => Promise.all(arr))
|
|
||||||
.value()
|
|
||||||
|
|
||||||
// create a `LdAllRes` wrapped by `Task`
|
|
||||||
const createTaskResult = <X>(t: LoadCodesTask<X>): Task<LdAllRes<X>> =>
|
|
||||||
t.then(createOkResult).catch(createErrResult)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量加载组件或插件的代码字符串,获取其导出 feature
|
|
||||||
*
|
|
||||||
* 只要有一个代码出现了错误,则返回错误。
|
|
||||||
*
|
|
||||||
* @param codes 代码字符串数组
|
|
||||||
* @returns 一个不会失败的 `Promise`,其结果值为 `LoadFeatureCodeAllResult`
|
|
||||||
*/
|
|
||||||
const loadFeatureCodeAll = <X>(codes: string[]): Promise<LoadFeatureCodeAllResult<X>> =>
|
|
||||||
lodash(codes).thru<LoadCodesTask<X>>(loadCodes).thru(createTaskResult).value()
|
|
||||||
|
|
||||||
export {
|
|
||||||
loadFeatureCodeAll,
|
|
||||||
type LoadFeatureCodeAllResult,
|
|
||||||
type LoadFeatureCodeAllResultCodeThrew,
|
|
||||||
type LoadFeatureCodeAllResultError,
|
|
||||||
type LoadFeatureCodeAllResultNoExport,
|
|
||||||
type LoadFeatureCodeAllResultOk,
|
|
||||||
}
|
|
||||||
@ -1,116 +1,87 @@
|
|||||||
import type { FeatureBase } from '@/components/types'
|
export class LoadFeatureCodeError extends Error {}
|
||||||
|
|
||||||
interface ResultInstance {
|
/**
|
||||||
readonly isOk: <X extends FeatureBase>(
|
* feature 代码运行沙箱
|
||||||
this: LoadFeatureCodeResult<X>,
|
*/
|
||||||
) => this is LoadFeatureCodeResultOk<X>
|
type CodeSandbox = {
|
||||||
|
/**
|
||||||
readonly isError: (this: LoadFeatureCodeResult<FeatureBase>) => this is LoadFeatureCodeResultError
|
* 在沙箱中执行代码
|
||||||
|
*
|
||||||
readonly isNoExport: (
|
* @remarks
|
||||||
this: LoadFeatureCodeResult<FeatureBase>,
|
* 代码执行时的相关注意事项见 {@link loadFeatureCode}
|
||||||
) => this is LoadFeatureCodeResultNoExport
|
*
|
||||||
|
* @returns 一个二元组:`[导出值, 返回值]`
|
||||||
readonly isCodeThrew: (
|
* @throws {@link LoadFeatureCodeError}
|
||||||
this: LoadFeatureCodeResult<FeatureBase>,
|
* 代码包含语法错误或代码执行时产生了异常
|
||||||
) => this is LoadFeatureCodeResultCodeThrew
|
*/
|
||||||
|
run(code: string): [unknown, unknown]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 成功从代码中获取 feature
|
* 创建 feature 代码的运行沙箱
|
||||||
*
|
*
|
||||||
* @namespace
|
* @returns 一个 `CodeSandbox`。
|
||||||
* @property feature 从代码中获取的导出值
|
|
||||||
*/
|
*/
|
||||||
interface LoadFeatureCodeResultOk<X extends FeatureBase> extends ResultInstance {
|
const createCodeSandbox = (): CodeSandbox => {
|
||||||
readonly tag: 'Ok'
|
// 需要被注入到 `sandbox` 中的键值对
|
||||||
readonly feature: X
|
const injection = new Map([
|
||||||
}
|
// 加固,防止逃逸
|
||||||
|
[Symbol.unscopables, undefined],
|
||||||
/** 代码没有导出任何值 */
|
['unsafeWindow', unsafeWindow],
|
||||||
interface LoadFeatureCodeResultNoExport extends ResultInstance {
|
] as [keyof any, unknown][])
|
||||||
readonly tag: 'NoExport'
|
// 目标代码执行时的全局对象代理
|
||||||
}
|
const sandbox = new Proxy(Object.create(null), {
|
||||||
|
has: () => true,
|
||||||
/**
|
get: (_, p) => (injection.has(p) ? injection.get(p) : window[p as string]),
|
||||||
* 执行代码过程中产生了抛出值。
|
set: (_, p, v) => !injection.has(p) && (window[p as string] = v),
|
||||||
*
|
|
||||||
* @namespace
|
|
||||||
* @property thrown 抛出的值
|
|
||||||
*/
|
|
||||||
interface LoadFeatureCodeResultCodeThrew extends ResultInstance {
|
|
||||||
readonly tag: 'CodeThrew'
|
|
||||||
readonly thrown: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoadFeatureCodeResultError = LoadFeatureCodeResultNoExport | LoadFeatureCodeResultCodeThrew
|
|
||||||
type LoadFeatureCodeResult<X extends FeatureBase> =
|
|
||||||
| LoadFeatureCodeResultOk<X>
|
|
||||||
| LoadFeatureCodeResultError
|
|
||||||
|
|
||||||
const resultProto: ResultInstance = {
|
|
||||||
isOk() {
|
|
||||||
return this.tag === 'Ok'
|
|
||||||
},
|
|
||||||
isError() {
|
|
||||||
return this.tag !== 'Ok'
|
|
||||||
},
|
|
||||||
isNoExport() {
|
|
||||||
return this.tag === 'NoExport'
|
|
||||||
},
|
|
||||||
isCodeThrew() {
|
|
||||||
return this.tag === 'CodeThrew'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const okResult = <X extends FeatureBase>(feature: X): LoadFeatureCodeResultOk<X> =>
|
|
||||||
lodash.create(resultProto, {
|
|
||||||
tag: 'Ok' as const,
|
|
||||||
feature,
|
|
||||||
})
|
})
|
||||||
|
const codeKey = 'BILIBILI_EVOLVED_LOAD_FEATURE_CODE_CODE_KEY_3B63D912__'
|
||||||
const noExportResult = lodash.create(resultProto, {
|
// eslint-disable-next-line no-new-func
|
||||||
tag: 'NoExport' as const,
|
const fn = Function(
|
||||||
})
|
'window',
|
||||||
|
`with (window) {
|
||||||
const codeThrewResult = (thrown: unknown): LoadFeatureCodeResultCodeThrew =>
|
return eval(${codeKey})
|
||||||
lodash.create(resultProto, {
|
}`,
|
||||||
tag: 'CodeThrew' as const,
|
).bind(undefined, sandbox)
|
||||||
thrown,
|
return {
|
||||||
})
|
run(code) {
|
||||||
|
injection.set('exports', {})
|
||||||
/**
|
injection.set(codeKey, code)
|
||||||
* 加载组件或插件的代码字符串,获取其导出 feature
|
let returned
|
||||||
*
|
|
||||||
* @param code 代码字符串
|
|
||||||
* @returns 一个不会失败的 `Promise`,其结果值为 {@link LoadFeatureCodeResult}
|
|
||||||
*/
|
|
||||||
const loadFeatureCode = async <X extends FeatureBase>(
|
|
||||||
code: string,
|
|
||||||
): Promise<LoadFeatureCodeResult<X>> => {
|
|
||||||
// 收集代码导出值
|
|
||||||
const exports = {}
|
|
||||||
let result: X
|
|
||||||
try {
|
try {
|
||||||
result = eval(code)
|
returned = fn()
|
||||||
} catch (thrown) {
|
} catch (e) {
|
||||||
return codeThrewResult(thrown)
|
throw new LoadFeatureCodeError(undefined, { cause: e })
|
||||||
}
|
}
|
||||||
const values = Object.values(exports)
|
const exportsValues = Object.values(injection.get('exports'))
|
||||||
if (values.length === 0) {
|
const exported = exportsValues.length > 0 ? exportsValues[0] : undefined
|
||||||
if (typeof result === 'object') {
|
return [exported, returned]
|
||||||
return okResult(result)
|
},
|
||||||
}
|
}
|
||||||
return noExportResult
|
|
||||||
}
|
|
||||||
return okResult(values[0] as X)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
let staticCodeSandbox: CodeSandbox | undefined
|
||||||
loadFeatureCode,
|
/**
|
||||||
type LoadFeatureCodeResult,
|
* 执行 feature (component, plugin, style) 的代码,并尝试获取其导出元数据
|
||||||
type LoadFeatureCodeResultCodeThrew,
|
*
|
||||||
type LoadFeatureCodeResultError,
|
* @remarks
|
||||||
type LoadFeatureCodeResultNoExport,
|
* feature 代码支持两种导出方式:
|
||||||
type LoadFeatureCodeResultOk,
|
* 1. 以 UMD 方式打包的库
|
||||||
|
* 2. 若代码整体为一个表达式,则导出表达式的返回值
|
||||||
|
*
|
||||||
|
* 该函数线程不安全
|
||||||
|
*
|
||||||
|
* 代码默认以非严格模式执行,启用需自行添加 `use strict`。(从本项目中打包的 feature 自带严格模式)
|
||||||
|
*
|
||||||
|
* 代码执行时的全局对象为脚本管理器提供的 `window`。代码中支持访问 `unsafeWindow`。
|
||||||
|
*
|
||||||
|
* @param code - 被执行的代码
|
||||||
|
* @returns 导出的元数据(不检测是否为正确的 feature)
|
||||||
|
* @throws {@link LoadFeatureCodeError}
|
||||||
|
* 代码包含语法错误或代码执行时产生了异常
|
||||||
|
*/
|
||||||
|
export const loadFeatureCode = (code: string): unknown => {
|
||||||
|
staticCodeSandbox || (staticCodeSandbox = createCodeSandbox())
|
||||||
|
const [exported, returned] = staticCodeSandbox.run(code)
|
||||||
|
return exported || returned
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
import type { ComponentMetadata } from '@/components/types'
|
|
||||||
import type {
|
|
||||||
LoadFeatureCodeResultError,
|
|
||||||
LoadFeatureCodeResultOk,
|
|
||||||
} from '@/core/external-input/load-feature-code'
|
|
||||||
import { loadFeatureCodeAllSettled } from '@/core/external-input/load-feature-code-all-settled'
|
|
||||||
import { useScopedConsole } from '@/core/utils/log'
|
|
||||||
import type { PluginMetadata } from '@/plugins/plugin'
|
|
||||||
|
|
||||||
const curConsole = useScopedConsole('@/core/external-input/load-features-from-codes.ts')
|
|
||||||
|
|
||||||
export enum FeatureKind {
|
|
||||||
Component = 'Component',
|
|
||||||
Plugin = 'Plugin',
|
|
||||||
}
|
|
||||||
|
|
||||||
const logError = (
|
|
||||||
kind: FeatureKind,
|
|
||||||
): ((featureName: string, err: LoadFeatureCodeResultError) => void) => {
|
|
||||||
const prefix = kind === FeatureKind.Component ? 'component' : 'plugin'
|
|
||||||
return (featureName, err) => {
|
|
||||||
if (err.isNoExport()) {
|
|
||||||
curConsole.error(`${prefix} '${featureName}' exports no value, failed to load`)
|
|
||||||
} else {
|
|
||||||
curConsole.error(
|
|
||||||
`${prefix} '${featureName}' throws something when importing, failed to load`,
|
|
||||||
{ thrown: err.thrown },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportErrToUser = (featureKind: FeatureKind, errNames: string[]): void => {
|
|
||||||
type ErrInfo = number | string[]
|
|
||||||
|
|
||||||
const emptyErrInfo: () => string[] = () => []
|
|
||||||
|
|
||||||
const accErrInfo = (acc: ErrInfo, featureName: string): ErrInfo => {
|
|
||||||
if (Array.isArray(acc)) {
|
|
||||||
if (acc.length < 3) {
|
|
||||||
acc.push(featureName)
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
return acc + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportErrInfo = async (kind: FeatureKind, info: ErrInfo) => {
|
|
||||||
const { Toast } = await import('../toast')
|
|
||||||
const kindName = kind === FeatureKind.Component ? '组件' : '插件'
|
|
||||||
if (Array.isArray(info)) {
|
|
||||||
Toast.error(
|
|
||||||
`${kindName} "${info.join('", "')}" 加载失败。请向我们反馈,以解决此问题。`,
|
|
||||||
`${kindName}加载失败`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Toast.error(
|
|
||||||
`有 ${info} 个${kindName}加载失败,请向我们反馈,以解决此问题。`,
|
|
||||||
`${kindName}加载失败`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const errInfo = errNames.reduce(accErrInfo, emptyErrInfo())
|
|
||||||
reportErrInfo(featureKind, errInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FeatureMetadata = ComponentMetadata | PluginMetadata
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量加载组件或插件代码
|
|
||||||
*
|
|
||||||
* 如果遇到错误会向 console 和用户输出错误信息
|
|
||||||
*
|
|
||||||
* `names` 和 `codes` 应该是一一对应的
|
|
||||||
*
|
|
||||||
* @param kind 组件或插件类型
|
|
||||||
* @param names 组件或插件名称
|
|
||||||
* @param codes 组件或插件代码
|
|
||||||
* @return 返回加载成功的组件或插件
|
|
||||||
*/
|
|
||||||
export async function loadFeaturesFromCodes(
|
|
||||||
kind: FeatureKind.Component,
|
|
||||||
names: string[],
|
|
||||||
codes: string[],
|
|
||||||
): Promise<ComponentMetadata[]>
|
|
||||||
export async function loadFeaturesFromCodes(
|
|
||||||
kind: FeatureKind.Plugin,
|
|
||||||
names: string[],
|
|
||||||
codes: string[],
|
|
||||||
): Promise<PluginMetadata[]>
|
|
||||||
export async function loadFeaturesFromCodes(
|
|
||||||
kind: FeatureKind,
|
|
||||||
names: string[],
|
|
||||||
codes: string[],
|
|
||||||
): Promise<FeatureMetadata[]> {
|
|
||||||
const results = await loadFeatureCodeAllSettled<FeatureMetadata>(codes)
|
|
||||||
const [namedOk, namedErr] = lodash(results)
|
|
||||||
.map((r, i) => [names[i], r] as const)
|
|
||||||
.partition(([, r]) => r.isOk())
|
|
||||||
.value()
|
|
||||||
|
|
||||||
// 输出日志
|
|
||||||
lodash.forEach(namedErr, lodash.spread(logError(kind)))
|
|
||||||
|
|
||||||
// 向用户输出错误报告
|
|
||||||
if (namedErr.length > 0) {
|
|
||||||
const errNames = namedErr.map(([name]) => name)
|
|
||||||
reportErrToUser(kind, errNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lodash.map(namedOk, ([, r]) => (r as LoadFeatureCodeResultOk<FeatureMetadata>).feature)
|
|
||||||
}
|
|
||||||
@ -38,8 +38,8 @@ export const installFeatureFromCode = async (
|
|||||||
metadata: FeatureType
|
metadata: FeatureType
|
||||||
message: string
|
message: string
|
||||||
}> => {
|
}> => {
|
||||||
const { parseExternalInput } = await import('../core/external-input')
|
const { loadFeatureCode } = await import('../core/external-input')
|
||||||
const item = await parseExternalInput<FeatureType>(code)
|
const item = loadFeatureCode(code) as FeatureType
|
||||||
const { type, installer } = (() => {
|
const { type, installer } = (() => {
|
||||||
if (isComponent(item)) {
|
if (isComponent(item)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
134
src/core/local-storage.ts
Normal file
134
src/core/local-storage.ts
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { select } from './spin-query'
|
||||||
|
import { deleteValue, getRandomId } from './utils'
|
||||||
|
|
||||||
|
interface CrossOriginMessage {
|
||||||
|
type: string
|
||||||
|
id?: string
|
||||||
|
key?: string
|
||||||
|
value?: unknown
|
||||||
|
}
|
||||||
|
interface CrossOriginMessageListener {
|
||||||
|
id: string
|
||||||
|
resolve: (result: unknown) => void
|
||||||
|
}
|
||||||
|
interface CrossOriginStorageListener {
|
||||||
|
id: string
|
||||||
|
callback: (newValue: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const postMessage = async (message: CrossOriginMessage) => {
|
||||||
|
const iframe = (await select(
|
||||||
|
'iframe[src^="https://s1.hdslb.com/bfs/seed/jinkela/short/cols/iframe.html"][data-loaded="true"]',
|
||||||
|
)) as HTMLIFrameElement
|
||||||
|
if (!iframe) {
|
||||||
|
throw new Error('COLS iframe not found')
|
||||||
|
}
|
||||||
|
iframe.contentWindow.postMessage(message, 'https://s1.hdslb.com')
|
||||||
|
}
|
||||||
|
const messageListeners: CrossOriginMessageListener[] = []
|
||||||
|
const storageListeners: CrossOriginStorageListener[] = []
|
||||||
|
window.addEventListener('message', e => {
|
||||||
|
if (e.data.type === 'COLS_RES') {
|
||||||
|
const data = e.data as CrossOriginMessage
|
||||||
|
const { id } = data
|
||||||
|
const handleMessageListeners = () => {
|
||||||
|
const listenerIndex = messageListeners.findIndex(it => it.id === id)
|
||||||
|
if (listenerIndex < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const [listener] = messageListeners.splice(listenerIndex, 1)
|
||||||
|
listener.resolve(data.value)
|
||||||
|
}
|
||||||
|
const handleStorageListeners = () => {
|
||||||
|
const index = storageListeners.findIndex(it => it.id === id)
|
||||||
|
if (index < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storageListeners[index].callback(data.value as string)
|
||||||
|
}
|
||||||
|
handleMessageListeners()
|
||||||
|
handleStorageListeners()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapMessageApi = <ReturnType = void>(handler: (messageId: string) => void) => {
|
||||||
|
return () =>
|
||||||
|
new Promise<ReturnType>(resolve => {
|
||||||
|
const id = getRandomId(32)
|
||||||
|
messageListeners.push({
|
||||||
|
id,
|
||||||
|
resolve,
|
||||||
|
})
|
||||||
|
handler(id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/** 跨域 Local Storage (由 b 站官方提供) */
|
||||||
|
export const crossOriginLocalStorage = {
|
||||||
|
setItem: (key: string, item: unknown) => {
|
||||||
|
return wrapMessageApi(id => {
|
||||||
|
postMessage({
|
||||||
|
id,
|
||||||
|
type: 'COLS_SET',
|
||||||
|
key,
|
||||||
|
value: item,
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
getItem: <ReturnType = string>(key: string) => {
|
||||||
|
return wrapMessageApi<ReturnType>(id => {
|
||||||
|
postMessage({
|
||||||
|
id,
|
||||||
|
type: 'COLS_GET',
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
removeItem: (key: string) => {
|
||||||
|
return wrapMessageApi(id => {
|
||||||
|
postMessage({
|
||||||
|
id,
|
||||||
|
type: 'COLS_RM',
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
keys: (prefix: string) => {
|
||||||
|
return wrapMessageApi<string[]>(id => {
|
||||||
|
postMessage({
|
||||||
|
id,
|
||||||
|
type: 'COLS_KEYS',
|
||||||
|
key: prefix,
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
clear: (prefix: string) => {
|
||||||
|
return wrapMessageApi(id => {
|
||||||
|
postMessage({
|
||||||
|
id,
|
||||||
|
type: 'COLS_CLR',
|
||||||
|
key: prefix,
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
},
|
||||||
|
addStorageListener: async (key: string, callback: CrossOriginStorageListener['callback']) => {
|
||||||
|
const callbackId = getRandomId(32)
|
||||||
|
storageListeners.push({
|
||||||
|
id: callbackId,
|
||||||
|
callback,
|
||||||
|
})
|
||||||
|
postMessage({
|
||||||
|
type: 'COLS_LIS',
|
||||||
|
key,
|
||||||
|
id: callbackId,
|
||||||
|
})
|
||||||
|
return callbackId
|
||||||
|
},
|
||||||
|
removeStorageListener: async (key: string, callbackId: string) => {
|
||||||
|
deleteValue(storageListeners, it => it.id === callbackId)
|
||||||
|
postMessage({
|
||||||
|
type: 'COLS_LIS_UN',
|
||||||
|
key,
|
||||||
|
id: callbackId,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -29,7 +29,7 @@ export class RuntimeLibrary<LibraryType> implements PromiseLike<LibraryType> {
|
|||||||
;(function runEval() {
|
;(function runEval() {
|
||||||
return eval(code)
|
return eval(code)
|
||||||
// eslint-disable-next-line no-extra-bind
|
// eslint-disable-next-line no-extra-bind
|
||||||
}.bind(window)())
|
}).bind(window)()
|
||||||
return getModule(window)
|
return getModule(window)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,9 +134,8 @@ export const preloadStyles = lodash.once(async () => {
|
|||||||
const { initColors } = await import('./theme-color')
|
const { initColors } = await import('./theme-color')
|
||||||
const { initMdiStyle } = await import('@/ui/mdi')
|
const { initMdiStyle } = await import('@/ui/mdi')
|
||||||
document.head.appendChild(initColors())
|
document.head.appendChild(initColors())
|
||||||
const [mdi, webFont] = initMdiStyle()
|
const mdi = initMdiStyle()
|
||||||
document.head.appendChild(mdi)
|
document.head.appendChild(mdi)
|
||||||
document.head.appendChild(webFont)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -53,8 +53,7 @@ export default defineComponent({
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const appendTarget = containerMap[this.container]
|
const appendTarget = containerMap[this.container]
|
||||||
this.toast = createMiniToast(this.message, this.content, {
|
this.toast = createMiniToast(this.toastRef, this.content, {
|
||||||
content: this.toastRef,
|
|
||||||
placement: this.placement,
|
placement: this.placement,
|
||||||
showOnCreate: this.show,
|
showOnCreate: this.show,
|
||||||
trigger: 'mouseenter focusin',
|
trigger: 'mouseenter focusin',
|
||||||
|
|||||||
@ -449,6 +449,7 @@ export class DoubleClickEvent {
|
|||||||
singleClickHandler: (e: MouseEvent) => void = none
|
singleClickHandler: (e: MouseEvent) => void = none
|
||||||
|
|
||||||
private clickedOnce = false
|
private clickedOnce = false
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
private readonly stopPropagationHandler = (e: MouseEvent) => {
|
private readonly stopPropagationHandler = (e: MouseEvent) => {
|
||||||
e.stopImmediatePropagation()
|
e.stopImmediatePropagation()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FeatureBase } from '@/components/types'
|
import type { FeatureBase } from '@/components/types'
|
||||||
|
|
||||||
import { parseExternalInput } from './external-input'
|
import { loadFeatureCode } from './external-input'
|
||||||
import { meta } from './meta'
|
import { meta } from './meta'
|
||||||
|
|
||||||
export enum CompareResult {
|
export enum CompareResult {
|
||||||
@ -52,7 +52,7 @@ export class Version {
|
|||||||
export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
|
export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
|
||||||
try {
|
try {
|
||||||
if (typeof feature === 'string') {
|
if (typeof feature === 'string') {
|
||||||
feature = await parseExternalInput<FeatureBase>(feature)
|
feature = loadFeatureCode(feature) as FeatureBase
|
||||||
}
|
}
|
||||||
// 无效代码
|
// 无效代码
|
||||||
if (feature === null || feature === undefined) {
|
if (feature === null || feature === undefined) {
|
||||||
|
|||||||
@ -87,6 +87,7 @@ export const plugin: PluginMetadata = {
|
|||||||
action: () => {
|
action: () => {
|
||||||
window.open(it.link(match), '_blank')
|
window.open(it.link(match), '_blank')
|
||||||
},
|
},
|
||||||
|
order: 0,
|
||||||
})
|
})
|
||||||
if (it.extend) {
|
if (it.extend) {
|
||||||
const actions = await it.extend(match)
|
const actions = await it.extend(match)
|
||||||
|
|||||||
@ -58,10 +58,12 @@ export const plugins: PluginMetadata[] = getBuiltInPlugins()
|
|||||||
* @param code 插件代码
|
* @param code 插件代码
|
||||||
*/
|
*/
|
||||||
export const installPlugin = async (code: string) => {
|
export const installPlugin = async (code: string) => {
|
||||||
const { parseExternalInput } = await import('../core/external-input')
|
const { loadFeatureCode } = await import('@/core/external-input')
|
||||||
const plugin = await parseExternalInput<PluginMetadata>(code)
|
let plugin: PluginMetadata
|
||||||
if (plugin === null) {
|
try {
|
||||||
throw new Error('无效的插件代码')
|
plugin = loadFeatureCode(code) as PluginMetadata
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('无效的插件代码', e)
|
||||||
}
|
}
|
||||||
const { settings } = await import('@/core/settings')
|
const { settings } = await import('@/core/settings')
|
||||||
const existingPlugin = settings.userPlugins[plugin.name]
|
const existingPlugin = settings.userPlugins[plugin.name]
|
||||||
@ -159,22 +161,27 @@ export const loadPlugin = async (plugin: PluginMetadata) => {
|
|||||||
*/
|
*/
|
||||||
export const loadAllPlugins = async (components: ComponentMetadata[]) => {
|
export const loadAllPlugins = async (components: ComponentMetadata[]) => {
|
||||||
const { settings, getGeneralSettings } = await import('@/core/settings')
|
const { settings, getGeneralSettings } = await import('@/core/settings')
|
||||||
const { loadFeaturesFromCodes, FeatureKind } = await import(
|
const { loadFeatureCode } = await import('@/core/external-input/load-feature-code')
|
||||||
'@/core/external-input/load-features-from-codes'
|
for (const component of components) {
|
||||||
)
|
const plugin = extractPluginFromComponent(component)
|
||||||
const otherPlugins = lodash(components)
|
if (plugin) {
|
||||||
.map(extractPluginFromComponent)
|
plugins.push(plugin)
|
||||||
.filter(p => p !== null)
|
}
|
||||||
.map(p => p as PluginMetadata)
|
}
|
||||||
.concat(
|
for (const [name, setting] of Object.entries(settings.userPlugins)) {
|
||||||
await loadFeaturesFromCodes(
|
const { code } = setting
|
||||||
FeatureKind.Plugin,
|
let metadata: PluginMetadata
|
||||||
Object.keys(settings.userPlugins),
|
try {
|
||||||
Object.values(settings.userPlugins).map(p => p.code),
|
metadata = loadFeatureCode(code) as PluginMetadata
|
||||||
),
|
} catch (e) {
|
||||||
)
|
console.error('从代码加载用户插件失败。代码可能包含语法错误或执行时产生了异常', {
|
||||||
.value()
|
pluginName: name,
|
||||||
plugins.push(...otherPlugins)
|
error: e,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
plugins.push(metadata)
|
||||||
|
}
|
||||||
return Promise.allSettled(plugins.map(loadPlugin)).then(async () => {
|
return Promise.allSettled(plugins.map(loadPlugin)).then(async () => {
|
||||||
if (getGeneralSettings().devMode) {
|
if (getGeneralSettings().devMode) {
|
||||||
const { pluginLoadTime, pluginResolveTime } = await import('@/core/performance/plugin-trace')
|
const { pluginLoadTime, pluginResolveTime } = await import('@/core/performance/plugin-trace')
|
||||||
|
|||||||
@ -31,9 +31,9 @@ export const styles: Required<UserStyle>[] = reactive(Object.values(settings.use
|
|||||||
export const installStyle = async (input: UserStyle | string) => {
|
export const installStyle = async (input: UserStyle | string) => {
|
||||||
try {
|
try {
|
||||||
let userStyle: UserStyle
|
let userStyle: UserStyle
|
||||||
const { parseExternalInput } = await import('../core/external-input')
|
const { loadFeatureCode } = await import('@/core/external-input')
|
||||||
if (typeof input === 'string') {
|
if (typeof input === 'string') {
|
||||||
userStyle = await parseExternalInput<UserStyle>(input)
|
userStyle = loadFeatureCode(input) as UserStyle
|
||||||
} else {
|
} else {
|
||||||
userStyle = input
|
userStyle = input
|
||||||
}
|
}
|
||||||
@ -61,8 +61,8 @@ export const installStyle = async (input: UserStyle | string) => {
|
|||||||
metadata: userStyle,
|
metadata: userStyle,
|
||||||
message: `已安装样式'${displayName || name}'`,
|
message: `已安装样式'${displayName || name}'`,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
throw new Error('无效的样式代码')
|
throw new Error('无效的样式代码', { cause: e })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,27 +1,33 @@
|
|||||||
import { meta } from '@/core/meta'
|
import mdiStyle from './mdi.css'
|
||||||
|
|
||||||
export const initMdiStyle = () => {
|
export const initMdiStyle = () => {
|
||||||
const mdi = document.createElement('link')
|
// const mdi = document.createElement('link')
|
||||||
mdi.rel = 'stylesheet'
|
// mdi.rel = 'stylesheet'
|
||||||
mdi.href = meta.compilationInfo.altCdn.library.mdi
|
// mdi.href = meta.compilationInfo.altCdn.library.mdi
|
||||||
mdi.media = 'none'
|
// mdi.media = 'none'
|
||||||
|
|
||||||
const webFont = document.createElement('link')
|
// const webFont = document.createElement('link')
|
||||||
webFont.rel = 'preload'
|
// webFont.rel = 'preload'
|
||||||
webFont.href = meta.compilationInfo.altCdn.library.mdi.replace(/\.css$/, '.woff2')
|
// webFont.href = meta.compilationInfo.altCdn.library.mdi.replace(/\.css$/, '.woff2')
|
||||||
webFont.as = 'font'
|
// webFont.as = 'font'
|
||||||
webFont.type = 'font/woff2'
|
// webFont.type = 'font/woff2'
|
||||||
webFont.crossOrigin = 'anonymous'
|
// webFont.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
const webFontPromise = new Promise<void>(resolve => {
|
// const webFontPromise = new Promise<void>(resolve => {
|
||||||
webFont.onload = () => resolve()
|
// webFont.onload = () => resolve()
|
||||||
})
|
// })
|
||||||
const cssPromise = new Promise<void>(resolve => {
|
// const cssPromise = new Promise<void>(resolve => {
|
||||||
mdi.onload = () => resolve()
|
// mdi.onload = () => resolve()
|
||||||
})
|
// })
|
||||||
Promise.allSettled([cssPromise, webFontPromise]).then(() => {
|
// Promise.allSettled([cssPromise, webFontPromise]).then(() => {
|
||||||
mdi.media = 'all'
|
// mdi.media = 'all'
|
||||||
})
|
// })
|
||||||
|
|
||||||
return [mdi, webFont]
|
// return [mdi, webFont]
|
||||||
|
|
||||||
|
const mdi = document.createElement('style')
|
||||||
|
mdi.id = 'be-mdi'
|
||||||
|
mdi.innerHTML = mdiStyle
|
||||||
|
|
||||||
|
return mdi
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/ui/mdi/mdi.css
Normal file
1
src/ui/mdi/mdi.css
Normal file
File diff suppressed because one or more lines are too long
BIN
src/ui/mdi/mdi.woff2
Normal file
BIN
src/ui/mdi/mdi.woff2
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user