diff --git a/.eslintignore b/.eslintignore index 14e38d70ab..c0ba0a5b9b 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,6 @@ packages/ typings/ -dist/ +**/dist/ dev/ node_modules/ !.github-json/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e03557eee9..d95254c066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,62 @@ # 更新日志 +## v2.9.1-preview +`2024-08-15` + +包含 [v2.9.1](https://github.com/the1812/Bilibili-Evolved/releases/tag/v2.9.1) 的所有更新内容. + +✨新增 +- `查看封面` 可以为 aria2 输出提供直接的封面下载. (PR #4798 by [Oxygenくん](https://github.com/oxygenkun)) +- 新增组件 `保存视频元数据`. (PR #4840 by [WakelessSloth56](https://github.com/WakelessSloth56)) +> - 保存视频元数据为 [FFMETADATA](https://ffmpeg.org/ffmpeg-formats.html#Metadata-2) 格式 +> - 使用组件 `下载视频` 时指定 `WASM` 输出方式(插件 `下载视频 - WASM 混流输出`)可选择是否直接混流入输出文件。 +> - 保存视频章节为 OGM 格式 (https://github.com/the1812/Bilibili-Evolved/discussions/2069#discussioncomment-10110916) + +- `简化首页` 支持隐藏轮播图. (PR #4852 by [Lime](https://github.com/Liumingxun)) +- 新增组件 `添加直播间用户超链接`. (PR #4856 by [Light_Quanta](https://github.com/LightQuanta)) +> 网页版直播间右上角的房间观众和大航海界面的用户列表只可查看用户名,不可进行点击。该组件为用户头像和用户名称处添加点击效果,允许通过点击直接查看用户空间。 + +- 插件 `下载视频 - WASM 混流输出` 支持并行下载库和音视频流. (PR #4864 by [WakelessSloth56](https://github.com/WakelessSloth56)) + +## v2.9.1 +`2024-08-15` + +
+正式版用户将获得 v2.9.0-preview 的所有改动 (新功能以及一项废弃), 点击展开查看 + +✨新增 +- `简化直播间` 支持屏蔽推荐直播间. (#4787) +- 新增功能 `删除直播马赛克遮罩` (#4634, PR #4814) +> 删除观看直播时某些分区的马赛克遮罩. + +- `启用视频截图` 截出来的图支持直接复制. (此功能需要 Firefox 127 版本以上) (#4806) +- `图片批量导出`, `下载视频` 支持更多变量, 详情可在更新组件后查看设置中的说明: (#3852) + - 动态 ID, 用户 ID, 动态发布时间, 被转发动态相关数据 + - 专栏 cv 号, 专栏发布时间 + - (仅对批量视频下载 (分P / 合集) 有效) up 主名称, up 主 ID, 视频发布时间 +- `自定义顶栏` 的稍后再看和历史面板现在始终显示 "已观看" 状态. (#4346) +- `自定义顶栏` 的稍后再看, 收藏和历史面板优化了分 P 数和观看进度的展示, 详见[此处](https://github.com/the1812/Bilibili-Evolved/discussions/1866#discussioncomment-10075203). (#1866) + +🗑️废弃 +- 删除 `下载视频` 的 Toast 输出方式. + +
+ +🐛修复 +- 部分修复 `简化评论区` 在新版评论区下失效. (#4843) + - 头像框目前还没找到比较好的方式隐藏, 暂不支持. + - 时间由于 Shadow DOM 限制, 无法再挪到右上角了, `装扮 & 时间` 只对装扮有效. + - 样式实现依赖 [:host-context](https://developer.mozilla.org/en-US/docs/Web/CSS/:host-context), 因此目前还不支持 Firefox. +- 修复 `删除视频弹窗` 和 `禁用特殊弹幕样式` 在新版播放器下失效. (#4843, #4823, PR #4839 by [festoney8](https://github.com/festoney8)) +- 修复 `快捷键扩展` 的部分操作和 `启用弹幕空降` 在新版播放器下失效. + +☕开发者相关 +- 新增 Shadow DOM 系列 API (`src/core/shadow-dom.ts`), 用于处理 Shadow DOM 相关的逻辑. + - `ShadowDomObserver`: 持续观测页面上的所有 Shadow DOM. + - `ShadowDomStyles`: 支持将样式注入页面, 包含所有 Shadow DOM 内部. +- `MutationObserver` 相关的 API 支持使用 `Node` 类型作为目标. + ## v2.9.0-preview `2024-07-19` diff --git a/doc/donate.md b/doc/donate.md index 3ec40d9f35..73930da0f7 100644 --- a/doc/donate.md +++ b/doc/donate.md @@ -26,9 +26,13 @@ https://afdian.net/@the1812?tab=sponsor + | 时间 | 用户名 | 单号后4位 | 金额 | | ------------------- | --------------------- | --------- | ------- | +| 2024.08.15 10:18:07 | *9 | 1653 | ¥10.00 | +| 2024.07.30 13:40:22 | Z*n | 2215 | ¥5.00 | +| 2024.07.24 09:38:46 | H*g | 7776 | ¥5.00 | | 2024.07.16 20:57:01 | *h | 4344 | ¥20.00 | | 2024.06.10 11:05:25 | *兽 | 6240 | ¥5.00 | | 2024.06.01 14:30:08 | R*0 | 8974 | ¥50.00 | diff --git a/doc/features/features.json b/doc/features/features.json index eb3ee9ee81..8d18298306 100644 --- a/doc/features/features.json +++ b/doc/features/features.json @@ -143,6 +143,14 @@ "fullRelativePath": "../../registry/dist/components/live/home-mute.js", "fullAbsolutePath": "registry/dist/components/live/home-mute.js" }, + { + "type": "component", + "name": "liveroomUsernameLink", + "displayName": "添加直播间用户超链接", + "description": "by [@Light_Quanta](https://github.com/LightQuanta)\n\n为直播间的房间观众和大航海界面的用户列表添加可以点击的超链接", + "fullRelativePath": "../../registry/dist/components/live/liveroom-username-link.js", + "fullAbsolutePath": "registry/dist/components/live/liveroom-username-link.js" + }, { "type": "component", "name": "originalLiveroom", @@ -735,6 +743,14 @@ "fullRelativePath": "../../registry/dist/components/video/full-episode-title.js", "fullAbsolutePath": "registry/dist/components/video/full-episode-title.js" }, + { + "type": "component", + "name": "saveVideoMetadata", + "displayName": "保存视频元数据", + "description": "by [@WakelessSloth56](https://github.com/WakelessSloth56),[@LainIO24](https://github.com/LainIO24)\n\n保存视频元数据(标题、描述、UP、章节等)", + "fullRelativePath": "../../registry/dist/components/video/metadata.js", + "fullAbsolutePath": "registry/dist/components/video/metadata.js" + }, { "type": "component", "name": "outerWatchlater", diff --git a/doc/features/features.md b/doc/features/features.md index 7e143954e2..ee4dcfa8b1 100644 --- a/doc/features/features.md +++ b/doc/features/features.md @@ -179,6 +179,17 @@ by [@TimmyOVO](https://github.com/TimmyOVO) 禁止直播首页的推荐直播间自动开始播放. +### [添加直播间用户超链接](../../registry/dist/components/live/liveroom-username-link.js) +`liveroomUsernameLink` + +**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/live/liveroom-username-link.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/live/liveroom-username-link.js) + +**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/live/liveroom-username-link.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/live/liveroom-username-link.js) + +by [@Light_Quanta](https://github.com/LightQuanta) + +为直播间的房间观众和大航海界面的用户列表添加可以点击的超链接 + ### [返回原版直播间](../../registry/dist/components/live/original.js) `originalLiveroom` @@ -1022,6 +1033,17 @@ by [@kdxcxs](https://github.com/kdxcxs) 打开 `展开选集列表` 时, 在选集区域的标题上按住 Alt 键点击可以临时切换展开/收起选集列表. +### [保存视频元数据](../../registry/dist/components/video/metadata.js) +`saveVideoMetadata` + +**jsDelivr:** [`Stable`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/video/metadata.js) / [`Preview`](https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@preview/registry/dist/components/video/metadata.js) + +**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/metadata.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/metadata.js) + +by [@WakelessSloth56](https://github.com/WakelessSloth56),[@LainIO24](https://github.com/LainIO24) + +保存视频元数据(标题、描述、UP、章节等) + ### [外置稍后再看](../../registry/dist/components/video/outer-watchlater.js) `outerWatchlater` diff --git a/registry/lib/components/live/liveroom-username-link/index.ts b/registry/lib/components/live/liveroom-username-link/index.ts new file mode 100644 index 0000000000..a55bf1a1f2 --- /dev/null +++ b/registry/lib/components/live/liveroom-username-link/index.ts @@ -0,0 +1,104 @@ +import { defineComponentMetadata } from '@/components/define' +import { select } from '@/core/spin-query' +import { delay } from '@/core/utils' + +const processed = new WeakSet() + +const entry = async () => { + const userInfoBar = await select('#rank-list-ctnr-box', { + queryInterval: 500, + }) + + const observer = new MutationObserver(async () => { + // 舰长列表 + const guardNodes = [...document.querySelectorAll('webcomponent-userinfo')] + let subtreeLoaded = false + + for (const node of guardNodes) { + if (processed.has(node)) { + continue + } + + // eslint-disable-next-line no-underscore-dangle + const { uid } = (node as any).__vue__.source.uinfo + + if (!subtreeLoaded) { + // 等待子节点创建 + while ( + node.shadowRoot.querySelector('a') === null || + node.shadowRoot.querySelector('.faceBox') === null + ) { + await delay(100) + } + subtreeLoaded = true + } + + const aNode = node.shadowRoot.querySelector('a') + const avatarNode: HTMLDivElement = node.shadowRoot.querySelector('.faceBox') + + aNode.href = `https://space.bilibili.com/${uid}` + aNode.style.textDecoration = 'none' + + avatarNode.style.cursor = 'pointer' + avatarNode.addEventListener('click', () => { + window.open(`https://space.bilibili.com/${uid}`) + }) + processed.add(node) + + // const name = a.innerText + // console.log(`已为舰长${name}(UID: ${uid})添加超链接`) + } + + // 观众列表 + const spectorNodes = [...document.querySelectorAll('.gift-rank-list-item')] + for (const node of spectorNodes) { + if (processed.has(node)) { + continue + } + + // 观众列表元素似乎会原地更新,不能直接预先获取UID并绑定,这里通过点击时获取父元素动态读取UID + + // 名称 + const nameNode: HTMLDivElement = node.querySelector('.common-nickname-wrapper .name') + nameNode.style.cursor = 'pointer' + nameNode.addEventListener('click', () => { + // eslint-disable-next-line no-underscore-dangle + const { uid } = (nameNode as any).parentNode.parentNode.parentNode.parentNode.__vue__.source + window.open(`https://space.bilibili.com/${uid}`) + }) + + // 头像 + const avatarNode: HTMLDivElement = node.querySelector('.face') + avatarNode.style.cursor = 'pointer' + avatarNode.addEventListener('click', () => { + // eslint-disable-next-line no-underscore-dangle + const { uid } = (avatarNode as any).parentNode.parentNode.__vue__.source + window.open(`https://space.bilibili.com/${uid}`) + }) + processed.add(node) + + // const name = nameNode.innerText + // console.log(`已为观众${name}(UID: ${uid})添加超链接`) + } + }) + + observer.observe(userInfoBar, { + childList: true, + subtree: true, + }) +} + +export const component = defineComponentMetadata({ + name: 'liveroomUsernameLink', + author: { + name: 'Light_Quanta', + link: 'https://github.com/LightQuanta', + }, + displayName: '添加直播间用户超链接', + entry, + tags: [componentsTags.live], + urlInclude: [/^https:\/\/live\.bilibili\.com\/\d+/], + description: { + 'zh-CN': '为直播间的房间观众和大航海界面的用户列表添加可以点击的超链接', + }, +}) diff --git a/registry/lib/components/style/simplify/comments/comments-v3.scss b/registry/lib/components/style/simplify/comments/comments-v3.scss new file mode 100644 index 0000000000..b9c428e9a7 --- /dev/null +++ b/registry/lib/components/style/simplify/comments/comments-v3.scss @@ -0,0 +1,59 @@ +@import 'common'; + +$prefix: 'simplifyComments-switch'; + +:host-context(.comment-m-v1) { + &:host(bili-rich-text) { + #contents img, #contents a i { + max-width: 1.4em; + max-height: 1.4em; + } + } + + &:host-context(body.#{$prefix}-replyEditor) { + &:host(bili-comment-box) { + #pub button { + font-size: 14px; + } + } + &:host(bili-checkbox) { + #label { + font-size: 15px; + } + } + &:host(bili-comment-textarea) { + #input { + font-size: 13px; + } + #input, + #input::placeholder { + line-height: normal; + } + } + } + &:host-context(body.#{$prefix}-userLevel):host(bili-comment-user-info) { + #user-level { + display: none; + } + } + &:host-context(body.#{$prefix}-decorateAndTime):host(bili-comment-renderer) { + bili-comment-user-sailing-card { + display: none; + } + } + &:host-context(body.#{$prefix}-fansMedal):host(bili-comment-user-info) { + bili-comment-user-medal { + display: none; + } + } + &:host-context(body.#{$prefix}-subReplyNewLine):host(bili-comment-reply-renderer) { + bili-comment-user-info { + display: block; + } + } + &:host-context(body.#{$prefix}-eventBanner):host(bili-comments-header-renderer) { + bili-comments-notice { + display: none; + } + } +} diff --git a/registry/lib/components/style/simplify/comments/index.ts b/registry/lib/components/style/simplify/comments/index.ts index 2f87ba0265..8efcf7e19a 100644 --- a/registry/lib/components/style/simplify/comments/index.ts +++ b/registry/lib/components/style/simplify/comments/index.ts @@ -50,14 +50,19 @@ export const component = wrapSwitchOptions({ }, true, ) + + const { ShadowDomStyles } = await import('@/core/shadow-dom') + const v3Styles = await import('./comments-v3.scss').then(m => m.default) + const shadowDom = new ShadowDomStyles() + shadowDom.addStyle(v3Styles) }, instantStyles: [ { - name, + name: `${name}v1`, style: () => import('./comments.scss'), }, { - name, + name: `${name}v2`, style: () => import('./comments-v2.scss'), }, ], diff --git a/registry/lib/components/style/simplify/home/home.scss b/registry/lib/components/style/simplify/home/home.scss index 66e28c38fd..b5343ba160 100644 --- a/registry/lib/components/style/simplify/home/home.scss +++ b/registry/lib/components/style/simplify/home/home.scss @@ -1,33 +1,46 @@ body.simplifyHome-switch { + &-carousel { + .recommended-swipe.grid-anchor { + display: none !important; + } + } + &-categories { .z-top-container.has-menu { height: auto !important; min-height: unset !important; } - .bili-header-m > .bili-wrapper { + + .bili-header-m>.bili-wrapper { visibility: hidden !important; height: 18px !important; } + .primary-menu-itnl { visibility: hidden !important; height: 24px !important; padding: 0 !important; } + .bili-header__channel { height: 12px !important; } - .bili-header__channel > * { + + .bili-header__channel>* { display: none !important; } + &.header-v3 .bili-wrapper { padding-top: 8px !important; border-top: none !important; } } + &-trends { .first-screen #reportFirst1 { display: none !important; } + .first-screen .space-between { margin-bottom: 0 !important; } @@ -38,30 +51,36 @@ body.simplifyHome-switch { display: none !important; } } + &-online { .first-screen #reportFirst2 { display: none !important; } } + &-ext-box { .first-screen #reportFirst3 { display: none !important; } } + &-special { #bili_report_spe_rec { display: none !important; } } + &-contact { + .bili-footer .b-footer-wrap, .international-footer { display: none !important; } } + &-elevator { .storey-box .elevator { display: none !important; } } -} +} \ No newline at end of file diff --git a/registry/lib/components/style/simplify/home/index.ts b/registry/lib/components/style/simplify/home/index.ts index f1bddf73aa..b2bd121afd 100644 --- a/registry/lib/components/style/simplify/home/index.ts +++ b/registry/lib/components/style/simplify/home/index.ts @@ -10,6 +10,10 @@ import { mainSiteUrls } from '@/core/utils/urls' const switchMetadata = defineSwitchMetadata({ name: 'simplifyOptions', switches: { + carousel: { + defaultValue: false, + displayName: '轮播图', + }, categories: { defaultValue: false, displayName: '分区栏', @@ -74,15 +78,17 @@ export const component = wrapSwitchOptions(switchMetadata)({ () => dqa('.proxy-box > div'), elements => elements.length > 0 || !isHome, ) - return Object.fromEntries( - categoryElements.map(it => [ - it.id.replace(/^bili_/, ''), - { - displayName: it.querySelector('header .name')?.textContent?.trim() ?? '未知分区', - defaultValue: false, - }, - ]), - ) + return categoryElements + ? Object.fromEntries( + categoryElements.map(it => [ + it.id.replace(/^bili_/, ''), + { + displayName: it.querySelector('header .name')?.textContent?.trim() ?? '未知分区', + defaultValue: false, + }, + ]), + ) + : {} } const skipIds = ['推广'] diff --git a/registry/lib/components/style/special-danmaku/special-danmaku.scss b/registry/lib/components/style/special-danmaku/special-danmaku.scss index dcae902312..0fd85e3dd7 100644 --- a/registry/lib/components/style/special-danmaku/special-danmaku.scss +++ b/registry/lib/components/style/special-danmaku/special-danmaku.scss @@ -1,9 +1,16 @@ body.disable-highlight-danmaku-style { .bili-danmaku-x-high, - .bili-danmaku-x-high-top, + .bili-danmaku-x-high-top { + display: flex !important; + } .bili-dm.bili-high, .b-danmaku-high { display: block !important; + } + .bili-danmaku-x-high, + .bili-danmaku-x-high-top, + .bili-dm.bili-high, + .b-danmaku-high { padding: 0 !important; line-height: 1.125 !important; .bili-danmaku-x-high-icon, diff --git a/registry/lib/components/utils/keymap/actions.ts b/registry/lib/components/utils/keymap/actions.ts index 7a3f6e5701..89c7add0c3 100644 --- a/registry/lib/components/utils/keymap/actions.ts +++ b/registry/lib/components/utils/keymap/actions.ts @@ -136,13 +136,13 @@ export const builtInActions: Record = { coin: { displayName: '投币', run: useClickElement( - '.video-toolbar .coin, .tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2), .video-toolbar-v1 .coin, .toolbar .coin', + '.video-toolbar .coin, .tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2), .video-toolbar-v1 .coin, .toolbar .coin, .video-toolbar-container .video-coin', ), }, favorite: { displayName: '收藏', run: useClickElement( - '.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3), .video-toolbar-v1 .collect', + '.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3), .video-toolbar-v1 .collect, .video-toolbar-container .video-fav', ), }, pause: { @@ -157,7 +157,7 @@ export const builtInActions: Record = { return (context: KeyBindingActionContext) => { const { event } = context const likeButton = dq( - '.video-toolbar .like, .tool-bar .like-info, .video-toolbar-v1 .like, .toolbar .like', + '.video-toolbar .like, .tool-bar .like-info, .video-toolbar-v1 .like, .toolbar .like, .video-toolbar-container .video-like', ) as HTMLSpanElement if (!likeButton) { return false diff --git a/registry/lib/components/video/danmaku/airborne/airborne.scss b/registry/lib/components/video/danmaku/airborne/airborne.scss index bd2bb4cf97..7e5e869462 100644 --- a/registry/lib/components/video/danmaku/airborne/airborne.scss +++ b/registry/lib/components/video/danmaku/airborne/airborne.scss @@ -1,5 +1,6 @@ .bili-dm, -.b-danmaku { +.b-danmaku, +.bili-danmaku-x-dm { &.airborne { text-decoration: underline; cursor: pointer; diff --git a/registry/lib/components/video/danmaku/airborne/index.ts b/registry/lib/components/video/danmaku/airborne/index.ts index ba1bca2ec4..ea07f9fe4b 100644 --- a/registry/lib/components/video/danmaku/airborne/index.ts +++ b/registry/lib/components/video/danmaku/airborne/index.ts @@ -51,7 +51,11 @@ export const component = defineComponentMetadata({ return } const target = e.target as HTMLElement - if (!['b-danmaku', 'bili-dm'].some(token => target.classList.contains(token))) { + if ( + !['b-danmaku', 'bili-dm', 'bili-danmaku-x-dm'].some(token => + target.classList.contains(token), + ) + ) { return } const time = getAirborneTime(target.textContent) diff --git a/registry/lib/plugins/video/download/wasm-output/handler.ts b/registry/lib/plugins/video/download/wasm-output/handler.ts index 3a22090ec5..7d6842e1cf 100644 --- a/registry/lib/plugins/video/download/wasm-output/handler.ts +++ b/registry/lib/plugins/video/download/wasm-output/handler.ts @@ -6,38 +6,38 @@ import { title as pluginTitle } from '.' import type { Options } from '../../../../components/video/download' import { DownloadVideoAction } from '../../../../components/video/download/types' import { FFmpeg } from './ffmpeg' -import { getCacheOrGet, httpGet, toastProgress, toBlobUrl } from './utils' +import { getCacheOrFetch, httpGet, toastProgress, toBlobUrl } from './utils' const ffmpeg = new FFmpeg() async function loadFFmpeg() { const toast = Toast.info('正在加载 FFmpeg', `${pluginTitle} - 初始化`) - await ffmpeg.load({ - workerLoadURL: toBlobUrl( - await getCacheOrGet( - 'ffmpeg-worker', - meta.compilationInfo.altCdn.library.ffmpeg.worker, - toastProgress(toast, '正在加载 FFmpeg Worker'), - ), - 'text/javascript', + + const progress = toastProgress(toast) + const [worker, core, wasm] = await Promise.all([ + getCacheOrFetch( + 'ffmpeg-worker', + meta.compilationInfo.altCdn.library.ffmpeg.worker, + progress(0, '正在加载 FFmpeg Worker'), ), - coreURL: toBlobUrl( - await getCacheOrGet( - 'ffmpeg-core', - meta.compilationInfo.altCdn.library.ffmpeg.core, - toastProgress(toast, '正在加载 FFmpeg Core'), - ), - 'text/javascript', + getCacheOrFetch( + 'ffmpeg-core', + meta.compilationInfo.altCdn.library.ffmpeg.core, + progress(1, '正在加载 FFmpeg Core'), ), - wasmURL: toBlobUrl( - await getCacheOrGet( - 'ffmpeg-wasm', - meta.compilationInfo.altCdn.library.ffmpeg.wasm, - toastProgress(toast, '正在加载 FFmpeg WASM'), - ), - 'application/wasm', + getCacheOrFetch( + 'ffmpeg-wasm', + meta.compilationInfo.altCdn.library.ffmpeg.wasm, + progress(2, '正在加载 FFmpeg WASM'), ), + ]) + + await ffmpeg.load({ + workerLoadURL: toBlobUrl(worker, 'text/javascript'), + coreURL: toBlobUrl(core, 'text/javascript'), + wasmURL: toBlobUrl(wasm, 'application/wasm'), }) + toast.message = '完成!' toast.close() } @@ -53,8 +53,14 @@ async function single( ) { const toast = Toast.info('', `${pluginTitle} - ${pageIndex} / ${totalPages}`) - ffmpeg.writeFile('video', await httpGet(videoUrl, toastProgress(toast, '正在下载视频流'))) - ffmpeg.writeFile('audio', await httpGet(audioUrl, toastProgress(toast, '正在下载音频流'))) + const progress = toastProgress(toast) + const [video, audio] = await Promise.all([ + httpGet(videoUrl, progress(0, '正在下载视频流')), + httpGet(audioUrl, progress(1, '正在下载音频流')), + ]) + + ffmpeg.writeFile('video', video) + ffmpeg.writeFile('audio', audio) const args = ['-i', 'video', '-i', 'audio'] diff --git a/registry/lib/plugins/video/download/wasm-output/utils.ts b/registry/lib/plugins/video/download/wasm-output/utils.ts index 165e3559d2..fb02e15adc 100644 --- a/registry/lib/plugins/video/download/wasm-output/utils.ts +++ b/registry/lib/plugins/video/download/wasm-output/utils.ts @@ -2,13 +2,21 @@ import { Toast } from '@/core/toast' import { formatFileSize, formatPercent } from '@/core/utils/formatters' import { getOrLoad, storeNames } from './database' -type OnProgress = (received: number, length: number) => void +type OnProgress = (received: number, total: number) => void -export function toastProgress(toast: Toast, message: string): OnProgress { - return (r, l) => { - toast.message = `${message}: ${formatFileSize(r)}${ - l > 0 ? ` / ${formatFileSize(l)} @ ${formatPercent(r / l)}` : '' - }` +function formatProgress(received: number, total: number) { + return `${formatFileSize(received)}${ + total > 0 ? ` / ${formatFileSize(total)} @ ${formatPercent(received / total)}` : '' + }` +} + +export function toastProgress(toast: Toast) { + const lines = [] + return (line: number, message: string): OnProgress => { + return (r, l) => { + lines[line] = `${message}: ${formatProgress(r, l)}` + toast.message = lines.join('\n') + } } } @@ -48,7 +56,7 @@ export async function httpGet(url: string, onprogress: OnProgress) { return chunksAll } -export async function getCacheOrGet(key: string, url: string, loading: OnProgress) { +export async function getCacheOrFetch(key: string, url: string, loading: OnProgress) { return getOrLoad(storeNames.cache, key, async () => httpGet(url, loading)) } diff --git a/src/client/common.meta.json b/src/client/common.meta.json index 43f6010a38..0c3a9889f9 100644 --- a/src/client/common.meta.json +++ b/src/client/common.meta.json @@ -1,5 +1,5 @@ { - "version": "2.9.0", + "version": "2.9.1", "author": "Grant Howard, Coulomb-G", "copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)", "license": "MIT", diff --git a/src/core/core-apis.ts b/src/core/core-apis.ts index 9c874e46a9..aa400dfd19 100644 --- a/src/core/core-apis.ts +++ b/src/core/core-apis.ts @@ -17,6 +17,7 @@ import * as spinQuery from '@/core/spin-query' import * as style from '@/core/style' import * as textColor from '@/core/text-color' import * as settings from '@/core/settings' +import * as shadowDom from '@/core/shadow-dom' import * as userInfo from '@/core/user-info' import * as version from '@/core/version' import * as commonUtils from '@/core/utils' @@ -59,6 +60,7 @@ export const coreApis = { userInfo, version, settings, + shadowDom, toast, themeColor, utils: { @@ -99,6 +101,7 @@ export const externalApis = { ...textColor, ...userInfo, ...version, + ...shadowDom, settingsApis: settings, get settings() { return settings.settings diff --git a/src/core/observer.ts b/src/core/observer.ts index 729a28d6bd..a95d561096 100644 --- a/src/core/observer.ts +++ b/src/core/observer.ts @@ -3,17 +3,24 @@ import { select } from './spin-query' import { matchCurrentPage, playerUrls } from './utils/urls' type ObserverTarget = string | Element[] | Element -export const resolveTargets = (target: ObserverTarget) => { +type MutationObserverTarget = string | Node[] | Node +export const resolveTargets = < + T extends ObserverTarget | MutationObserverTarget, + ResultType = T extends ObserverTarget ? Element[] : Node[], +>( + target: T, +): ResultType => { if (typeof target === 'string') { - return dqa(target) + return dqa(target) as ResultType } if (Array.isArray(target)) { - return target + return target as ResultType } - return [target] + return [target] as ResultType } + export const mutationObserve = ( - targets: Element[], + targets: Node[], config: MutationObserverInit, callback: MutationCallback, ) => { @@ -26,7 +33,7 @@ export const mutationObserve = ( * @param target 监听目标 * @param callback 回调函数 */ -export const childList = (target: ObserverTarget, callback: MutationCallback) => +export const childList = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { @@ -40,7 +47,7 @@ export const childList = (target: ObserverTarget, callback: MutationCallback) => * @param target 监听目标 * @param callback 回调函数 */ -export const childListSubtree = (target: ObserverTarget, callback: MutationCallback) => +export const childListSubtree = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { @@ -54,7 +61,7 @@ export const childListSubtree = (target: ObserverTarget, callback: MutationCallb * @param target 监听目标 * @param callback 回调函数 */ -export const attributes = (target: ObserverTarget, callback: MutationCallback) => +export const attributes = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { @@ -68,7 +75,7 @@ export const attributes = (target: ObserverTarget, callback: MutationCallback) = * @param target 监听目标 * @param callback 回调函数 */ -export const attributesSubtree = (target: ObserverTarget, callback: MutationCallback) => +export const attributesSubtree = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { @@ -82,7 +89,7 @@ export const attributesSubtree = (target: ObserverTarget, callback: MutationCall * @param target 监听目标 * @param callback 回调函数 */ -export const characterData = (target: ObserverTarget, callback: MutationCallback) => +export const characterData = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { @@ -97,7 +104,7 @@ export const characterData = (target: ObserverTarget, callback: MutationCallback * @param target 监听目标 * @param callback 回调函数 */ -export const characterDataSubtree = (target: ObserverTarget, callback: MutationCallback) => +export const characterDataSubtree = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { @@ -114,7 +121,7 @@ export const characterDataSubtree = (target: ObserverTarget, callback: MutationC * @param target 监听目标 * @param callback 回调函数 */ -export const allMutationsOn = (target: ObserverTarget, callback: MutationCallback) => +export const allMutationsOn = (target: MutationObserverTarget, callback: MutationCallback) => mutationObserve( resolveTargets(target), { diff --git a/src/core/shadow-dom.ts b/src/core/shadow-dom.ts new file mode 100644 index 0000000000..98c788d0f1 --- /dev/null +++ b/src/core/shadow-dom.ts @@ -0,0 +1,192 @@ +import { allMutations, childListSubtree } from './observer' +import { addStyle } from './style' +import { deleteValue, getRandomId } from './utils' + +enum ShadowRootEvents { + Added = 'shadowRootAdded', + Removed = 'shadowRootRemoved', + Updated = 'shadowRootUpdated', +} +interface ShadowRootEntry { + shadowRoot: ShadowRoot + observer: MutationObserver +} +interface ShadowRootStyleEntry { + shadowRoot: ShadowRoot +} +export class ShadowDomObserver extends EventTarget { + static enforceOpenRoot() { + const originalAttachShadow = Element.prototype.attachShadow + Element.prototype.attachShadow = function attachShadow(options: ShadowRootInit) { + return originalAttachShadow.call(this, { + ...options, + mode: 'open', + }) + } + } + + private observing = false + + entries: ShadowRootEntry[] = [] + + constructor() { + super() + } + + protected queryAllShadowRoots( + root: DocumentFragment | Element = document.body, + deep = false, + ): ShadowRoot[] { + return [root, ...root.querySelectorAll('*')] + .filter((e): e is Element => e instanceof Element && e.shadowRoot !== null) + .flatMap(e => { + if (deep) { + return [e.shadowRoot, ...this.queryAllShadowRoots(e.shadowRoot)] + } + return [e.shadowRoot] + }) + } + + protected mutationHandler(records: MutationRecord[]) { + records.forEach(record => { + record.removedNodes.forEach(node => { + if (node instanceof Element && node.shadowRoot !== null) { + this.removeEntry(node.shadowRoot) + } + }) + record.addedNodes.forEach(node => { + if (node instanceof Element) { + this.queryAllShadowRoots(node).forEach(shadowRoot => { + this.addEntry(shadowRoot) + }) + } + }) + }) + } + + protected addEntry(shadowRoot: ShadowRoot) { + if (this.shadowRoots.includes(shadowRoot)) { + return + } + const shadowRootChildren = this.queryAllShadowRoots(shadowRoot) + shadowRootChildren.forEach(child => this.addEntry(child)) + + const [observer] = childListSubtree(shadowRoot, records => this.mutationHandler(records)) + this.entries.push({ + shadowRoot, + observer, + }) + this.dispatchEvent(new CustomEvent('shadowRootAdded', { detail: shadowRoot })) + } + + protected removeEntry(shadowRoot: ShadowRoot) { + const children = this.shadowRoots.filter(it => shadowRoot.contains(it.host)) + children.forEach(child => this.removeEntry(child)) + + const entry = this.entries.find(it => it.shadowRoot === shadowRoot) + if (entry !== undefined) { + entry.observer.disconnect() + deleteValue(this.entries, it => it === entry) + } + } + + get shadowRoots() { + return this.entries.map(entry => entry.shadowRoot) + } + + addEventListener( + type: `${ShadowRootEvents}`, + callback: EventListenerOrEventListenerObject | null, + options?: AddEventListenerOptions | boolean, + ): void { + super.addEventListener(type, callback, options) + } + + removeEventListener( + type: `${ShadowRootEvents}`, + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean, + ): void { + super.removeEventListener(type, callback, options) + } + + forEachShadowRoot(callbacks: { + added?: (shadowRoot: ShadowRoot) => void + removed?: (shadowRoot: ShadowRoot) => void + }) { + this.shadowRoots.forEach(it => callbacks.added?.(it)) + const addedListener = (e: CustomEvent) => callbacks?.added(e.detail) + const removedListener = (e: CustomEvent) => callbacks?.removed(e.detail) + this.addEventListener(ShadowRootEvents.Added, addedListener) + this.addEventListener(ShadowRootEvents.Removed, removedListener) + return () => { + this.removeEventListener(ShadowRootEvents.Added, addedListener) + this.removeEventListener(ShadowRootEvents.Removed, removedListener) + } + } + + observe() { + if (this.observing) { + return + } + const existingRoots = this.queryAllShadowRoots() + existingRoots.forEach(root => this.addEntry(root)) + allMutations(records => this.mutationHandler(records)) + this.observing = true + } + + disconnect() { + this.entries.forEach(entry => entry.observer.disconnect()) + this.entries = [] + this.observing = false + } +} + +const shadowDomObserver = new ShadowDomObserver() + +export class ShadowDomStyles { + observer: ShadowDomObserver = shadowDomObserver + entries: ShadowRootStyleEntry[] = [] + + get shadowRoots() { + return this.entries.map(entry => entry.shadowRoot) + } + + protected addEntry(shadowRoot: ShadowRoot) { + this.entries.push({ + shadowRoot, + }) + } + + protected removeEntry(shadowRoot: ShadowRoot) { + const entry = this.entries.find(it => it.shadowRoot === shadowRoot) + if (entry !== undefined) { + deleteValue(this.entries, it => it === entry) + } + } + + addStyle(text: string) { + this.observer.observe() + const id = `shadow-dom-style-${getRandomId()}` + const element = addStyle(text, id) + const destroy = this.observer.forEachShadowRoot({ + added: async shadowRoot => { + if (this.shadowRoots.includes(shadowRoot)) { + return + } + this.addEntry(shadowRoot) + const sheet = new CSSStyleSheet() + await sheet.replace(element.innerHTML) + shadowRoot.adoptedStyleSheets.push(sheet) + }, + removed: shadowRoot => { + shadowRoot.getElementById(id)?.remove() + this.removeEntry(shadowRoot) + }, + }) + return () => { + destroy() + element.remove() + } + } +}