Merge branch 'preview-features' into preview

This commit is contained in:
the1812 2023-03-26 15:07:00 +08:00
commit 1b524b518a
81 changed files with 1695 additions and 290 deletions

View File

@ -1,7 +1,61 @@
# 更新日志
## v2.7.0-preview
`2023-03-26`
包含 v2.7.0 的所有更新内容.
- 新增功能 `组件二等分`, 在出现问题时可帮助定位是哪个组件导致的问题. (#3829, PR #3965 by [JLoeve](https://github.com/LonelySteve))
- 优化了 `loadFeatureCode` 的代码. (PR #4013 by [timongh](https://github.com/timongh))
- 搜索栏在搜索 ID 时的优化, 新增功能可以通过安装对应的插件来使用: (#677)
- 搜索 av / BV 号时, 支持显示视频标题
- 支持搜索专栏 cv 号 / 文集 rl 号
- 支持搜索音频 au 号 / 播放列表 am 号
- 支持搜索番剧 ss / ep 号, 或番剧详情 md 号
- `自定义顶栏` 的收藏和稍后再看支持显示数量. (#4069)
- 优化了 `网址参数清理` 的逻辑, 通过 Hook History API 来减少出现重复的历史记录. (#4039)
- 新增功能 `隐藏热搜`. (#3744)
> 隐藏搜索栏和搜索页面中的 `bilibili 热搜`.
- 新增功能 `隐藏视频分享`. (#3663)
> 隐藏视频和番剧播放器下方的分享按钮.
- `自定义顶栏` 的订阅支持过滤 在看 / 看过 / 想看. (#3217)
- 新增插件 `快捷键扩展 - 开关灯`. (#3587)
> 在快捷键的动作列表里添加一个 "开关灯".
## v2.7.0
`2023-03-26`
增加了 GitHub Projects 看板: https://github.com/users/the1812/projects/1/views/3, 上面会列出当前和未来计划的功能, 可供参考.
✨新增
- 快捷键扩展的几个插件自带了 YouTube / PotPlayer 等预设的默认按键. (#3971)
- `简化直播间` 在开启隐藏付费礼物时, 将直接隐藏左下角的打榜入口 (以前是领银瓜子所以没算进付费礼物). (#4067)
- `专栏文字选择` 更名为 `专栏复制优化`, 现在专栏的文字默认就可以直接选择, 本功能可以用于去除复制后带上的多余文本. (#4065)
- 删除功能 `直播录像下载`, 现在已经没有直播录像的页面了. (#4061)
- 在检测到[解除番剧区域限制](https://greasyfork.org/zh-CN/scripts/25718-%E8%A7%A3%E9%99%A4b%E7%AB%99%E5%8C%BA%E5%9F%9F%E9%99%90%E5%88%B6)的脚本在运行时, 本脚本将停止运行以避免兼容性问题. (#2704)
- `选集区域优化` 在多个分组时支持仅展开当前分组. (#3899)
- `播放时自动关灯` 支持星光动画. (PR #4077 by [z503722728](https://github.com/z503722728))
🐛修复
- 修复 `直播信息扩充` 失效的问题. (#4034, PR #4043 by [deepdarkssj](https://github.com/deepdarkssj))
- 修复 `简化直播间` 在开启隐藏付费礼物时, 右下布局错乱. (PR #4089 by [Pencil](https://github.com/pencilqaq))
- 修复动态相关功能对新版专栏卡片不生效. (#3994)
- 修复搜索界面的广告链接仍然可点击. (#3969)
- 修复 `图片批量导出` 在动态失效了. (#4038)
- 修复 `选集区域优化` 仅开启 `展开选集标题` 时视频标题无法显示完全. (#3909)
- 修复 `查看封面` 在复制链接后没有显示成功图标. (#4002)
- 修复番剧页面的部分夜间模式. (PR #4077 by [z503722728](https://github.com/z503722728))
☕开发者相关
- 修复 `IframePopup.vue` 的类型定义. (PR #4014 by [timongh](https://github.com/timongh))
- 修复 `UserItem.vue` 初始化 Toast 的逻辑问题. (PR #4056 by [timongh](https://github.com/timongh))
- 优化了 `switch-options.ts` 的注释. (PR #4021 by [timongh](https://github.com/timongh))
- 清理了项目中 Vue 文件里对未定义字段的引用. (#3992)
- 导出了 Dialog API (`@/core/dialog`), 允许其他组件使用.
- 调整了 CONTRIBUTING 中的本地开发流程描述. (PR #4077 by [z503722728](https://github.com/z503722728))
## v2.6.3-preview
`2023-02-09`
`2023-02-20`
包含 v2.6.3 的所有更新内容.

View File

@ -1,5 +1,10 @@
# 代码贡献指南
Bilibili Evolved 是一个基于 Web 前端技术构建的油猴脚本, 贡献代码前应具备的相关能力有:
- 了解最基础的油猴脚本的开发流程, 包括理解 [UserScript Header 和 GM API](https://www.tampermonkey.net/documentation.php?locale=en).
- 熟悉 Web 前端技术, 编写逻辑时使用 [TypeScript](https://www.typescriptlang.org/), 编写样式时使用 [Scss](https://sass-lang.com/), 创建 UI 时使用 [Vue 2](https://v2.cn.vuejs.org/).
- 新增的代码能够通过 [ESLint](https://eslint.org/) 和 [TypeScript](https://www.typescriptlang.org/) 检查.
## 搭建开发环境
- 需要安装 [Node.js](https://nodejs.org/en/download/) (>= 14.0), [Visual Studio Code](https://code.visualstudio.com/) 和 [pnpm](https://pnpm.io/installation).
@ -13,6 +18,10 @@ cd registry
pnpm install
```
- [配置 VS Code 插件](https://code.visualstudio.com/docs/editor/extension-marketplace):
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint), 用于格式化 TypeScript 和 Vue 文件.
- [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode), 用于格式化 Scss 和其他文件.
### 本体
需要说明的是, 脚本本体和功能是分开的两个项目. 本体的代码在 `src/` 下, 开发时产生 `dist/bilibili-evolved.dev.user.js` 文件. 功能的代码位于 `registry/` 下, 开发时在 `registry/dist/` 下产生文件.
> 如果不使用 Visual Studio Code, 则需要根据 `.vscode/tasks.json` 中各个任务定义的命令手动在终端执行. (npm scripts 仅用于 CI)
@ -20,10 +29,11 @@ pnpm install
配置本地调试环境:
**如果使用的是基于 Chromium 的浏览器**
1. Chrome 插件管理 `chrome://extensions/` > Tampermonkey > 详细信息
2. 打开 `允许访问文件网址`
3. 新建脚本
4. 粘贴内容:
1. VS Code 中运行 `启动开发服务 dev-server` 任务, 会在项目的 `dist/` 文件夹下生成一个开发用的脚本 `dist/bilibili-evolved.dev.user.js`.
2. Chrome 插件管理 `chrome://extensions/` > Tampermonkey > 详细信息
3. 打开 `允许访问文件网址`
4. 新建脚本
5. 粘贴内容:
```js
// ==UserScript==
// @name Bilibili Evolved (Local)
@ -63,19 +73,20 @@ pnpm install
// @connect localhost
// @connect *
// @require https://raw.githubusercontent.com/lodash/lodash/4.17.15/dist/lodash.min.js
// @require file://{{ bilibili-evolved.dev.user.js的绝对路径 }}
// @icon https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/images/logo-small.png
// @icon64 https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/images/logo.png
// ==/UserScript==
```
6. 在那些 `@require` 下面再添加一行 `@require file://{{ bilibili-evolved.dev.user.js的绝对路径 }}`
> Windows 例子: `@require file://C:/xxx/Bilibili-Evolved/bilibili-evolved.dev.user.js`
> macOS 例子: `@require file:///Users/xxx/Documents/Bilibili-Evolved/bilibili-evolved.dev.user.js`
6. 将里面的 `{{ bilibili-evolved.dev.user.js的绝对路径 }}` 替换为第一步生成的文件的真实路径.
> Windows 例子: `@require file://C:/xxx/Bilibili-Evolved/dist/bilibili-evolved.dev.user.js`
> macOS 例子: `@require file:///Users/xxx/Documents/Bilibili-Evolved/dist/bilibili-evolved.dev.user.js`
> 上面那些其他的 @require`src/client/common.meta.json` 里的保持一致就行, 偶尔这些依赖项会变动导致这个本地调试脚本失效, 到时候照着改一下就行.
7. 保存脚本, 运行 `启动开发服务 dev-server` 任务
8. 进入 b 站, 安装 `DevClient` 组件, 功能中显示已连接时就是成功了
7. 进入 b 站, 安装 `DevClient` 组件, 功能中显示已连接时就是成功了
**如果使用 Firefox 或 Safari**
1. 运行 `启动开发服务 dev-server` 任务时, 假设得到的本体链接为 `http://localhost:23333/dist/bilibili-evolved.dev.user.js`
@ -138,6 +149,7 @@ pnpm install
## 可用资源
本体提供了大量 API 供组件 / 插件使用.
### 全局
全局变量, 无需 `import` 就可以直接使用. (Tampermonkey API 这里不再列出了, 可根据代码提示使用)
@ -230,7 +242,11 @@ pnpm install
- `ui/AsyncButton.vue`: `click` 事件为异步函数时, 执行期间自动使 `Button` 禁用, 其他和 `Button` 相同.
## 代码风格检查
项目中含有 ESLint, 不通过 ESLint 是无法进行 Pull Request 的. 配置基于 `airbnb-base`, `typescript-eslint/recommended`, `vue/recommended` 修改而来, 几个比较特殊的规则如下:
项目中含有 ESLint, 不通过 ESLint 是无法进行 Pull Request 的.
你可以使用 [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 插件实时检查当前代码, 也可以运行 `生产:代码检查 prod:lint``生产:代码修复 prod:lint-fix` 来使用 ESLint 的命令行进行检查.
配置基于 `airbnb-base`, `typescript-eslint/recommended`, `vue/recommended` 修改而来, 几个比较特殊的规则如下:
### 强制性
- 除了 Vue 单文件组件, 禁止使用 `export default`, 所有导出必须命名.
@ -243,12 +259,11 @@ pnpm install
- 不需要使用 `this` 特性的函数, 均使用箭头函数.
## 提交 commit
仅提交源代码上的修改即可, 不建议把 dist 文件夹里的产物也提交, 否则容易在 PR 时产生冲突.
仅提交源代码上的修改即可, 不要把 dist 文件夹里的产物也提交, 产物会在发布新版本时在对应的生产分支上构建.
commit message 只需写明改动点, 中英文随意, 也不强求类似 [commit-lint](https://github.com/conventional-changelog/commitlint) 的格式.
## 发起 PR (合并请求)
将你的分支往主仓库的 `preview-features` (新增功能) 或 `preview-fixes` (功能修复) 分支合并就行.
## 自行保留
你可以选择不将功能代码合并到主仓库, 因此也没有 ESLint 的限制. PR 时仅添加指向你的仓库中的组件信息即可, 具体来说, 是在 `registry/lib/docs/third-party.ts` 中, 往对应数组中添加你的功能的相关信息, 当然别忘了把 `owner` 设为你的 GitHub 用户名.
或者, 也可以选择不将功能代码合并到主仓库, 因此也没有 ESLint 的限制. PR 时仅添加指向你的仓库中的组件信息即可, 具体来说, 是在 `registry/lib/docs/third-party.ts` 中, 往对应数组中添加你的功能的相关信息, 当然别忘了把 `owner` 设为你的 GitHub 用户名.

View File

@ -28,6 +28,16 @@ https://afdian.net/@the1812?tab=sponsor
| 时间 | 用户名 | 单号后4位 | 金额 |
| ------------------- | ---------------- | --------- | ------- |
| 2023.03.20 21:41:05 | 匿名 | 2237 | ¥6.66 |
| 2023.03.19 22:06:03 | *碧 | 3670 | ¥20.00 |
| 2023.03.15 11:36:31 | k*n | 6491 | ¥1.00 |
| 2023.03.14 00:29:54 | *曰 | 6448 | ¥10.00 |
| 2023.03.14 00:28:35 | *曰 | 8124 | ¥10.00 |
| 2023.03.04 11:47:53 | 匿名 | 0764 | ¥5.00 |
| 2023.02.28 20:56:51 | *肠 | 2857 | ¥10.00 |
| 2023.02.26 19:52:19 | 无*n | 0780 | ¥1.00 |
| 2023.02.20 23:40:27 | 匿名 | 5551 | ¥5.00 |
| 2023.02.20 21:15:04 | 匿名 | 6088 | ¥5.00 |
| 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 |

View File

@ -95,14 +95,6 @@
"fullRelativePath": "../../registry/dist/components/live/danmaku-sendbar.js",
"fullAbsolutePath": "registry/dist/components/live/danmaku-sendbar.js"
},
{
"type": "component",
"name": "downloadLiveRecords",
"displayName": "直播录像下载",
"description": "在直播录像页面 `live.bilibili.com/record/` 中添加下载支持.",
"fullRelativePath": "../../registry/dist/components/live/download-records.js",
"fullAbsolutePath": "registry/dist/components/live/download-records.js"
},
{
"type": "component",
"name": "liveGiftBox",
@ -223,6 +215,14 @@
"fullRelativePath": "../../registry/dist/components/style/hide/banner.js",
"fullAbsolutePath": "registry/dist/components/style/hide/banner.js"
},
{
"type": "component",
"name": "hideTrendingSearch",
"displayName": "隐藏热搜",
"description": "隐藏搜索栏和搜索页面中的 `bilibili 热搜`.",
"fullRelativePath": "../../registry/dist/components/style/hide/trending-search.js",
"fullAbsolutePath": "registry/dist/components/style/hide/trending-search.js"
},
{
"type": "component",
"name": "hideRecommendedLive",
@ -239,6 +239,14 @@
"fullRelativePath": "../../registry/dist/components/style/hide/video/related-videos.js",
"fullAbsolutePath": "registry/dist/components/style/hide/video/related-videos.js"
},
{
"type": "component",
"name": "hideVideoShare",
"displayName": "隐藏视频分享",
"description": "隐藏视频和番剧播放器下方的分享按钮.\r\n",
"fullRelativePath": "../../registry/dist/components/style/hide/video/share.js",
"fullAbsolutePath": "registry/dist/components/style/hide/video/share.js"
},
{
"type": "component",
"name": "hideVideoTopMask",
@ -402,8 +410,8 @@
{
"type": "component",
"name": "columnUnlock",
"displayName": "专栏文字选择",
"description": "使专栏的文字可以选择.",
"displayName": "专栏复制优化",
"description": "(原名: 专栏文字选择, 现在专栏已经不限制选中文字了)\r\n\r\n避免专栏的文字复制后在最后带上出处信息, 更贴近原生的复制行为.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/column-unlock.js",
"fullAbsolutePath": "registry/dist/components/utils/column-unlock.js"
},
@ -483,7 +491,7 @@
"type": "component",
"name": "urlParamsClean",
"displayName": "网址参数清理",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/url-params-clean.js",
"fullAbsolutePath": "registry/dist/components/utils/url-params-clean.js"
},
@ -776,6 +784,30 @@
"description": "by FoundTheWOUT\n\n在视频播放器右上角显示系统时间.",
"owner": "FoundTheWOUT"
},
{
"type": "plugin",
"name": "launchBar.actions.audioSearch",
"displayName": "搜索栏 - 音频跳转",
"description": "在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/audio-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/audio-search.js"
},
{
"type": "plugin",
"name": "launchBar.actions.bangumiSearch",
"displayName": "搜索栏 - 番剧跳转",
"description": "在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/bangumi-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/bangumi-search.js"
},
{
"type": "plugin",
"name": "launchBar.actions.cvSearch",
"displayName": "搜索栏 - 专栏跳转",
"description": "在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.\r\n",
"fullRelativePath": "../../registry/dist/plugins/launch-bar/cv-search.js",
"fullAbsolutePath": "registry/dist/plugins/launch-bar/cv-search.js"
},
{
"type": "plugin",
"name": "launchBar.trendingSearch",
@ -832,6 +864,14 @@
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-danmaku-list.js",
"fullAbsolutePath": "registry/dist/plugins/utils/keymap-toggle-danmaku-list.js"
},
{
"type": "plugin",
"name": "keymap.actions.togglePlayerLight",
"displayName": "快捷键扩展 - 开关灯",
"description": "在快捷键的动作列表里添加一个 \"开关灯\".",
"fullRelativePath": "../../registry/dist/plugins/utils/keymap-toggle-player-light.js",
"fullAbsolutePath": "registry/dist/plugins/utils/keymap-toggle-player-light.js"
},
{
"type": "plugin",
"name": "keymap.actions.toggleSubtitle",

View File

@ -116,15 +116,6 @@
在直播的网页全屏和全屏模式状态下, 在底部显示弹幕栏.
### [直播录像下载](../../registry/dist/components/live/download-records.js)
`downloadLiveRecords`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/live/download-records.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/live/download-records.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/live/download-records.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/live/download-records.js)
在直播录像页面 `live.bilibili.com/record/` 中添加下载支持.
### [直播全屏包裹](../../registry/dist/components/live/gift-box.js)
`liveGiftBox`
@ -264,6 +255,15 @@
隐藏首页顶部横幅.
### [隐藏热搜](../../registry/dist/components/style/hide/trending-search.js)
`hideTrendingSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/hide/trending-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/hide/trending-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/hide/trending-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/hide/trending-search.js)
隐藏搜索栏和搜索页面中的 `bilibili 热搜`.
### [隐藏直播推荐](../../registry/dist/components/style/hide/video/recommended-live.js)
`hideRecommendedLive`
@ -282,6 +282,15 @@
隐藏番剧和视频页面右侧的推荐视频列表. 注意: 如果你想关闭 b 站的自动连播 (自动播放下一个推荐视频) 功能, 需要先取消隐藏视频推荐才能看到开关.
### [隐藏视频分享](../../registry/dist/components/style/hide/video/share.js)
`hideVideoShare`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/style/hide/video/share.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/style/hide/video/share.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/style/hide/video/share.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/style/hide/video/share.js)
隐藏视频和番剧播放器下方的分享按钮.
### [隐藏视频标题层](../../registry/dist/components/style/hide/video/top-mask.js)
`hideVideoTopMask`
@ -487,14 +496,16 @@ by [@snowraincloud](https://github.com/snowraincloud)
在功能面板中提供一些可以每日进行的操作.
### [专栏文字选择](../../registry/dist/components/utils/column-unlock.js)
### [专栏复制优化](../../registry/dist/components/utils/column-unlock.js)
`columnUnlock`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/column-unlock.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/utils/column-unlock.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/column-unlock.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/column-unlock.js)
使专栏的文字可以选择.
(原名: 专栏文字选择, 现在专栏已经不限制选中文字了)
避免专栏的文字复制后在最后带上出处信息, 更贴近原生的复制行为.
### [复制评论链接](../../registry/dist/components/utils/comments/copy-link.js)
`copyCommentsLink`
@ -1024,6 +1035,33 @@ by FoundTheWOUT
在视频播放器右上角显示系统时间.
## 插件
### [搜索栏 - 音频跳转](../../registry/dist/plugins/launch-bar/audio-search.js)
`launchBar.actions.audioSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/audio-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/audio-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/audio-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/audio-search.js)
在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.
### [搜索栏 - 番剧跳转](../../registry/dist/plugins/launch-bar/bangumi-search.js)
`launchBar.actions.bangumiSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/bangumi-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/bangumi-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/bangumi-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/bangumi-search.js)
在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.
### [搜索栏 - 专栏跳转](../../registry/dist/plugins/launch-bar/cv-search.js)
`launchBar.actions.cvSearch`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/launch-bar/cv-search.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/launch-bar/cv-search.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/launch-bar/cv-search.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/launch-bar/cv-search.js)
在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.
### [搜索栏 - 搜索推荐](../../registry/dist/plugins/launch-bar/trending-search.js)
`launchBar.trendingSearch`
@ -1087,6 +1125,15 @@ by FoundTheWOUT
在快捷键的动作列表里添加一个 "开关弹幕列表".
### [快捷键扩展 - 开关灯](../../registry/dist/plugins/utils/keymap-toggle-player-light.js)
`keymap.actions.togglePlayerLight`
**jsDelivr:** [`Stable`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/plugins/utils/keymap-toggle-player-light.js) / [`Preview`](https://fastly.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/plugins/utils/keymap-toggle-player-light.js)
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/plugins/utils/keymap-toggle-player-light.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/plugins/utils/keymap-toggle-player-light.js)
在快捷键的动作列表里添加一个 "开关灯".
### [快捷键扩展 - 开关 CC 字幕](../../registry/dist/plugins/utils/keymap-toggle-subtitle.js)
`keymap.actions.toggleSubtitle`

View File

@ -111,13 +111,12 @@
{
"name": "downloader",
"displayName": "下载器",
"description": "支持下载各种内容.\n\n包含以下功能:\n下载视频, 下载字幕, 下载弹幕, 下载音频, 直播录像下载",
"description": "支持下载各种内容.\n\n包含以下功能:\n下载视频, 下载字幕, 下载弹幕, 下载音频",
"components": [
"downloadVideo",
"downloadSubtitle",
"downloadDanmaku",
"downloadAudio",
"downloadLiveRecords"
"downloadAudio"
],
"items": [
{
@ -151,14 +150,6 @@
"description": "开启音频下载支持, 音频页面中可以在功能面板中下载当前音频.\n\n> 需要进入音频的详细信息页面才能下载, 在其他页面中此按钮将不可点击.",
"fullRelativePath": "../../registry/dist/components/utils/download-audio.js",
"fullAbsolutePath": "registry/dist/components/utils/download-audio.js"
},
{
"type": "component",
"name": "downloadLiveRecords",
"displayName": "直播录像下载",
"description": "在直播录像页面 `live.bilibili.com/record/` 中添加下载支持.",
"fullRelativePath": "../../registry/dist/components/live/download-records.js",
"fullAbsolutePath": "registry/dist/components/live/download-records.js"
}
],
"type": "pack"
@ -166,7 +157,7 @@
{
"name": "starter",
"displayName": "常用功能包",
"description": "提供一些常用功能.\n\n包含以下功能:\n使用细滚动条, 自定义顶栏, 删除广告, 专栏文字选择, 网址参数清理, 快捷键扩展, 查看封面, BV 号转换, 删除直播水印, 直播弹幕发送栏, 直播全屏包裹, 展开动态内容, 动态反折叠, 快速收起评论, 禁止跳转动态详情, 展开视频简介, 设置面板 - \"最近使用\" 类别",
"description": "提供一些常用功能.\n\n包含以下功能:\n使用细滚动条, 自定义顶栏, 删除广告, 专栏复制优化, 网址参数清理, 快捷键扩展, 查看封面, BV 号转换, 删除直播水印, 直播弹幕发送栏, 直播全屏包裹, 展开动态内容, 动态反折叠, 快速收起评论, 禁止跳转动态详情, 展开视频简介, 设置面板 - \"最近使用\" 类别",
"components": [
"elegantScrollbar",
"customNavbar",
@ -216,8 +207,8 @@
{
"type": "component",
"name": "columnUnlock",
"displayName": "专栏文字选择",
"description": "使专栏的文字可以选择.",
"displayName": "专栏复制优化",
"description": "(原名: 专栏文字选择, 现在专栏已经不限制选中文字了)\r\n\r\n避免专栏的文字复制后在最后带上出处信息, 更贴近原生的复制行为.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/column-unlock.js",
"fullAbsolutePath": "registry/dist/components/utils/column-unlock.js"
},
@ -225,7 +216,7 @@
"type": "component",
"name": "urlParamsClean",
"displayName": "网址参数清理",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.",
"description": "自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.\r\n",
"fullRelativePath": "../../registry/dist/components/utils/url-params-clean.js",
"fullAbsolutePath": "registry/dist/components/utils/url-params-clean.js"
},

View File

@ -84,7 +84,7 @@ https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist
支持下载各种内容.
包含以下功能:
下载视频, 下载字幕, 下载弹幕, 下载音频, 直播录像下载
下载视频, 下载字幕, 下载弹幕, 下载音频
<details>
<summary><strong>jsDelivr Stable</strong></summary>
@ -94,7 +94,6 @@ https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/subtitle/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/download-audio.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/live/download-records.js
```
</details>
@ -106,7 +105,6 @@ https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/subtitle/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/download-audio.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/live/download-records.js
```
</details>
@ -118,7 +116,6 @@ https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/subtitle/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/utils/download-audio.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/live/download-records.js
```
</details>
@ -130,7 +127,6 @@ https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/subtitle/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/download.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/utils/download-audio.js
https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/live/download-records.js
```
</details>
@ -139,7 +135,7 @@ https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist
提供一些常用功能.
包含以下功能:
使用细滚动条, 自定义顶栏, 删除广告, 专栏文字选择, 网址参数清理, 快捷键扩展, 查看封面, BV 号转换, 删除直播水印, 直播弹幕发送栏, 直播全屏包裹, 展开动态内容, 动态反折叠, 快速收起评论, 禁止跳转动态详情, 展开视频简介, 设置面板 - "最近使用" 类别
使用细滚动条, 自定义顶栏, 删除广告, 专栏复制优化, 网址参数清理, 快捷键扩展, 查看封面, BV 号转换, 删除直播水印, 直播弹幕发送栏, 直播全屏包裹, 展开动态内容, 动态反折叠, 快速收起评论, 禁止跳转动态详情, 展开视频简介, 设置面板 - "最近使用" 类别
<details>
<summary><strong>jsDelivr Stable</strong></summary>

View File

@ -51,9 +51,12 @@ const entry = async () => {
}
const face = dq(
clone,
'.live-up-img, .bili-dyn-live-users__item__face .bili-awesome-img',
'.live-up-img, .bili-dyn-live-users__item__face, .bili-awesome-img',
) as HTMLElement
face.style.backgroundImage = `url(${it.face})`
face.style.backgroundSize = 'cover'
const facepic = dq(clone, '.b-img--face') as HTMLElement
facepic.style.visibility = 'hidden'
const title = dq(clone, '.live-name, .bili-dyn-live-users__item__title') as HTMLElement
title.innerHTML = it.title
title.title = it.title

View File

@ -0,0 +1,5 @@
.bili-album__watch__content {
max-height: calc(100vh - 200px);
overflow: auto;
// overscroll-behavior: contain;
}

View File

@ -0,0 +1 @@
在动态里查看图片时 (非全屏), 限制高度不超过一屏, 超过则可滚动, 避免长图看完后动态被顶到很上面.

View File

@ -0,0 +1,14 @@
import { defineComponentMetadata } from '@/components/define'
export const component = defineComponentMetadata({
name: 'imagePreviewScroll',
displayName: '动态图片限高',
tags: [componentsTags.feeds],
instantStyles: [
{
name: 'imagePreviewScroll',
style: () => import('./image-preview-scroll.scss'),
},
],
entry: none,
})

View File

@ -7,9 +7,7 @@
:value="folder"
@change="change($event)"
>
<template #item="{ item }">
{{ item.name }}
</template>
<template #item="{ item }"> {{ item.name }} ({{ item.count }}) </template>
</VDropdown>
</template>
<script lang="ts">

View File

@ -9,7 +9,7 @@
</div>
<div class="live-info">
<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>
</a>
</transition-group>

View File

@ -3,10 +3,19 @@
</template>
<script lang="ts">
import type { PropType } from 'vue'
import type { NavbarIframeConfig } from './iframe'
import { popperMixin } from '../mixins'
import { CustomNavbarItem } from '../custom-navbar-item'
export default Vue.extend({
name: 'IframePopup',
mixins: [popperMixin],
props: {
item: {
type: CustomNavbarItem as unknown as PropType<CustomNavbarItem & NavbarIframeConfig>,
required: true,
},
},
})
</script>

View File

@ -1,6 +1,6 @@
import { CustomNavbarItemInit } from '../custom-navbar-item'
interface NavbarIframeConfig {
export interface NavbarIframeConfig {
src: string
href: string
width: number

View File

@ -1,5 +1,5 @@
<template>
<SubscriptionsList type="bangumi"></SubscriptionsList>
<SubscriptionsList type="bangumi" :filter="filter"></SubscriptionsList>
</template>
<script lang="ts">
import SubscriptionsList from './SubscriptionsList.vue'
@ -8,5 +8,11 @@ export default Vue.extend({
components: {
SubscriptionsList,
},
props: {
filter: {
type: [Object, null],
default: null,
},
},
})
</script>

View File

@ -1,5 +1,5 @@
<template>
<SubscriptionsList type="cinema"></SubscriptionsList>
<SubscriptionsList type="cinema" :filter="filter"></SubscriptionsList>
</template>
<script lang="ts">
import SubscriptionsList from './SubscriptionsList.vue'
@ -8,5 +8,11 @@ export default Vue.extend({
components: {
SubscriptionsList,
},
props: {
filter: {
type: [Object, null],
default: null,
},
},
})
</script>

View File

@ -1,43 +1,107 @@
<template>
<div class="navbar-subscriptions">
<TabControl ref="tabControl" :tabs="tabs" :more-link="moreLink"></TabControl>
<TabControl ref="tabControl" :tabs="tabs" :more-link="moreLink">
<template #header-item>
<div class="navbar-subscriptions-filter">
<VDropdown v-model="selectedFilter" round :items="filterItems" />
</div>
</template>
</TabControl>
</div>
</template>
<script lang="ts">
import { TabControl } from '@/ui'
import { TabMappings, TabMapping } from '@/ui/tab-mapping'
import { TabControl, VDropdown } from '@/ui'
import { TabMapping, TabMappings } from '@/ui/tab-mapping'
import { getUID } from '@/core/utils'
import { popperMixin } from '../mixins'
import { SubscriptionTypes } from './subscriptions'
import { SubscriptionStatus, SubscriptionStatusFilter } from './types'
const filterItems: {
name: string
value: SubscriptionStatusFilter
displayName: string
}[] = [
{
name: 'all',
value: {
viewAll: true,
status: SubscriptionStatus.Viewing,
},
displayName: '全部',
},
{
name: 'viewing',
value: {
viewAll: false,
status: SubscriptionStatus.Viewing,
},
displayName: '在看',
},
{
name: 'toView',
value: {
viewAll: false,
status: SubscriptionStatus.ToView,
},
displayName: '想看',
},
{
name: 'viewed',
value: {
viewAll: false,
status: SubscriptionStatus.Viewed,
},
displayName: '看过',
},
]
export default Vue.extend({
components: {
TabControl,
VDropdown,
},
mixins: [popperMixin],
data() {
const uid = getUID()
return {
uid,
filterItems,
selectedFilter: filterItems[0],
moreLink: (tab: TabMapping) => `https://space.bilibili.com/${uid}/${tab.name}`,
tabs: [
filter: {
viewAll: true,
status: SubscriptionStatus.Viewing,
} as SubscriptionStatusFilter,
}
},
computed: {
tabs(): TabMappings {
return [
{
name: SubscriptionTypes.Bangumi,
displayName: '追番',
activeLink: `https://space.bilibili.com/${uid}/bangumi`,
activeLink: `https://space.bilibili.com/${this.uid}/bangumi`,
component: () => import('./BangumiSubscriptions.vue').then(m => m.default),
propsData: {
filter: this.selectedFilter.value,
},
},
{
name: SubscriptionTypes.Cinema,
displayName: '追剧',
activeLink: `https://space.bilibili.com/${uid}/cinema`,
activeLink: `https://space.bilibili.com/${this.uid}/cinema`,
component: () => import('./CinemaSubscriptions.vue').then(m => m.default),
propsData: {
filter: this.selectedFilter.value,
},
},
] as TabMappings,
}
]
},
},
})
</script>
<style lang="scss">
@import 'common';
@import '../popup';
.navbar-subscriptions {
@ -54,5 +118,10 @@ export default Vue.extend({
padding-bottom: 0;
}
}
&-filter {
@include h-stretch();
justify-content: flex-end;
height: 26px;
}
}
</style>

View File

@ -46,12 +46,8 @@ import { logError } from '@/core/utils/log'
import { DpiImage, VLoading, VEmpty, VIcon, ScrollTrigger } from '@/ui'
import { getJsonWithCredentials } from '@/core/ajax'
import { SubscriptionTypes } from './subscriptions'
import { SubscriptionItem, SubscriptionStatus, SubscriptionStatusFilter } from './types'
enum SubscriptionStatus {
ToView = 1,
Viewing,
Viewed,
}
const getStatusText = (status: SubscriptionStatus) => {
switch (status) {
case SubscriptionStatus.ToView:
@ -63,10 +59,7 @@ const getStatusText = (status: SubscriptionStatus) => {
return '看过'
}
}
const subscriptionSorter = (
a: { status: SubscriptionStatus },
b: { status: SubscriptionStatus },
) => {
const subscriptionSorter = (a: SubscriptionItem, b: SubscriptionItem) => {
let statusA = a.status
if (statusA !== SubscriptionStatus.Viewed) {
statusA = SubscriptionStatus.Viewed - statusA
@ -86,6 +79,10 @@ export default Vue.extend({
ScrollTrigger,
},
props: {
filter: {
type: [Object, null],
default: null,
},
type: {
type: String,
default: SubscriptionTypes.Bangumi,
@ -99,12 +96,21 @@ export default Vue.extend({
page: 1,
}
},
watch: {
filter() {
this.cards = []
this.loading = true
this.page = 1
this.nextPage()
},
},
async created() {
this.nextPage()
},
methods: {
async nextPage() {
try {
const filter = this.filter as SubscriptionStatusFilter
const json = await getJsonWithCredentials(
`https://api.bilibili.com/x/space/bangumi/follow/list?type=${
this.type !== SubscriptionTypes.Bangumi ? '2' : '1'
@ -114,10 +120,10 @@ export default Vue.extend({
logError(`加载订阅信息失败: ${json.message}`)
return
}
const cards = lodash
const newCards: SubscriptionItem[] = lodash
.uniqBy(
(this.cards as any[]).concat(
(lodash.get(json, 'data.list') as any[]).map(item => ({
(lodash.get(json, 'data.list') as any[]).map(
(item): SubscriptionItem => ({
title: item.title,
coverUrl: item.square_cover.replace('http:', 'https:'),
latest: item.new_ep.index_show,
@ -127,14 +133,20 @@ export default Vue.extend({
statusText: getStatusText(item.follow_status),
playUrl: `https://www.bilibili.com/bangumi/play/ss${item.season_id}`,
mediaUrl: `https://www.bilibili.com/bangumi/media/md${item.media_id}`,
})),
}),
),
card => card.id,
)
.filter(card => {
if (filter.viewAll) {
return true
}
return card.status === filter.status
})
.sort(subscriptionSorter)
this.page++
this.cards = cards
this.hasMorePage = lodash.get(json, 'data.total', 0) > this.cards.length
this.cards = this.cards.concat(newCards)
this.hasMorePage = newCards.length > 0
} finally {
this.loading = false
}

View File

@ -0,0 +1,20 @@
export enum SubscriptionStatus {
ToView = 1,
Viewing,
Viewed,
}
export interface SubscriptionStatusFilter {
viewAll: boolean
status: SubscriptionStatus
}
export interface SubscriptionItem {
title: string
coverUrl: string
latest: number
progress: string
id: string
status: SubscriptionStatus
statusText: string
playUrl: string
mediaUrl: string
}

View File

@ -1,6 +1,7 @@
<template>
<div class="watchlater-list">
<div class="header">
<div class="watchlater-list-summary"> {{ filteredCards.length }} </div>
<div class="search">
<TextBox v-model="search" linear placeholder="搜索"></TextBox>
</div>
@ -203,13 +204,17 @@ export default Vue.extend({
cursor: pointer;
}
.header {
@include h-stretch();
@include h-center();
justify-content: space-between;
align-self: stretch;
margin: 16px 12px;
.watchlater-list-summary {
margin-right: 6px;
}
.search {
position: relative;
flex-grow: 1;
align-self: stretch;
margin-right: 8px;
.be-textbox {
height: 100%;

View File

@ -888,9 +888,133 @@
.startlive-icon {
@include to-theme('blue');
}
.right-area .tip-wrap .icon path[fill="#00AEEC"] {
.right-area .tip-wrap .icon path[fill='#00AEEC'] {
@include theme-fill();
}
.bpx-player-pbp-videoshot rect[clip-path] {
@include theme-fill();
}
[class*='numberListItem_number_list_item'] {
@include background-color('5');
@include border-color();
&:hover {
@include theme-border-color();
}
&[class*='numberListItem_select'] {
@include theme-border-color();
[class*='numberListItem_title'] {
@include theme-color();
}
}
}
[class*='review_review_module'] {
@include border-color('4');
[class*='review_review_item_fill'] {
@include background-color('4');
}
[class*='review_module_title'],
[class*='review_review_title'] {
@include color('e');
}
[class*='review_review_author'] {
@include color('e');
&[class*='review_is-vip'] {
@include vip-color();
}
}
[class*='review_review_content'] {
@include color('a');
}
}
[class*='epitem_ep_item'] {
&:hover {
@include background-color('5');
}
a {
@include color('e');
&:hover {
@include theme-color();
}
}
&[class*='epitem_cursor'] {
@include background-color('5');
path {
@include theme-fill();
}
}
}
[class*='seasonlist_season_list'] {
[class*='seasonlist_ss_title'] {
@include color('e');
}
[class*='seasonlist_expand_more'] {
@include background-color('4');
@include color('e');
}
}
[class*='mediainfo_media_info'] {
@include border-color('4');
[class*='mediainfo_media_desc'] {
&,
& i {
@include color('e');
}
}
[class*='mediainfo_media_title'] {
@include color('e');
}
[class*='mediainfo_av_link'],
[class*='mediainfo_home_link'],
[class*='mediainfo_media_pub'] {
@include color('a');
}
[class*='mediainfo_ellipsis'],
[class*='mediainfo_media_desc'] i {
@include background();
@include theme-color();
}
}
[class*='section_ep_section_module'] {
@include background-color('4');
[class*='section_section_title'] {
@include color('e');
}
}
[class*='RecommendItem_wrap'] {
[class*='RecommendItem_title'] {
@include color('e');
}
}
[class*='eplist_ep_list_wrapper'] {
@include background-color('4');
[class*='eplist_list_title'] [class*='eplist_left_wrap'] h4 {
@include color('e');
}
[class*='numberListItem_title'] {
@include color('e');
}
}
[class*='operation_split_line'] {
@include border-color('4');
}
[class*='mediainfo_btn_rating'] {
@include background-color();
}
[class*='DanmukuBox_wrap'] {
@include background();
}

View File

@ -0,0 +1,5 @@
.search-panel {
> .trending {
display: none !important;
}
}

View File

@ -0,0 +1 @@
隐藏搜索栏和搜索页面中的 `bilibili 热搜`.

View File

@ -0,0 +1,27 @@
import { defineComponentMetadata } from '@/components/define'
import { allMutations } from '@/core/observer'
export const component = defineComponentMetadata({
name: 'hideTrendingSearch',
displayName: '隐藏热搜',
tags: [componentsTags.style],
instantStyles: [
{
name: 'hideTrendingSearch',
style: () => import('./hide-trending-search.scss'),
},
],
entry: async () => {
allMutations(records => {
records.forEach(record => {
if (
record.target instanceof HTMLInputElement &&
record.target.classList.contains('nav-search-input') &&
record.target.placeholder !== '搜索'
) {
record.target.placeholder = '搜索'
}
})
})
},
})

View File

@ -0,0 +1,15 @@
.video-toolbar-v1 {
.share-wrap {
display: none !important;
}
}
.video-toolbar-container {
.video-share-wrap {
display: none !important;
}
}
.player-left-components {
[class*="toolbar_share_info"] {
display: none !important;
}
}

View File

@ -0,0 +1 @@
隐藏视频和番剧播放器下方的分享按钮.

View File

@ -0,0 +1,14 @@
import { defineComponentMetadata } from '@/components/define'
export const component = defineComponentMetadata({
name: 'hideVideoShare',
displayName: '隐藏视频分享',
tags: [componentsTags.style, componentsTags.video],
instantStyles: [
{
name: 'hideVideoShare',
style: () => import('./hide-video-share.scss'),
},
],
entry: none,
})

View File

@ -193,81 +193,14 @@ $prefix: 'simplifyLiveroom-switch';
.gift-section.guard-ent,
.seeds-wrap > .dp-i-block > .item:not(.seeds),
.gift-control-panel .wish-icon,
.gift-control-panel .wish-tip {
.gift-control-panel .wish-tip,
.gift-control-panel .left-part-ctnr {
display: none !important;
}
.gift-control-section,
.gift-control-panel {
height: 48px !important;
}
.gift-left-part {
display: flex !important;
align-items: center !important;
gap: 6px !important;
width: auto !important;
height: auto !important;
padding: 10px 0 0 16px !important;
.entry-icon {
margin: 0 !important;
width: 24px !important;
height: 24px !important;
}
.wait-num {
display: none !important;
}
.time-span {
margin: 1px 0 0 0 !important;
}
}
.draw-box,
.anchor-lottery-entry,
.treasure-box {
display: flex !important;
align-items: center !important;
padding: 10px 0 0 16px !important;
max-height: 36px !important;
.draw-box-pic,
.anchor-lot-icon,
.box-icon {
margin: 0 !important;
width: 24px !important;
height: 24px !important;
background-position: 0 -2.5px !important;
&.open {
background-position: 0 -1px !important;
}
}
.draw-box-pic {
transform: scale(1.1);
}
#BLRHH_treasure_div_tip br {
display: none !important;
}
#BLRHH_treasure_div {
img,
canvas {
max-height: 24px !important;
}
}
.draw-box-text,
.anchor-lot-text,
.count-down,
#BLRHH_treasure_div_tip,
#BLRHH_treasure_div_timer {
margin-left: 12px !important;
padding: 4px 8px !important;
max-width: unset !important;
}
.awarding-panel {
bottom: 42px !important;
}
}
.draw-box {
padding-top: 11px !important;
}
.anchor-lottery-entry {
padding-top: 12px !important;
}
.gift-control-panel .right-part {
height: 48px !important;
display: flex !important;
@ -281,6 +214,29 @@ $prefix: 'simplifyLiveroom-switch';
}
}
}
.gift-control-panel {
.more-gift-section {
position: static !important;
margin: 0 10px 0 0 !important;
height: 48px !important;
}
.z-gift-package {
margin: auto 0 !important;
position: static !important;
.gift-package {
margin-top: 2px;
}
}
.right-part {
padding-top: 0 !important;
}
.right-section > .gift-section {
height: 42px !important;
}
.battery-pic {
display: none !important;
}
}
}
.#{$prefix}-guard {
.guard-danmaku::before {

View File

@ -0,0 +1,3 @@
(原名: 专栏文字选择, 现在专栏已经不限制选中文字了)
避免专栏的文字复制后在最后带上出处信息, 更贴近原生的复制行为.

View File

@ -22,7 +22,7 @@ const entry = async () => {
}
export const component = defineComponentMetadata({
name: 'columnUnlock',
displayName: '专栏文字选择',
displayName: '专栏复制优化',
entry,
reload: entry,
unload: async () => {
@ -30,8 +30,5 @@ export const component = defineComponentMetadata({
enable = false
},
tags: [componentsTags.utils],
description: {
'zh-CN': '使专栏的文字可以选择.',
},
urlInclude: ['//www.bilibili.com/read/'],
})

View File

@ -34,8 +34,6 @@ export default Vue.extend({
data() {
return {
client: null,
// sessions: [],
// devRecords: options.devRecords,
isConnected: false,
}
},
@ -44,12 +42,10 @@ export default Vue.extend({
this.client = devClient
this.updateConnectionStatus()
devClient.addEventListener(DevClientEvents.ServerChange, this.updateConnectionStatus)
// devClient.addEventListener(DevClientEvents.SessionsUpdate, this.updateSessionsStatus)
},
beforeDestroy() {
const devClient = this.client as DevClient
devClient.removeEventListener(DevClientEvents.ServerChange, this.updateConnectionStatus)
// devClient.removeEventListener(DevClientEvents.SessionsUpdate, this.updateSessionsStatus)
},
methods: {
async connect() {
@ -61,9 +57,6 @@ export default Vue.extend({
updateConnectionStatus() {
this.isConnected = this.client.isConnected
},
updateSessionsStatus() {
this.sessions = [...this.client.sessions]
},
},
})
</script>

View File

@ -19,7 +19,7 @@ const importSeries = async (sid, uid, csrf) => {
// 创建收藏夹获取新收藏夹id
let favId = 0
while (true) {
for (;;) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/folder/add', {
method: 'POST',
credentials: 'include',
@ -43,7 +43,7 @@ const importSeries = async (sid, uid, csrf) => {
for (let i = 0; i < seriesVideos.length; i++) {
// 做个延迟,防止太快而遭服务器拒绝
await delay(500)
while (true) {
for (;;) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/resource/deal', {
method: 'POST',
credentials: 'include',
@ -72,7 +72,7 @@ const importSeries = async (sid, uid, csrf) => {
}
const importCollection = async (sid, csrf) => {
while (true) {
for (;;) {
const response = await fetch('https://api.bilibili.com/x/v3/fav/season/fav', {
method: 'POST',
credentials: 'include',

View File

@ -4,7 +4,7 @@
{{ row.displayName }}
</div>
<div
class="row-default-binding"
class="row-default-binding row-binding"
:class="{
overwritten: isOverwrittern(presets[selectedPreset], customKeyBindings),
'not-set': presetBase[row.name] === undefined,
@ -13,7 +13,7 @@
{{ showReadonlyKey(presetBase) }}
</div>
<div
class="row-preset-binding"
class="row-preset-binding row-binding"
:class="{
overwritten: isOverwrittern(customKeyBindings),
'not-set': presets[selectedPreset][row.name] === undefined,
@ -21,7 +21,7 @@
>
{{ showReadonlyKey(presets[selectedPreset]) }}
</div>
<div class="row-custom-binding">
<div class="row-custom-binding row-binding">
<div v-if="editable" class="custom-binding-edit">
<TextBox
ref="customBindingTextBox"
@ -129,6 +129,12 @@ export default Vue.extend({
opacity: 0.25;
}
}
.row-binding {
&,
& input {
font-family: monospace, sans-serif;
}
}
.row-custom-binding {
.be-button {
padding: 4px;

View File

@ -84,6 +84,9 @@ body:not(.preserve-feed-goods) .bili-dyn-content__orig__additional:has(.dyn-good
}
.bili-video-card__wrap {
a[href*="cm.bilibili.com"] {
pointer-events: none !important;
cursor: default !important;
> * {
visibility: hidden !important;
}

View File

@ -0,0 +1 @@
自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.

View File

@ -49,6 +49,7 @@ const entry = async () => {
'vd_source',
'is_story_h5',
'buvid',
'plat_id',
]
const [blockParams] = registerAndGetData('urlParamsClean.params', builtInBlockParams)
const builtInSiteSpecifiedParams = [
@ -80,10 +81,11 @@ const entry = async () => {
const builtInTailingSlash: { match: string | RegExp }[] = []
const [tailingSlash] = registerAndGetData('urlParamsClean.tailingSlash', builtInTailingSlash)
const clean = () => {
const urlParams = window.location.search.substring(1).split('&')
const getCleanUrl = (originalUrl: string) => {
const url = new URL(originalUrl, location.origin)
const urlParams = url.search.substring(1).split('&')
if (urlParams.some(param => noClean.some(it => param.includes(it)))) {
return
return originalUrl
}
const filteredParams = urlParams.filter(p => {
if (blockParams.some(b => p.startsWith(`${b}=`))) {
@ -99,19 +101,47 @@ const entry = async () => {
return true
})
const filteredParamsString = filteredParams.join('&')
let url = document.URL.replace(window.location.search, '')
tailingSlash.forEach(({ match }) => {
if (matchPattern(url, match) && url.endsWith('/')) {
url = url.slice(0, url.length - 1)
const pathName = url.pathname
if (matchPattern(pathName, match) && pathName.endsWith('/')) {
url.pathname = pathName.slice(0, pathName.length - 1)
}
})
const query = filteredParamsString ? `?${filteredParamsString}` : ''
const newUrl = url + query
url.search = query
return url.toString()
}
const createHistoryHook = (
original: (data: unknown, unused: string, url: string, ...restArgs: unknown[]) => void,
) => {
return function historyHook(
data: unknown,
unused: string,
url: string,
...restArgs: unknown[]
) {
const newUrl = getCleanUrl(url)
if (newUrl !== url) {
console.log('History API 拦截', url, newUrl)
return original.call(this, data, unused, newUrl, ...restArgs)
}
return original.call(this, data, unused, url, ...restArgs)
}
}
const originalPushState = unsafeWindow.history.pushState
unsafeWindow.history.pushState = createHistoryHook(originalPushState)
const originalReplaceState = unsafeWindow.history.replaceState
unsafeWindow.history.replaceState = createHistoryHook(originalReplaceState)
const clean = () => {
const newUrl = getCleanUrl(document.URL)
if (newUrl !== document.URL) {
console.log(document.URL, newUrl)
console.log('直接清理', document.URL, newUrl)
window.history.replaceState(history.state, '', newUrl)
}
}
const { fullyLoaded } = await import('@/core/life-cycle')
const { urlChange } = await import('@/core/observer')
fullyLoaded(() => {
@ -122,10 +152,6 @@ export const component = defineComponentMetadata({
name: 'urlParamsClean',
displayName,
entry,
description: {
'zh-CN':
'自动删除网址中的多余跟踪参数. 请注意这会导致浏览器历史记录出现重复的标题 (分别是转换前后的网址), 并可能导致后退要多退几次.',
},
tags: [componentsTags.utils],
urlExclude: [/game\.bilibili\.com\/fgo/, /live\.bilibili\.com\/p\/html\/live-app-hotrank\//],
})

View File

@ -18,6 +18,9 @@ body {
line-height: 1.5 !important;
}
}
.video-section-list[style]:not([style*='height: 0px']) {
height: auto !important;
}
}
.multi-page-v1,
.multi-page {
@ -43,7 +46,7 @@ body {
.video-sections-content-list {
max-height: unset !important;
height: auto !important;
.video-section-list:not([style*="height: 0px"]) {
.video-section-list[style]:not([style*='height: 0px']) {
height: auto !important;
}
}

View File

@ -0,0 +1,64 @@
// author: https://github.com/z503722728
const CreateAnim = (): void => {
const biliMainHeader = document.getElementById('biliMainHeader')
if (biliMainHeader == null) {
return
}
const mstars1 = document.createElement('div')
mstars1.id = 'mstars1'
const mstars2 = document.createElement('div')
mstars2.id = 'mstars2'
biliMainHeader.appendChild(mstars1)
biliMainHeader.appendChild(mstars2)
// 添加一段css 样式到document最后
const style = document.createElement('style')
// generate random stars
function generate(numCtrl) {
let star = ''
const max = window.innerWidth * window.innerHeight
for (let i = 0; i < max / numCtrl; i++) {
const x = Math.floor(Math.random() * window.innerWidth * 1.5)
const y = Math.floor(Math.random() * (window.innerHeight + 2000))
star += `${x}px ${y}px #FFF,`
}
const x = Math.floor(Math.random() * window.innerWidth * 1.5)
const y = Math.floor(Math.random() * (window.innerHeight + 2000))
star += `${x}px ${y}px #FFF;`
return star
}
const starNumCtl = 400
const stars1Shadow = generate(starNumCtl)
const stars2Shadow = generate(starNumCtl * 2)
const stars3Shadow = generate(starNumCtl * 4)
const stars4Shadow = generate(starNumCtl * 8)
style.innerHTML = `
#mstars1{z-index: 1009;position: fixed;left:0px; width:1px;height:1px;background:transparent;box-shadow:${stars1Shadow};animation:animStar 50s linear infinite}
#mstars1:after{content:' ';position:fixed;left:0px;top:0px;width:1px;height:1px;background:transparent;box-shadow:${stars2Shadow}}
#mstars2{z-index: 1009;position: fixed;left:0px;width:2px;height:2px;background:transparent;box-shadow:${stars3Shadow};animation:animStar 100s linear infinite}
#mstars2:after{content:' ';position:fixed;left:0px;top:0px;width:2px;height:2px;background:transparent;box-shadow:${stars4Shadow}}
@keyframes animStar{from{transform:translateY(-200px)}to{transform:translateY(-2200px)}}
`
document.body.appendChild(style)
}
export const StarAnim = (on: boolean) => {
// 查找id mstars1 的div
let mstars1 = document.getElementById('mstars1')
let mstars2 = document.getElementById('mstars2')
// 如果没有找到id biliMainHeader 的div创建2个id为 mstars1 mstars2 的div
if (on) {
if (mstars1 == null) {
CreateAnim()
mstars1 = document.getElementById('mstars1')
mstars2 = document.getElementById('mstars2')
}
// 设置mstars1 mstars2 visible 为true
mstars1.style.visibility = 'visible'
mstars2.style.visibility = 'visible'
} else if (mstars1 != null) {
mstars1.style.visibility = 'hidden'
mstars2.style.visibility = 'hidden'
}
}

View File

@ -1,44 +1,64 @@
import { defineComponentMetadata } from '@/components/define'
import { playerAgent } from '@/components/video/player-agent'
import { lightOn, lightOff } from '@/components/video/player-light'
import { videoChange } from '@/core/observer'
import { allVideoUrls } from '@/core/utils/urls'
import type { PlayerAgent } from '@/components/video/player-agent/base'
import { StarAnim } from './animation'
import { defineComponentMetadata } from '@/components/define'
let playerAgentInstance
let playerAgentInstance: PlayerAgent
export const component = defineComponentMetadata({
name: 'playerAutoLight',
displayName: '播放时自动关灯',
urlInclude: allVideoUrls,
tags: [componentsTags.video],
options: {
starAnimation: {
defaultValue: true,
displayName: '启用星光动画',
},
},
description: {
'zh-CN': '在视频播放时自动关灯, 暂停或结束时再自动打开.',
},
entry: async () => {
entry: async ({ settings }) => {
const { isEmbeddedPlayer } = await import('@/core/utils')
const { lightOn, lightOff } = await import('@/components/video/player-light')
if (isEmbeddedPlayer()) {
return
}
const makeLightOn = () => {
lightOn()
StarAnim(false)
}
const makeLightOff = () => {
lightOff()
if (settings.options.starAnimation) {
StarAnim(true)
}
}
videoChange(async () => {
if (playerAgentInstance != null) {
const oldVideo = await playerAgentInstance.query.video.element()
oldVideo.removeEventListener('ended', lightOn)
oldVideo.removeEventListener('pause', lightOn)
oldVideo.removeEventListener('play', lightOff)
oldVideo.removeEventListener('ended', makeLightOn)
oldVideo.removeEventListener('pause', makeLightOn)
oldVideo.removeEventListener('play', makeLightOff)
}
playerAgentInstance = playerAgent
const video = await playerAgentInstance.query.video.element()
if (playerAgentInstance.isAutoPlay()) {
lightOff()
makeLightOff()
}
video.addEventListener('ended', lightOn)
video.addEventListener('pause', lightOn)
video.addEventListener('play', lightOff)
video.addEventListener('ended', makeLightOn)
video.addEventListener('pause', makeLightOn)
video.addEventListener('play', makeLightOff)
})
},
})

View File

@ -444,6 +444,7 @@ export class ExtendSpeedComponent extends EntrySpeedComponent<Options> {
setTimeout(() => this.forceUpdateStyle(value))
}
// eslint-disable-next-line class-methods-use-this
protected readonly filterNativeSpeed =
() =>
({ subscribe, next }: PublishContext<number>) => {

View File

@ -9,7 +9,6 @@ export const pack: Package = {
'downloadSubtitle',
'downloadDanmaku',
'downloadAudio',
'downloadLiveRecords',
],
}

View File

@ -0,0 +1 @@
在输入音频的 au 号或播放列表的 am 号时, 提供对应的跳转选项.

View File

@ -0,0 +1,36 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.audioSearch',
displayName: '搜索栏 - 音频跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'audioSearchProvider',
getActions: async input => {
const { match, type, id, indexer } = matchInput(input, /^(a[um])(\d+)$/)
if (!match) {
return []
}
const json = await getJson(
type === 'am'
? `https://www.bilibili.com/audio/music-service-c/web/menu/info?sid=${id}`
: `https://www.bilibili.com/audio/music-service-c/web/song/info?sid=${id}`,
)
const { title } = lodash.get(json, 'data', {})
return [
createLinkAction({
name: title,
description: type === 'am' ? '播放列表跳转' : '音频跳转',
link: `https://www.bilibili.com/audio/${indexer}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1 @@
在输入番剧的 ss 号 / ep 号, 或番剧详情的 md 号时, 提供对应的跳转选项.

View File

@ -0,0 +1,60 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.bangumiSearch',
displayName: '搜索栏 - 番剧跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'bangumiSearchProvider',
getActions: async input => {
const { match, type, id, indexer } = matchInput(input, /^(md|ss|ep)(\d+)$/)
if (!match) {
return []
}
if (type === 'md') {
const json = await getJson(`https://api.bilibili.com/pgc/review/user?media_id=${id}`)
const { title } = lodash.get(json, 'result.media', {})
return [
createLinkAction({
name: title,
description: '番剧详情跳转',
link: `https://www.bilibili.com/bangumi/media/${indexer}`,
indexer,
}),
]
}
if (type === 'ep') {
const json = await getJson(`https://api.bilibili.com/pgc/view/web/season?ep_id=${id}`)
const episodes = lodash.get(json, 'result.episodes', []) as {
id: number
share_copy: string
}[]
const episode = episodes.find(e => e.id.toString() === id)
return [
createLinkAction({
name: episode?.share_copy,
description: '番剧跳转',
link: `https://www.bilibili.com/bangumi/play/${indexer}`,
indexer,
}),
]
}
const json = await getJson(`https://api.bilibili.com/pgc/view/web/season?season_id=${id}`)
const { title } = lodash.get(json, 'result', {})
return [
createLinkAction({
name: title,
description: '番剧跳转',
link: `https://www.bilibili.com/bangumi/play/${indexer}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -0,0 +1,34 @@
export const matchInput = (input: string, pattern: RegExp) => {
const match = input.match(pattern)
if (!match) {
return {}
}
const type = match[1]
const id = match[2]
const indexer = `${type}${id}`
return {
match,
type,
id,
indexer,
}
}
export const createLinkAction = (input: {
name: string
description?: string
indexer: string
link: string
}) => {
const { name, description, indexer, link } = input
return {
name: name || indexer,
icon: 'mdi-open-in-new',
indexer,
description,
action: () => {
window.open(link, '_blank')
},
order: 0,
}
}

View File

@ -0,0 +1 @@
在输入专栏的 cv 号或专栏文集的 rl 号时, 提供对应的跳转选项.

View File

@ -0,0 +1,46 @@
import type { LaunchBarActionProvider } from '@/components/launch-bar/launch-bar-action'
import type { PluginMetadata } from '@/plugins/plugin'
import { getJson } from '@/core/ajax'
import { createLinkAction, matchInput } from '../common'
export const plugin: PluginMetadata = {
name: 'launchBar.actions.cvSearch',
displayName: '搜索栏 - 专栏跳转',
async setup({ addData }) {
addData('launchBar.actions', (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'cvSearchProvider',
getActions: async input => {
const { match, type, id, indexer } = matchInput(input, /^(cv|rl)(\d+)$/)
if (!match) {
return []
}
if (type === 'rl') {
const json = await getJson(
`https://api.bilibili.com/x/article/list/web/articles?id=${id}`,
)
const { name } = lodash.get(json, 'data.list', {})
return [
createLinkAction({
name,
description: '文集跳转',
link: `https://www.bilibili.com/read/readlist/${indexer}`,
indexer,
}),
]
}
const json = await getJson(`https://api.bilibili.com/x/article/viewinfo?id=${id}`)
const { title } = lodash.get(json, 'data', {})
return [
createLinkAction({
name: title,
description: '专栏跳转',
link: `https://www.bilibili.com/read/${indexer}`,
indexer,
}),
]
},
})
})
},
}

View File

@ -16,8 +16,16 @@ export const plugin: PluginMetadata = {
},
}
})
addData('keymap.presets', (presetBase: Record<string, string>) => {
presetBase.toggleDanmakuList = 'shift d'
})
addData(
'keymap.presets',
(
presetBase: Record<string, string>,
builtInPresets: Record<string, Record<string, string>>,
) => {
presetBase.toggleDanmakuList = 'shift d'
builtInPresets.HTML5Player.toggleDanmakuList = ''
builtInPresets.PotPlayer.toggleDanmakuList = ''
},
)
},
}

View File

@ -0,0 +1,22 @@
import { KeyBindingAction } from 'registry/lib/components/utils/keymap/bindings'
import { PluginMetadata } from '@/plugins/plugin'
import { toggleLight } from '@/components/video/player-light'
export const plugin: PluginMetadata = {
name: 'keymap.actions.togglePlayerLight',
displayName: '快捷键扩展 - 开关灯',
description: '在快捷键的动作列表里添加一个 "开关灯".',
setup: ({ addData }) => {
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
actions.togglePlayerLight = {
displayName: '开关灯',
run: async () => {
toggleLight()
},
}
})
addData('keymap.presets', (presetBase: Record<string, string>) => {
presetBase.togglePlayerLight = 'shift l'
})
},
}

View File

@ -17,8 +17,17 @@ export const plugin: PluginMetadata = {
},
}
})
addData('keymap.presets', (presetBase: Record<string, string>) => {
presetBase.toggleSubtitle = 'shift c'
})
addData(
'keymap.presets',
(
presetBase: Record<string, string>,
builtInPresets: Record<string, Record<string, string>>,
) => {
presetBase.toggleSubtitle = 'shift c'
builtInPresets.YouTube.toggleSubtitle = 'c'
builtInPresets.YouTube.coin = ''
builtInPresets.PotPlayer.toggleSubtitle = 'alt h'
},
)
},
}

View File

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

View File

@ -34,6 +34,11 @@ export const init = async () => {
compatibilityPatch()
})
if (unsafeWindow.bangumi_area_limit_hack) {
console.log('BALH detected, Bilibili Evolved is disabled.')
return
}
const { coreApis, externalApis } = await import('@/core/core-apis')
unsafeWindow.bilibiliEvolved = externalApis
/** sand-boxed window, safe to use original name */

View File

@ -0,0 +1,69 @@
<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="onGood">正常</VButton>
<VButton @click="onBad">不正常</VButton>
<VButton type="primary" @click="onAbort">终止</VButton>
</div>
<div class="bisector-dialog-content-text weak">
*可以点击×号关闭对话框刷新页面后会再次询问
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import { VButton } from '@/ui'
export default Vue.extend({
components: { VButton },
props: {
rouge: {
type: Number,
required: true,
},
onAbort: {
type: Function,
default: none,
},
onGood: {
type: Function,
default: none,
},
onBad: {
type: Function,
default: none,
},
},
})
</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>

View 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 Vue from 'vue'
import { VIcon } from '@/ui'
export default Vue.extend({
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>

View File

@ -0,0 +1,54 @@
<template>
<div>
<div>可能出问题的组件是{{ displayName }}{{ name }}</div>
<div>
<span class="link" @click="restore">恢复原状{{ countdown }} </span>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
data() {
return {
countdown: 30,
userComponent: 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)
},
destroyed() {
clearInterval(this.interval)
},
methods: {
restore() {
clearInterval(this.interval)
this.$emit('restore')
},
},
})
</script>

View File

@ -0,0 +1,198 @@
import { DialogInstance, showDialog } from '@/core/dialog'
import type { Settings } from '@/core/settings/types'
import { Toast } from '@/core/toast'
import { getRandomId, mountVueComponent, delay } from '@/core/utils'
import { useScopedConsole } from '@/core/utils/log'
import type { RecordValue } from '../types'
import type { BisectNext, BisectReturn } from './bisect'
import { bisect } from './bisect'
import { BisectorOptions } from './options'
import ResultToastContent from './ResultToastContent.vue'
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 vm = mountVueComponent<{ userComponent: UserComponent }>(
ResultToastContent,
`#${elementId}`,
)
vm.userComponent = value.target
vm.$on('restore', () => {
stop()
})
} 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)
}

View 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

View 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()
},
},
],
})
})
},
},
})

View File

@ -0,0 +1,31 @@
import { defineOptionsMetadata, 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>

View File

@ -4,6 +4,7 @@ import { component as LaunchBar } from './launch-bar'
import { component as I18n } from './i18n'
import { component as AutoUpdate } from './auto-update'
import { component as NotifyNewVersion } from './notify-new-version'
import { component as Bisector } from './bisector'
export const getBuiltInComponents = (): ComponentMetadata[] => [
SettingsPanel,
@ -11,6 +12,7 @@ export const getBuiltInComponents = (): ComponentMetadata[] => [
I18n,
AutoUpdate,
NotifyNewVersion,
Bisector,
]
export const isBuiltInComponent = (name: string) =>

View File

@ -41,8 +41,13 @@ const getText = (dynamicModule: any, cardType: FeedsCardType) => {
case feedsCardTypes.column:
case feedsCardTypes.video: {
const target = major.archive ?? major.pgc ?? major.article
const { title, desc } = target
typeText = combineText(title, desc)
if (target) {
const { title, desc } = target
typeText = combineText(title, desc)
} else if (major.opus) {
const { title, summary } = major.opus
typeText = combineText(title, summary.text)
}
break
}
}

View File

@ -12,11 +12,15 @@ export class Translator {
static map: Map<string, any>
static regex: [RegExp, string][]
// eslint-disable-next-line class-methods-use-this
protected accepts = (node: Node) => node.nodeType === Node.ELEMENT_NODE
// eslint-disable-next-line class-methods-use-this
protected getValue = (node: Node) => node.nodeValue
// eslint-disable-next-line class-methods-use-this
protected setValue = (node: Node, value: string) => {
node.nodeValue = value
}
// eslint-disable-next-line class-methods-use-this
protected getElement = (node: Node) => node as Element
translate(node: Node) {
let value = this.getValue(node)
@ -99,17 +103,23 @@ export class Translator {
}
}
export class TextNodeTranslator extends Translator {
// eslint-disable-next-line class-methods-use-this
accepts = (node: Node) => node.nodeType === Node.TEXT_NODE
// eslint-disable-next-line class-methods-use-this
getElement = (node: Node) => node.parentElement
}
export class TitleTranslator extends Translator {
// eslint-disable-next-line class-methods-use-this
getValue = (node: Node) => (node as Element).getAttribute('title')
// eslint-disable-next-line class-methods-use-this
setValue = (node: Node, value: string) => {
;(node as Element).setAttribute('title', value)
}
}
export class PlaceholderTranslator extends Translator {
// eslint-disable-next-line class-methods-use-this
getValue = (node: Node) => (node as Element).getAttribute('placeholder')
// eslint-disable-next-line class-methods-use-this
setValue = (node: Node, value: string) => {
;(node as Element).setAttribute('placeholder', value)
}

View File

@ -50,10 +50,12 @@ export default Vue.extend({
if (this.config.getSettings) {
this.settings = this.config.getSettings(this.config.item)
}
Toast.mini(this.$refs.removeConfirmTemplate, this.$refs.removeIcon, {
trigger: 'click',
hideOnClick: true,
})
if (this.config.isUserItem) {
Toast.mini(this.$refs.removeConfirmTemplate, this.$refs.removeIcon, {
trigger: 'click',
hideOnClick: true,
})
}
},
methods: {
async removeItem() {

View File

@ -2,10 +2,12 @@
* Switch Options API
* @module src/components/switch-options
*
* ComponentMetadata
* {@link ComponentMetadata}
* Widget Widget
*
* API {@link newSwitchComponentWrapper}
* 使 {@link newSwitchComponentWrapper}
*
* {@link SwitchOptionsOfMetadata} {@link SwitchOptionsOfSwitchMetadata} options
*/
import { getComponentSettings, addComponentListener } from '@/core/settings'
@ -39,14 +41,14 @@ export type SwitchItemsMetadata<S extends string> = {
}
/**
* SwitchItemsMetadata
* {@link SwitchItemsMetadata}
*/
export const defineSwitchItemsMetadata = <S extends string>(
c: SwitchItemsMetadata<S>,
): SwitchItemsMetadata<S> => c
/**
* API 使 {@link defineSwitchMetadata}
* API 使 {@link defineSwitchMetadata}
*/
export interface SwitchMetadata<N extends string, S extends string> {
/**
@ -82,7 +84,7 @@ export interface SwitchMetadata<N extends string, S extends string> {
}
/**
* {@link SwitchMetadata}
* {@link SwitchMetadata}
*/
export const defineSwitchMetadata = <N extends string, S extends string>(
c: SwitchMetadata<N, S>,
@ -113,7 +115,7 @@ export interface SwitchMetadataOption<N extends string, S extends string> {
}
/**
* SwitchMetadataOption
* {@link SwitchMetadataOption}
*/
const newSwitchMetadataOption = <N extends string, S extends string>(
metadata: SwitchMetadata<N, S>,
@ -151,7 +153,7 @@ export type SwitchOptions<O extends UnknownOptions, N extends string, S extends
} & SwitchItemOptions<S>
/**
* SwitchOptions
* {@link SwitchOptions}
*/
export type SwitchOptionsOfSwitchMetadata<
O extends UnknownOptions,
@ -159,7 +161,7 @@ export type SwitchOptionsOfSwitchMetadata<
> = C extends SwitchMetadata<infer N, infer S> ? SwitchOptions<O, N, S> : never
/**
* SwitchOptions
* {@link SwitchOptions}
*/
export type SwitchOptionsOfMetadata<
M extends OptionsMetadata,
@ -178,7 +180,7 @@ export type SwitchOptionsMetadata<
/**
* `options` API
*
* `options`
* `options`
*/
type SwitchOptionsMetadataExtender<S extends string> = <O extends UnknownOptions, N extends string>(
options: OptionsMetadata<O>,
@ -267,14 +269,12 @@ export type SwitchComponentMetadata<
> = ComponentMetadata<SwitchOptions<O, N, S>>
/**
*
*
*
* 使 'switch-' option
*
* Widget Widget
* Widget options
*
* `entry`
*
* - options {@link SwitchOptions}
* - Widget Widget Widget options
* - `entry`
*
*
*
@ -288,9 +288,7 @@ export type SwitchComponentWrapper<N extends string, S extends string> = <O exte
/**
* {@link SwitchComponentWrapper}
*
* 使 {@link defineSwitchMetadata}
*
* @param metadata -
* @param metadata - 使 {@link defineSwitchMetadata}
* @returns
*/
export const newSwitchComponentWrapper = <N extends string, S extends string>(

View File

@ -216,3 +216,6 @@ export interface ComponentMetadata<O extends UnknownOptions = UnknownOptions>
/** 用户组件的非函数基本信息, 用于直接保存为 JSON */
export type UserComponentMetadata = Omit<ComponentMetadata, keyof FunctionalMetadata>
/** 推断 Record 的 Value 类型 */
export type RecordValue<R> = R extends Record<any, infer V> ? V : never

View File

@ -1,6 +1,8 @@
import { componentToSettings } from '@/core/settings'
import { isBuiltInComponent } from './built-in-components'
import { ComponentMetadata, componentsMap } from './component'
import * as bisector from './bisector/api'
import { BisectorOptions } from './bisector/options'
/**
*
@ -132,3 +134,16 @@ export const toggleComponent = async (nameOrDisplayName: string) => {
const { displayName } = userComponent.metadata
return `${enabled ? '开启' : '关闭'}组件'${displayName}', 可能需要刷新后才能生效`
}
/**
*
*
* @param options
* @returns
*/
export const bisectComponent = async (options?: BisectorOptions) => {
if (options) {
bisector.setOptions(options)
}
return bisector
}

View File

@ -70,8 +70,13 @@ export abstract class PlayerAgent {
}
/** true 开灯false 关灯 */
async toggleLight(on: boolean) {
async toggleLight(on?: boolean) {
const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement
// 无指定参数, 直接 toggle
if (on === undefined) {
checkbox.click()
return
}
// 关灯状态 && 要开灯 -> 开灯
checkbox.checked && on && checkbox.click()
// 开灯状态 && 要关灯 -> 关灯

View File

@ -5,7 +5,7 @@ import { playerAgent } from './player-agent'
// let initialized = false
const setLight = (on: boolean) => {
const setLight = (on?: boolean) => {
if (!playerUrls.some(url => matchUrlPattern(url))) {
return none
}
@ -27,3 +27,4 @@ const setLight = (on: boolean) => {
export const lightOn = setLight(true)
export const lightOff = setLight(false)
export const toggleLight = setLight()

View File

@ -1,6 +1,7 @@
import * as ajax from '@/core/ajax'
import * as cdnTypes from '@/core/cdn-types'
import * as download from '@/core/download'
import * as dialog from '@/core/dialog'
import * as externalInput from '@/core/external-input'
import * as filePicker from '@/core/file-picker'
import * as installFeature from '@/core/install-feature'
@ -40,6 +41,7 @@ export const coreApis = {
ajax,
cdnTypes,
download,
dialog,
externalInput,
filePicker,
installFeature,
@ -80,6 +82,7 @@ export const externalApis = {
ajax,
...cdnTypes,
...download,
...dialog,
...externalInput,
...filePicker,
...installFeature,

View File

@ -10,7 +10,7 @@ export interface DialogInputs {
}
export interface DialogInstance extends Required<DialogInputs> {
open: boolean
close: Promise<void>
close(): Promise<void>
closeListeners: (() => void)[]
}
export const showDialog = (inputs: DialogInputs) => {

View File

@ -1,6 +1,9 @@
export class LoadFeatureCodeError extends Error {}
interface CodeSandbox {
/**
* feature
*/
type CodeSandbox = {
/**
*
*
@ -15,11 +18,11 @@ interface CodeSandbox {
}
/**
*
* feature
*
* @returns
* @returns `CodeSandbox`
*/
const getSandbox = lodash.once((): CodeSandbox => {
const createCodeSandbox = (): CodeSandbox => {
// 需要被注入到 `sandbox` 中的键值对
const injection = new Map([
// 加固,防止逃逸
@ -55,8 +58,9 @@ const getSandbox = lodash.once((): CodeSandbox => {
return [exported, returned]
},
}
})
}
let staticCodeSandbox: CodeSandbox | undefined
/**
* feature (component, plugin, style)
*
@ -69,14 +73,15 @@ const getSandbox = lodash.once((): CodeSandbox => {
*
* `use strict` feature
*
* `window`访 `unsafeWindow`
* `window`访 `unsafeWindow`
*
* @param code -
* @returns
* @returns feature
* @throws {@link LoadFeatureCodeError}
*
*/
export const loadFeatureCode = (code: string): unknown => {
const [exported, returned] = getSandbox().run(code)
staticCodeSandbox || (staticCodeSandbox = createCodeSandbox())
const [exported, returned] = staticCodeSandbox.run(code)
return exported || returned
}

View File

@ -49,8 +49,7 @@ export default Vue.extend({
async mounted() {
await this.$nextTick()
const appendTarget = containerMap[this.container]
this.toast = createMiniToast(this.message, this.$refs.content, {
content: this.$refs.toast,
this.toast = createMiniToast(this.$refs.toast, this.$refs.content, {
placement: this.placement,
showOnCreate: this.show,
trigger: 'mouseenter focusin',

View File

@ -407,6 +407,7 @@ export class DoubleClickEvent {
singleClickHandler: (e: MouseEvent) => void = none
private clickedOnce = false
// eslint-disable-next-line class-methods-use-this
private readonly stopPropagationHandler = (e: MouseEvent) => {
e.stopImmediatePropagation()
}
@ -543,6 +544,9 @@ export const retrieveImageUrl = (element: HTMLElement) => {
url = element.getAttribute('data-src')
} else if (element instanceof HTMLImageElement) {
url = element.src
} else if (dq(element, 'picture img')) {
const image = dq(element, 'picture img') as HTMLImageElement
url = image.src
} else {
const { backgroundImage } = element.style
if (!backgroundImage) {

View File

@ -1,5 +1,6 @@
import { LaunchBarActionProvider, LaunchBarAction } from '@/components/launch-bar/launch-bar-action'
import { PluginMetadata } from '../plugin'
import { IdSearchProvider } from './types'
const getCopyItem = async (name: string, id: string, original: string) => {
const item: LaunchBarAction = {
@ -17,78 +18,82 @@ const getCopyItem = async (name: string, id: string, original: string) => {
}
return [item]
}
const idMatches = [
const idMatches: IdSearchProvider[] = [
{
pattern: /^av([\d]+)$/i,
name: (match: RegExpMatchArray) => `av${match[1]}`,
badge: 'av号跳转',
link: (match: RegExpMatchArray) => `https://www.bilibili.com/av${match[1]}`,
extend: async (match: RegExpMatchArray) => {
getActions: async match => {
const { getJsonWithCredentials } = await import('@/core/ajax')
const json = await getJsonWithCredentials(
`https://api.bilibili.com/x/web-interface/view?aid=${match[1]}`,
)
const bv = lodash.get(json, 'data.bvid', null)
if (bv === null) {
return []
const data = lodash.get(json, 'data', {})
const { bvid, title } = data
const indexer = `av${match[1]}`
return {
name: title || indexer,
description: 'av号跳转',
indexer,
link: `https://www.bilibili.com/av${match[1]}`,
extraActions: bvid ? await getCopyItem('BV号', bvid, indexer) : [],
}
return getCopyItem('BV号', bv, `av${match[1]}`)
},
},
{
pattern: /^bv([\da-zA-Z]+)$/i,
name: (match: RegExpMatchArray) => `BV${match[1]}`,
badge: 'BV号跳转',
link: (match: RegExpMatchArray) => `https://www.bilibili.com/BV${match[1]}`,
extend: async (match: RegExpMatchArray) => {
getActions: async match => {
const { getJsonWithCredentials } = await import('@/core/ajax')
const json = await getJsonWithCredentials(
`https://api.bilibili.com/x/web-interface/view?bvid=${match[1]}`,
)
const av = lodash.get(json, 'data.aid', null)
if (av === null) {
return []
const data = lodash.get(json, 'data', {})
const { aid, title } = data
const indexer = `BV${match[1]}`
return {
name: title || indexer,
description: 'BV号跳转',
indexer,
link: `https://www.bilibili.com/BV${match[1]}`,
extraActions: aid ? await getCopyItem('av号', `av${aid}`, indexer) : [],
}
return getCopyItem('av号', `av${av}`, `BV${match[1]}`)
},
},
]
export const plugin: PluginMetadata = {
name: 'launchBar.actions.IDSearch',
displayName: 'ID搜索快速跳转',
displayName: '搜索栏 - 视频跳转',
async setup() {
const { addData } = await import('../data')
const { LaunchBarActionProviders } = await import('@/components/launch-bar/launch-bar-action')
addData(LaunchBarActionProviders, (providers: LaunchBarActionProvider[]) => {
providers.push({
name: 'IDSearchProvider',
// getEnterAction: input => {
// for (const it of idMatches) {
// const match = input.match(it.pattern)
// if (match) {
// return () => window.open(it.link(match), '_blank')
// }
// }
// return null
// },
getActions: async input => {
const results: LaunchBarAction[] = []
for (const it of idMatches) {
const match = input.match(it.pattern)
if (match) {
results.push({
name: it.name(match),
icon: 'mdi-open-in-new',
description: it.badge,
action: () => {
window.open(it.link(match), '_blank')
const {
name,
description = '',
indexer,
link,
extraActions = [],
} = await it.getActions(match)
results.push(
{
name,
icon: 'mdi-open-in-new',
indexer,
description,
action: () => {
window.open(link, '_blank')
},
order: 0,
},
order: 0,
})
if (it.extend) {
const actions = await it.extend(match)
results.push(...actions)
}
...extraActions,
)
}
}
return results

View File

@ -0,0 +1,19 @@
import type { LaunchBarAction } from '@/components/launch-bar/launch-bar-action'
export interface IdSearchProvider {
/** 匹配的正则 */
pattern: RegExp
/** 匹配时运行此函数获取结果 */
getActions: (match: RegExpMatchArray) => Promise<{
/** 显示的选项名称 */
name: string
/** 选择此选项后打开的链接 */
link: string
/** 提供给搜索栏过滤的关键词 */
indexer: string
/** 显示的选项描述 */
description?: string
/** 额外提供一些自定义的 LaunchBarAction */
extraActions?: LaunchBarAction[]
}>
}

View File

@ -8,7 +8,8 @@
<VIcon :size="48" icon="mdi-close"></VIcon>
</div>
<a target="_blank" class="copy-link image-viewer-icon" title="复制原链接" @click="copyLink()">
<VIcon :size="48" icon="mdi-link"></VIcon>
<VIcon v-if="copiedTimer" :size="48" icon="mdi-check"></VIcon>
<VIcon v-else :size="48" icon="mdi-link"></VIcon>
</a>
<a target="_blank" class="new-tab image-viewer-icon" title="在新标签页打开" @click="newTab()">
<VIcon :size="48" icon="mdi-open-in-new"></VIcon>
@ -49,6 +50,7 @@ export default Vue.extend({
open: false,
blobUrl: '',
keyHandler: null,
copiedTimer: 0,
}
},
watch: {
@ -85,6 +87,12 @@ export default Vue.extend({
methods: {
async copyLink() {
await navigator.clipboard.writeText(this.image)
if (this.copiedTimer) {
window.clearTimeout(this.copiedTimer)
}
this.copiedTimer = window.setTimeout(() => {
this.copiedTimer = 0
}, 2000)
},
newTab() {
window.open(this.image, '_blank')

View File

@ -84,18 +84,24 @@ export default Vue.extend({
},
},
data() {
const tabs = this.tabs as TabMappings
return {
selectedTab: (this.tabs.find((t: TabMapping) => t.name === this.defaultTab) ??
this.tabs[0]) as TabMapping,
selectedTabName:
tabs.find((t: TabMapping) => t.name === this.defaultTab)?.name ?? tabs[0].name,
}
},
computed: {
selectedTab() {
return this.tabs.find((t: TabMapping) => t.name === this.selectedTabName)
},
},
mounted() {
this.$emit('change', this.selectedTab.activeLink)
},
methods: {
selectTab(tab: TabMapping) {
if (this.selectedTab !== tab) {
this.selectedTab = tab
if (this.selectedTabName !== tab.name) {
this.selectedTabName = tab.name
tab.count = 0
this.$emit('change', this.selectedTab.activeLink)
} else if (tab.activeLink) {