mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
Merge branch 'preview-features' into preview
This commit is contained in:
commit
3d56cf2259
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,6 +1,43 @@
|
|||||||
<!-- spell-checker: disable -->
|
<!-- spell-checker: disable -->
|
||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v2.9.2-preview
|
||||||
|
`2024-09-08`
|
||||||
|
|
||||||
|
包含 [v2.9.2](https://github.com/the1812/Bilibili-Evolved/releases/tag/v2.9.2) 的所有更新内容.
|
||||||
|
|
||||||
|
✨新增
|
||||||
|
- `弹幕转义` 支持对正斜杠的换行 (`/n`) 进行转义. (#4865)
|
||||||
|
- `自定义顶栏` 支持直接在功能中打开布局设置. (#2666)
|
||||||
|
- `高分辨率图片` 支持处理没有指定高度的图片, 支持在专栏页面中请求原图. (#2868)
|
||||||
|
- `直播间网页全屏自适应` 样式适配较低的宽度值. (#4895)
|
||||||
|
|
||||||
|
☕开发者相关
|
||||||
|
- 外部资源接入 Subresource Integrity. (#4896)
|
||||||
|
|
||||||
|
## v2.9.2
|
||||||
|
`2024-09-08`
|
||||||
|
|
||||||
|
✨新增
|
||||||
|
- `网址参数清理` 支持清理 `is_room_feed`. (PR #4886 by [dreammu](https://github.com/dreammu))
|
||||||
|
|
||||||
|
🐛修复
|
||||||
|
- 新版评论区相关功能修复: (#4843)
|
||||||
|
- 修复 `快速收起评论` 按钮错位. (#4890)
|
||||||
|
- 恢复功能: `禁用评论区搜索词`, `评论区IP属地显示`, `复制动态链接`.
|
||||||
|
- `简化评论区` 支持 Firefox.
|
||||||
|
- 样式实现使用 [Container style queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries#container_style_queries_2) 替代 [:host-context](https://developer.mozilla.org/en-US/docs/Web/CSS/:host-context), 虽然 Firefox 还是不支持, 但是能稍微标准化一点.
|
||||||
|
- 夜间模式适配
|
||||||
|
|
||||||
|
☕开发者相关
|
||||||
|
- Shadow DOM API (`./src/core/shadow-dom`) 更名为 Shadow Root API (`./src/core/shadow-root`), 模块内的功能导出单例:
|
||||||
|
- `shadowDomObserver`: 持续观测页面上的所有 Shadow DOM.
|
||||||
|
- `shadowRootStyles`: 支持将样式注入到 Shadow DOM 内部.
|
||||||
|
- Comments API 增加 `CommentAreaV3` 实现, 支持基于 Shadow DOM 的新版评论区. (#4843)
|
||||||
|
- 增加 `isContainerStyleQuerySupported` 来检测当前浏览器对 [Container style queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries#container_style_queries_2) 的支持.
|
||||||
|
- 组件样式支持在 `ComponentMetadata.instantStyles` 中声明 `shadowDom: true` 来插入到 Shadow DOM 中.
|
||||||
|
|
||||||
|
|
||||||
## v2.9.1-preview
|
## v2.9.1-preview
|
||||||
`2024-08-15`
|
`2024-08-15`
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,10 @@ https://afdian.net/@the1812?tab=sponsor
|
|||||||
|
|
||||||
| 时间 | 用户名 | 单号后4位 | 金额 |
|
| 时间 | 用户名 | 单号后4位 | 金额 |
|
||||||
| ------------------- | --------------------- | --------- | ------- |
|
| ------------------- | --------------------- | --------- | ------- |
|
||||||
|
| 2024.09.06 16:05:23 | 匿名 | 0752 | ¥5.00 |
|
||||||
|
| 2024.09.03 23:08:43 | *根 | 3956 | ¥20.00 |
|
||||||
|
| 2024.08.25 16:55:07 | 匿名 | 6595 | ¥10.00 |
|
||||||
|
| 2024.08.16 11:01:03 | 匿名 | 7648 | ¥30.00 |
|
||||||
| 2024.08.15 10:18:07 | *9 | 1653 | ¥10.00 |
|
| 2024.08.15 10:18:07 | *9 | 1653 | ¥10.00 |
|
||||||
| 2024.07.30 13:40:22 | Z*n | 2215 | ¥5.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.24 09:38:46 | H*g | 7776 | ¥5.00 |
|
||||||
|
|||||||
@ -707,7 +707,7 @@
|
|||||||
"type": "component",
|
"type": "component",
|
||||||
"name": "unescapeDanmaku",
|
"name": "unescapeDanmaku",
|
||||||
"displayName": "弹幕转义",
|
"displayName": "弹幕转义",
|
||||||
"description": "将弹幕中的 `\\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.\r\n",
|
"description": "将弹幕中的 `\\n` 或 `/n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.\r\n",
|
||||||
"fullRelativePath": "../../registry/dist/components/video/danmaku/unescape.js",
|
"fullRelativePath": "../../registry/dist/components/video/danmaku/unescape.js",
|
||||||
"fullAbsolutePath": "registry/dist/components/video/danmaku/unescape.js"
|
"fullAbsolutePath": "registry/dist/components/video/danmaku/unescape.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -969,7 +969,7 @@ by [@kdxcxs](https://github.com/kdxcxs)
|
|||||||
|
|
||||||
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/unescape.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/unescape.js)
|
**GitHub:** [`Stable`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/master/registry/dist/components/video/danmaku/unescape.js) / [`Preview`](https://raw.githubusercontent.com/the1812/Bilibili-Evolved/preview/registry/dist/components/video/danmaku/unescape.js)
|
||||||
|
|
||||||
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
将弹幕中的 `\n` 或 `/n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
||||||
|
|
||||||
### [视频页默认定位](../../registry/dist/components/video/default-location.js)
|
### [视频页默认定位](../../registry/dist/components/video/default-location.js)
|
||||||
`videoDefaultLocation`
|
`videoDefaultLocation`
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
:host(bili-comments) {
|
||||||
|
#end .bottombar {
|
||||||
|
padding-bottom: 8px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { select } from '@/core/spin-query'
|
|||||||
import { childListSubtree } from '@/core/observer'
|
import { childListSubtree } from '@/core/observer'
|
||||||
|
|
||||||
const entry = async () => {
|
const entry = async () => {
|
||||||
|
const { shadowRootStyles } = await import('@/core/shadow-root')
|
||||||
const { forEachFeedsCard } = await import('@/components/feeds/api')
|
const { forEachFeedsCard } = await import('@/components/feeds/api')
|
||||||
const { childList } = await import('@/core/observer')
|
const { childList } = await import('@/core/observer')
|
||||||
const commentSelector = '.bb-comment, .bili-comment-container'
|
const commentSelector = '.bb-comment, .bili-comment-container'
|
||||||
@ -30,20 +31,23 @@ const entry = async () => {
|
|||||||
commentBox.insertAdjacentElement('beforeend', button)
|
commentBox.insertAdjacentElement('beforeend', button)
|
||||||
}
|
}
|
||||||
if (feedsCardsManager.managerType === 'v2') {
|
if (feedsCardsManager.managerType === 'v2') {
|
||||||
const existingComment = dq(card, commentSelector) as HTMLElement
|
const getExistingComment = () => dq(card, commentSelector) as HTMLElement
|
||||||
|
const isCommentAreaReady = () => {
|
||||||
|
const existingComment = getExistingComment()
|
||||||
|
return existingComment !== null && dq(existingComment, 'bili-comments')
|
||||||
|
}
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
const button = dq(card, '.bili-dyn-action.comment') as HTMLElement
|
const button = dq(card, '.bili-dyn-action.comment') as HTMLElement
|
||||||
button?.click()
|
button?.click()
|
||||||
}
|
}
|
||||||
if (!existingComment) {
|
if (!isCommentAreaReady()) {
|
||||||
childListSubtree(card, () => {
|
childListSubtree(card, () => {
|
||||||
const panel = dq(card, commentSelector)
|
if (isCommentAreaReady()) {
|
||||||
if (panel) {
|
|
||||||
injectToComment(card, handler)
|
injectToComment(card, handler)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
injectToComment(existingComment, handler)
|
injectToComment(getExistingComment(), handler)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -75,6 +79,12 @@ const entry = async () => {
|
|||||||
forEachFeedsCard({
|
forEachFeedsCard({
|
||||||
added: c => injectButton(c.element),
|
added: c => injectButton(c.element),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const style = await import('./fold-comment-shadow.scss').then(m => m.default)
|
||||||
|
shadowRootStyles.addStyle({
|
||||||
|
id: 'foldComments',
|
||||||
|
style,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
|
|||||||
@ -1,10 +1,30 @@
|
|||||||
.player-full-win:not(.hide-aside-area) {
|
.player-full-win:not(.hide-aside-area) {
|
||||||
.live-room-app {
|
.live-room-app {
|
||||||
.aside-area {
|
.aside-area {
|
||||||
|
container-name: aside-area;
|
||||||
|
container-type: size;
|
||||||
width: var(--live-chat-panel-width, 302px) !important;
|
width: var(--live-chat-panel-width, 302px) !important;
|
||||||
}
|
}
|
||||||
.player-section {
|
.player-section {
|
||||||
width: calc(100% - var(--live-chat-panel-width, 302px)) !important;
|
width: calc(100% - var(--live-chat-panel-width, 302px)) !important;
|
||||||
}
|
}
|
||||||
|
@container aside-area (max-width: 290px) {
|
||||||
|
.control-panel-icon-row-new .icon-left-part-new {
|
||||||
|
.super-chat,
|
||||||
|
.like-btn {
|
||||||
|
width: 32px;
|
||||||
|
&-icon {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
&-text {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chat-input-ctnr-new .medal-section {
|
||||||
|
min-width: 0 !important;
|
||||||
|
max-width: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,6 +133,9 @@ export const component = defineComponentMetadata({
|
|||||||
// const { addImportantStyle } = await import('@/core/style')
|
// const { addImportantStyle } = await import('@/core/style')
|
||||||
// addImportantStyle(style, styleID)
|
// addImportantStyle(style, styleID)
|
||||||
},
|
},
|
||||||
|
widget: {
|
||||||
|
component: () => import('./settings/Widget.vue').then(m => m.default),
|
||||||
|
},
|
||||||
extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default),
|
extraOptions: () => import('./settings/ExtraOptions.vue').then(m => m.default),
|
||||||
plugin: {
|
plugin: {
|
||||||
displayName: '自定义顶栏 - 功能扩展',
|
displayName: '自定义顶栏 - 功能扩展',
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<DefaultWidget
|
||||||
|
name="顶栏布局设置"
|
||||||
|
icon="mdi-sort"
|
||||||
|
@mouseover="loadNavbarSettings()"
|
||||||
|
@click="toggleNavbarSettings()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { DefaultWidget } from '@/ui'
|
||||||
|
import { loadNavbarSettings, toggleNavbarSettings } from './vm'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
components: {
|
||||||
|
DefaultWidget,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadNavbarSettings() {
|
||||||
|
await loadNavbarSettings()
|
||||||
|
},
|
||||||
|
toggleNavbarSettings,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
77
registry/lib/components/style/dark-mode/dark-shadow-dom.scss
Normal file
77
registry/lib/components/style/dark-mode/dark-shadow-dom.scss
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
@import './dark-definitions';
|
||||||
|
|
||||||
|
:host(bili-comment-action-buttons-renderer) {
|
||||||
|
@include color('a');
|
||||||
|
button {
|
||||||
|
@include color('a');
|
||||||
|
&:hover {
|
||||||
|
@include theme-color();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bili-icon[style*='var(--brand_blue)'] ~ #count {
|
||||||
|
@include theme-color();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comment-replies-renderer) {
|
||||||
|
#view-more {
|
||||||
|
@include color('a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comment-user-info) {
|
||||||
|
#user-name {
|
||||||
|
@include color('e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comments) {
|
||||||
|
.bottombar {
|
||||||
|
@include color('e');
|
||||||
|
&.clickable:hover {
|
||||||
|
@include theme-color();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comment-menu) {
|
||||||
|
#options li:hover {
|
||||||
|
@include background-color('3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-rich-text) {
|
||||||
|
#contents {
|
||||||
|
a {
|
||||||
|
@include theme-color();
|
||||||
|
}
|
||||||
|
img,
|
||||||
|
a i {
|
||||||
|
@include to-theme('blue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comment-box) {
|
||||||
|
#pub button {
|
||||||
|
@include theme-background-color();
|
||||||
|
@include foreground-color();
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
@include theme-background-color('80');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comments-header-renderer) {
|
||||||
|
#title #count {
|
||||||
|
@include color('a');
|
||||||
|
}
|
||||||
|
bili-text-button {
|
||||||
|
@include set-color('--_label-text-color', 'e');
|
||||||
|
&:hover {
|
||||||
|
@include set-theme-color('--_label-text-color-hover');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#sort-actions.hot bili-text-button:first-child,
|
||||||
|
#sort-actions.time bili-text-button:last-child {
|
||||||
|
@include set-theme-color('--_label-text-color');
|
||||||
|
}
|
||||||
|
.bili-comments-bottom-fixed-wrapper > div {
|
||||||
|
@include background-color('2');
|
||||||
|
@include border-color('3');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -67,8 +67,28 @@
|
|||||||
.bili-dyn-publishing {
|
.bili-dyn-publishing {
|
||||||
@include dyn-container-skeleton();
|
@include dyn-container-skeleton();
|
||||||
@include background-color('4');
|
@include background-color('4');
|
||||||
|
&__title {
|
||||||
|
&__input {
|
||||||
|
@include background-color();
|
||||||
|
@include color('e');
|
||||||
|
&::placeholder {
|
||||||
|
@include color('a');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__indicator {
|
||||||
|
@include color('a');
|
||||||
|
}
|
||||||
|
&__close {
|
||||||
|
@include background-color('6');
|
||||||
|
}
|
||||||
|
}
|
||||||
&__tools {
|
&__tools {
|
||||||
&__item {
|
&__item {
|
||||||
|
@include color('e');
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
@include theme-color();
|
||||||
|
}
|
||||||
// 这里用 filter 会导致弹窗内容也被影响, 暂时先去掉了
|
// 这里用 filter 会导致弹窗内容也被影响, 暂时先去掉了
|
||||||
// &.active,
|
// &.active,
|
||||||
// &:hover {
|
// &:hover {
|
||||||
@ -79,6 +99,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&__settings__btn {
|
||||||
|
@include color('e');
|
||||||
|
&.active,
|
||||||
|
&:hover {
|
||||||
|
@include theme-color();
|
||||||
|
}
|
||||||
|
}
|
||||||
.bili-rich-textarea__inner {
|
.bili-rich-textarea__inner {
|
||||||
@include background-color();
|
@include background-color();
|
||||||
@include color('e');
|
@include color('e');
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { defineComponentMetadata } from '@/components/define'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import { darkExcludes } from './dark-urls'
|
import { darkExcludes } from './dark-urls'
|
||||||
|
|
||||||
|
const name = 'darkMode'
|
||||||
const changeDelay = 200
|
const changeDelay = 200
|
||||||
const darkMetaColor = '#111'
|
const darkMetaColor = '#111'
|
||||||
const add = async () => {
|
const add = async () => {
|
||||||
@ -40,16 +41,15 @@ const remove = async () => {
|
|||||||
colorSchemeMeta.content = 'light'
|
colorSchemeMeta.content = 'light'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const entry = async () => {
|
||||||
|
setTimeout(add, changeDelay)
|
||||||
|
}
|
||||||
|
|
||||||
export const component = defineComponentMetadata({
|
export const component = defineComponentMetadata({
|
||||||
name: 'darkMode',
|
name,
|
||||||
displayName: '夜间模式',
|
displayName: '夜间模式',
|
||||||
entry: () => {
|
entry,
|
||||||
setTimeout(add, changeDelay)
|
reload: entry,
|
||||||
},
|
|
||||||
reload: () => {
|
|
||||||
setTimeout(add, changeDelay)
|
|
||||||
},
|
|
||||||
unload: () => {
|
unload: () => {
|
||||||
setTimeout(remove, changeDelay)
|
setTimeout(remove, changeDelay)
|
||||||
},
|
},
|
||||||
@ -66,6 +66,11 @@ export const component = defineComponentMetadata({
|
|||||||
style: () => import('./dark-mode.important.scss'),
|
style: () => import('./dark-mode.important.scss'),
|
||||||
important: true,
|
important: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'dark-shadow-dom',
|
||||||
|
style: () => import('./dark-shadow-dom.scss'),
|
||||||
|
shadowDom: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
plugin: {
|
plugin: {
|
||||||
displayName: '夜间模式 - 提前注入',
|
displayName: '夜间模式 - 提前注入',
|
||||||
@ -76,8 +81,8 @@ export const component = defineComponentMetadata({
|
|||||||
const { contentLoaded } = await import('@/core/life-cycle')
|
const { contentLoaded } = await import('@/core/life-cycle')
|
||||||
const { isComponentEnabled } = await import('@/core/settings')
|
const { isComponentEnabled } = await import('@/core/settings')
|
||||||
contentLoaded(() => {
|
contentLoaded(() => {
|
||||||
// 提前添加dark的class, 防止颜色抖动
|
// 提前添加 dark 的 class, 防止颜色抖动
|
||||||
if (isComponentEnabled('darkMode')) {
|
if (isComponentEnabled(name)) {
|
||||||
document.body.classList.add('dark')
|
document.body.classList.add('dark')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
:host(bili-rich-text) {
|
||||||
|
#contents img,
|
||||||
|
#contents a i {
|
||||||
|
max-width: 1.4em;
|
||||||
|
max-height: 1.4em;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
:host(bili-comment-renderer) {
|
||||||
|
bili-comment-user-sailing-card {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
:host(bili-comments-header-renderer) {
|
||||||
|
bili-comments-notice {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
:host(bili-comment-user-info) {
|
||||||
|
bili-comment-user-medal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
:host(bili-comment-reply-renderer) {
|
||||||
|
bili-comment-user-info {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
:host(bili-comment-user-info) {
|
||||||
|
#user-level {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,57 +1,64 @@
|
|||||||
@import 'common';
|
|
||||||
|
|
||||||
$prefix: 'simplifyComments-switch';
|
$prefix: 'simplifyComments-switch';
|
||||||
|
|
||||||
:host-context(.comment-m-v1) {
|
:host(bili-rich-text) {
|
||||||
&:host(bili-rich-text) {
|
#contents img,
|
||||||
#contents img, #contents a i {
|
#contents a i {
|
||||||
max-width: 1.4em;
|
max-width: 1.4em;
|
||||||
max-height: 1.4em;
|
max-height: 1.4em;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:host-context(body.#{$prefix}-replyEditor) {
|
@container style(--#{$prefix}-replyEditor: true) {
|
||||||
&:host(bili-comment-box) {
|
:host(bili-comment-box) {
|
||||||
#pub button {
|
#pub button {
|
||||||
font-size: 14px;
|
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) {
|
:host(bili-checkbox) {
|
||||||
|
#label {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:host(bili-comment-textarea) {
|
||||||
|
#input {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
#input,
|
||||||
|
#input::placeholder {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container style(--#{$prefix}-userLevel: true) {
|
||||||
|
:host(bili-comment-user-info) {
|
||||||
#user-level {
|
#user-level {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:host-context(body.#{$prefix}-decorateAndTime):host(bili-comment-renderer) {
|
}
|
||||||
|
@container style(--#{$prefix}-decorateAndTime: true) {
|
||||||
|
:host(bili-comment-renderer) {
|
||||||
bili-comment-user-sailing-card {
|
bili-comment-user-sailing-card {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:host-context(body.#{$prefix}-fansMedal):host(bili-comment-user-info) {
|
}
|
||||||
|
@container style(--#{$prefix}-fansMedal: true) {
|
||||||
|
:host(bili-comment-user-info) {
|
||||||
bili-comment-user-medal {
|
bili-comment-user-medal {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:host-context(body.#{$prefix}-subReplyNewLine):host(bili-comment-reply-renderer) {
|
}
|
||||||
|
@container style(--#{$prefix}-subReplyNewLine: true) {
|
||||||
|
:host(bili-comment-reply-renderer) {
|
||||||
bili-comment-user-info {
|
bili-comment-user-info {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:host-context(body.#{$prefix}-eventBanner):host(bili-comments-header-renderer) {
|
}
|
||||||
|
@container style(--#{$prefix}-eventBanner: true) {
|
||||||
|
:host(bili-comments-header-renderer) {
|
||||||
bili-comments-notice {
|
bili-comments-notice {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,9 @@ export const component = wrapSwitchOptions({
|
|||||||
})({
|
})({
|
||||||
name,
|
name,
|
||||||
displayName: '简化评论区',
|
displayName: '简化评论区',
|
||||||
entry: async ({ metadata }) => {
|
entry: async ({ metadata, settings }) => {
|
||||||
|
const { addStyle, getDefaultStyleID } = await import('@/core/style')
|
||||||
|
const { isContainerStyleQuerySupported } = await import('@/core/container-query')
|
||||||
const { addComponentListener } = await import('@/core/settings')
|
const { addComponentListener } = await import('@/core/settings')
|
||||||
addComponentListener(
|
addComponentListener(
|
||||||
metadata.name,
|
metadata.name,
|
||||||
@ -51,10 +53,45 @@ export const component = wrapSwitchOptions({
|
|||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|
||||||
const { ShadowDomStyles } = await import('@/core/shadow-dom')
|
// 等 Firefox 支持 [Container Style Queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_size_and_style_queries#container_style_queries_2) 可去除此判断
|
||||||
const v3Styles = await import('./comments-v3.scss').then(m => m.default)
|
if (isContainerStyleQuerySupported()) {
|
||||||
const shadowDom = new ShadowDomStyles()
|
const { shadowRootStyles } = await import('@/core/shadow-root')
|
||||||
shadowDom.addStyle(v3Styles)
|
const v3Style = await import('./comments-v3.scss').then(m => m.default)
|
||||||
|
shadowRootStyles.toggleWithComponent(metadata.name, { id: name, style: v3Style })
|
||||||
|
} else {
|
||||||
|
const { shadowRootStyles } = await import('@/core/shadow-root')
|
||||||
|
const firefoxStyles = require.context('./comments-v3-firefox', false, /\.scss$/)
|
||||||
|
|
||||||
|
Object.keys(settings.options).forEach(key => {
|
||||||
|
if (!key.startsWith('switch-')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = `${component.name}.${key}`
|
||||||
|
const styleName = lodash.kebabCase(key.replace(/^switch-/, ''))
|
||||||
|
const path = `./${styleName}.scss`
|
||||||
|
if (!firefoxStyles.keys().includes(path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addComponentListener(
|
||||||
|
id,
|
||||||
|
(value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
const style = firefoxStyles(path) as string
|
||||||
|
addStyle(style, styleName)
|
||||||
|
shadowRootStyles.addStyle({
|
||||||
|
id,
|
||||||
|
style,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
document.getElementById(getDefaultStyleID(styleName))?.remove()
|
||||||
|
shadowRootStyles.removeStyle(id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
instantStyles: [
|
instantStyles: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
:host(bili-rich-text) {
|
||||||
|
#contents a[data-type='search'] {
|
||||||
|
color: inherit !important;
|
||||||
|
cursor: inherit !important;
|
||||||
|
display: contents !important;
|
||||||
|
img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { defineComponentMetadata } from '@/components/define'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import { forEachCommentArea } from '@/components/utils/comment-apis'
|
import { forEachCommentArea, CommentAreaV3 } from '@/components/utils/comment-apis'
|
||||||
|
import { ShadowRootEvents } from '@/core/shadow-root'
|
||||||
import { preventEvent } from '@/core/utils'
|
import { preventEvent } from '@/core/utils'
|
||||||
|
|
||||||
const name = 'disableCommentsSearchLink'
|
const name = 'disableCommentsSearchLink'
|
||||||
@ -13,25 +14,51 @@ export const component = defineComponentMetadata({
|
|||||||
style: () => import('./disable-search-link.scss'),
|
style: () => import('./disable-search-link.scss'),
|
||||||
important: true,
|
important: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
style: () => import('./disable-search-link-shadow.scss'),
|
||||||
|
shadowDom: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
tags: [componentsTags.utils, componentsTags.style],
|
tags: [componentsTags.utils, componentsTags.style],
|
||||||
entry: async () => {
|
entry: async () => {
|
||||||
prevent = true
|
prevent = true
|
||||||
forEachCommentArea(area => {
|
forEachCommentArea(async area => {
|
||||||
preventEvent(area.element, 'click', e => {
|
const isV3Area = area instanceof CommentAreaV3
|
||||||
if (!(e.target instanceof HTMLElement) || !prevent) {
|
if (isV3Area) {
|
||||||
|
area.commentAreaEntry.addEventListener(
|
||||||
|
ShadowRootEvents.Updated,
|
||||||
|
(e: CustomEvent<MutationRecord[]>) => {
|
||||||
|
const records = e.detail
|
||||||
|
records.forEach(record => {
|
||||||
|
record.addedNodes.forEach(node => {
|
||||||
|
const isCommentLink =
|
||||||
|
node instanceof HTMLAnchorElement && node.getAttribute('data-type') === 'search'
|
||||||
|
if (!isCommentLink) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node.removeAttribute('href')
|
||||||
|
node.removeAttribute('target')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
preventEvent(area.element, 'click', e => {
|
||||||
|
if (!(e.target instanceof HTMLElement) || !prevent) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const element = e.target as HTMLElement
|
||||||
|
if (
|
||||||
|
['.jump-link.search-word', '.icon.search-word'].some(selector =>
|
||||||
|
element.matches(selector),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
})
|
||||||
const element = e.target as HTMLElement
|
}
|
||||||
if (
|
|
||||||
['.jump-link.search-word', '.icon.search-word'].some(selector =>
|
|
||||||
element.matches(selector),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
reload: () => {
|
reload: () => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { ItemStopPayload, Payload } from 'dev-tools/dev-server/payload'
|
import type { ItemStopPayload, Payload } from 'dev-tools/dev-server/payload'
|
||||||
import { useScopedConsole } from '@/core/utils/log'
|
import { useScopedConsole } from '@/core/utils/log'
|
||||||
import { ComponentMetadata, componentsMap } from '@/components/component'
|
import { ComponentMetadata, componentsMap } from '@/components/component'
|
||||||
import { loadInstantStyle, removeStyle } from '@/core/style'
|
import { loadInstantStyle, removeInstantStyle } from '@/core/style'
|
||||||
import { autoUpdateOptions, getDevClientOptions } from './options'
|
import { autoUpdateOptions, getDevClientOptions } from './options'
|
||||||
import { RefreshMethod, HotReloadMethod } from './update-method'
|
import { RefreshMethod, HotReloadMethod } from './update-method'
|
||||||
import { monkey } from '@/core/ajax'
|
import { monkey } from '@/core/ajax'
|
||||||
@ -161,10 +161,10 @@ export class DevClient extends EventTarget {
|
|||||||
}
|
}
|
||||||
const reloadInstantStyles = () => {
|
const reloadInstantStyles = () => {
|
||||||
if (oldInstantStyles.length > 0 || newInstantStyles.length > 0) {
|
if (oldInstantStyles.length > 0 || newInstantStyles.length > 0) {
|
||||||
loadInstantStyle(newComponent)
|
|
||||||
oldInstantStyles.forEach(style => {
|
oldInstantStyles.forEach(style => {
|
||||||
removeStyle(style.name)
|
removeInstantStyle(style)
|
||||||
})
|
})
|
||||||
|
loadInstantStyle(newComponent)
|
||||||
// 修改旧的引用, 否则之前设的事件监听还是用旧样式
|
// 修改旧的引用, 否则之前设的事件监听还是用旧样式
|
||||||
oldComponent.instantStyles = newInstantStyles
|
oldComponent.instantStyles = newInstantStyles
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -11,6 +11,10 @@ const options = defineOptionsMetadata({
|
|||||||
defaultValue: 'auto',
|
defaultValue: 'auto',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
|
originalImageInArticles: {
|
||||||
|
displayName: '在专栏中请求原图',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Options = OptionsOfMetadata<typeof options>
|
export type Options = OptionsOfMetadata<typeof options>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { styledComponentEntry } from '@/components/styled-component'
|
import { styledComponentEntry } from '@/components/styled-component'
|
||||||
import { Options } from '.'
|
import { Options } from '.'
|
||||||
|
|
||||||
const resizeRegex = /@(\d+)[Ww]_(\d+)[Hh]/
|
const resizeRegex = /@(\d+)[Ww](_(\d+)[Hh])?/
|
||||||
|
|
||||||
/** 排除 */
|
/** 排除 */
|
||||||
const excludeSelectors = ['#certify-img1', '#certify-img2']
|
const excludeSelectors = ['#certify-img1', '#certify-img2']
|
||||||
@ -17,6 +17,7 @@ const widthAndHeightSelectors = [
|
|||||||
// https://github.com/the1812/Bilibili-Evolved/issues/4480
|
// https://github.com/the1812/Bilibili-Evolved/issues/4480
|
||||||
'.logo-img',
|
'.logo-img',
|
||||||
]
|
]
|
||||||
|
const originalImageInArticlesSelectors = ['.article-detail .article-content img']
|
||||||
|
|
||||||
const walk = (rootElement: Node, action: (node: HTMLElement) => void) => {
|
const walk = (rootElement: Node, action: (node: HTMLElement) => void) => {
|
||||||
const walker = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT)
|
const walker = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT)
|
||||||
@ -26,12 +27,21 @@ const walk = (rootElement: Node, action: (node: HTMLElement) => void) => {
|
|||||||
node = walker.nextNode()
|
node = walker.nextNode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ImageResolutionHandler {
|
||||||
|
getWidth: (width: number, element: HTMLElement) => number
|
||||||
|
getHeight: (height: number, element: HTMLElement) => number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从开始元素`element`向下遍历所有子节点, 更换其中的图片URL至目标DPI
|
* 从开始元素`element`向下遍历所有子节点, 更换其中的图片URL至目标DPI
|
||||||
* @param dpi 目标DPI
|
* @param dpi 目标DPI
|
||||||
* @param element 开始元素
|
* @param element 开始元素
|
||||||
*/
|
*/
|
||||||
export const imageResolution = async (dpi: number, element: HTMLElement) => {
|
export const imageResolution = async (
|
||||||
|
element: HTMLElement,
|
||||||
|
resolutionHandler: ImageResolutionHandler,
|
||||||
|
) => {
|
||||||
const { attributes } = await import('@/core/observer')
|
const { attributes } = await import('@/core/observer')
|
||||||
const replaceSource = (
|
const replaceSource = (
|
||||||
getValue: (e: HTMLElement) => string | null,
|
getValue: (e: HTMLElement) => string | null,
|
||||||
@ -48,16 +58,22 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
|
|||||||
if (value.includes(',')) {
|
if (value.includes(',')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = value.match(resizeRegex)
|
const match = value.match(resizeRegex)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const [, currentWidth, currentHeight] = match
|
const [, currentWidth, , currentHeight] = match
|
||||||
const lastWidth = parseInt(element.getAttribute('data-resolution-width') || '0')
|
const lastWidth = parseInt(element.getAttribute('data-resolution-width') || '0')
|
||||||
if (parseInt(currentWidth) >= lastWidth && lastWidth !== 0) {
|
if (parseInt(currentWidth) >= lastWidth && lastWidth !== 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (element.getAttribute('width') === null && element.getAttribute('height') === null) {
|
|
||||||
|
if (
|
||||||
|
element.getAttribute('width') === null &&
|
||||||
|
element.getAttribute('height') === null &&
|
||||||
|
currentHeight !== undefined
|
||||||
|
) {
|
||||||
if (widthAndHeightSelectors.some(selector => element.matches(selector))) {
|
if (widthAndHeightSelectors.some(selector => element.matches(selector))) {
|
||||||
element.setAttribute('height', currentHeight)
|
element.setAttribute('height', currentHeight)
|
||||||
element.setAttribute('width', currentWidth)
|
element.setAttribute('width', currentWidth)
|
||||||
@ -67,10 +83,26 @@ export const imageResolution = async (dpi: number, element: HTMLElement) => {
|
|||||||
element.setAttribute('width', currentWidth)
|
element.setAttribute('width', currentWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newWidth = Math.round(dpi * parseInt(currentWidth)).toString()
|
|
||||||
const newHeight = Math.round(dpi * parseInt(currentHeight)).toString()
|
const getReplacedValue = (newWidth: number, newHeight?: number) => {
|
||||||
element.setAttribute('data-resolution-width', newWidth)
|
if (newWidth === Infinity || newHeight === Infinity) {
|
||||||
setValue(element, value.replace(resizeRegex, `@${newWidth}w_${newHeight}h`))
|
return value.replace(resizeRegex, '@')
|
||||||
|
}
|
||||||
|
if (newHeight === undefined) {
|
||||||
|
return value.replace(resizeRegex, `@${newWidth}w`)
|
||||||
|
}
|
||||||
|
return value.replace(resizeRegex, `@${newWidth}w_${newHeight}h`)
|
||||||
|
}
|
||||||
|
if (currentHeight !== undefined) {
|
||||||
|
const newWidth = resolutionHandler.getWidth(parseInt(currentWidth), element)
|
||||||
|
const newHeight = resolutionHandler.getHeight(parseInt(currentHeight), element)
|
||||||
|
element.setAttribute('data-resolution-width', newWidth.toString())
|
||||||
|
setValue(element, getReplacedValue(newWidth, newHeight))
|
||||||
|
} else {
|
||||||
|
const newWidth = resolutionHandler.getWidth(parseInt(currentWidth), element)
|
||||||
|
element.setAttribute('data-resolution-width', newWidth.toString())
|
||||||
|
setValue(element, getReplacedValue(newWidth))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
attributes(element, () => {
|
attributes(element, () => {
|
||||||
replaceSource(
|
replaceSource(
|
||||||
@ -95,14 +127,35 @@ export const startResolution = styledComponentEntry<Options>(
|
|||||||
settings.options.scale === 'auto'
|
settings.options.scale === 'auto'
|
||||||
? window.devicePixelRatio
|
? window.devicePixelRatio
|
||||||
: parseFloat(settings.options.scale)
|
: parseFloat(settings.options.scale)
|
||||||
walk(document.body, it => imageResolution(dpi, it))
|
const handleResolution: ImageResolutionHandler = {
|
||||||
|
getWidth: (currentWidth, element) => {
|
||||||
|
if (
|
||||||
|
settings.options.originalImageInArticles &&
|
||||||
|
originalImageInArticlesSelectors.some(selector => element.matches(selector))
|
||||||
|
) {
|
||||||
|
return Infinity
|
||||||
|
}
|
||||||
|
return Math.round(dpi * currentWidth)
|
||||||
|
},
|
||||||
|
getHeight: (currentHeight, element) => {
|
||||||
|
if (
|
||||||
|
settings.options.originalImageInArticles &&
|
||||||
|
originalImageInArticlesSelectors.some(selector => element.matches(selector))
|
||||||
|
) {
|
||||||
|
return Infinity
|
||||||
|
}
|
||||||
|
return Math.round(dpi * currentHeight)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
walk(document.body, it => imageResolution(it, handleResolution))
|
||||||
allMutations(records => {
|
allMutations(records => {
|
||||||
records.forEach(record =>
|
records.forEach(record =>
|
||||||
record.addedNodes.forEach(node => {
|
record.addedNodes.forEach(node => {
|
||||||
if (node instanceof HTMLElement) {
|
if (node instanceof HTMLElement) {
|
||||||
imageResolution(dpi, node)
|
imageResolution(node, handleResolution)
|
||||||
if (node.nodeName.toUpperCase() !== 'IMG') {
|
if (node.nodeName.toUpperCase() !== 'IMG') {
|
||||||
walk(node, it => imageResolution(dpi, it))
|
walk(node, it => imageResolution(it, handleResolution))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -2,10 +2,11 @@
|
|||||||
/* eslint-disable yoda */
|
/* eslint-disable yoda */
|
||||||
import { defineComponentMetadata } from '@/components/define'
|
import { defineComponentMetadata } from '@/components/define'
|
||||||
import { CommentItem, CommentReplyItem } from '@/components/utils/comment-apis'
|
import { CommentItem, CommentReplyItem } from '@/components/utils/comment-apis'
|
||||||
|
import { select } from '@/core/spin-query'
|
||||||
|
|
||||||
// 新版评论区IP属地获取
|
// 新版评论区IP属地获取
|
||||||
const getIpLocation = (item: CommentReplyItem) => {
|
const getIpLocation = (item: CommentReplyItem) => {
|
||||||
const reply = item.vueProps
|
const reply = item.frameworkSpecificProps
|
||||||
return reply?.reply_control?.location ?? undefined
|
return reply?.reply_control?.location ?? undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,20 +273,37 @@ const observer = new MutationObserver(mutations => {
|
|||||||
observer.observe(document.head, { childList: true })
|
observer.observe(document.head, { childList: true })
|
||||||
|
|
||||||
const processItems = (items: CommentReplyItem[]) => {
|
const processItems = (items: CommentReplyItem[]) => {
|
||||||
items.forEach(item => {
|
items.forEach(async item => {
|
||||||
const location = getIpLocation(item)
|
const location = getIpLocation(item)
|
||||||
if (location !== undefined) {
|
if (location === undefined) {
|
||||||
const replyTime =
|
return
|
||||||
|
}
|
||||||
|
const replyTime = await (() => {
|
||||||
|
if (item.shadowDomEntry) {
|
||||||
|
return select(() => item.shadowDomEntry.querySelector('#pubdate'), {
|
||||||
|
queryInterval: 100,
|
||||||
|
maxRetry: 30,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
item.element.querySelector('.reply-info>.reply-time') ??
|
item.element.querySelector('.reply-info>.reply-time') ??
|
||||||
item.element.querySelector('.sub-reply-info>.sub-reply-time')
|
item.element.querySelector('.sub-reply-info>.sub-reply-time')
|
||||||
if (replyTime.childElementCount === 0) {
|
)
|
||||||
// 避免在评论更新的情况下重复添加
|
})()
|
||||||
const replyLocation = document.createElement('span')
|
if (replyTime === null) {
|
||||||
replyLocation.style.marginLeft = `${marginLeft}px`
|
return
|
||||||
replyLocation.innerText = location
|
|
||||||
replyTime.appendChild(replyLocation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const existingLocation = replyTime.querySelector('.ip-location') as HTMLElement | null
|
||||||
|
if (existingLocation !== null) {
|
||||||
|
existingLocation.innerText = location
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyLocation = document.createElement('span')
|
||||||
|
replyLocation.className = 'ip-location'
|
||||||
|
replyLocation.style.marginLeft = `${marginLeft}px`
|
||||||
|
replyLocation.innerText = location
|
||||||
|
replyTime.appendChild(replyLocation)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -74,6 +74,10 @@ const entry = async () => {
|
|||||||
match: /\/\/live\.bilibili\.com\//,
|
match: /\/\/live\.bilibili\.com\//,
|
||||||
param: 'session_id',
|
param: 'session_id',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
match: /\/\/live\.bilibili\.com\//,
|
||||||
|
param: 'is_room_feed',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
match: /\/\/www\.bilibili\.com\/bangumi\//,
|
match: /\/\/www\.bilibili\.com\/bangumi\//,
|
||||||
param: 'theme',
|
param: 'theme',
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
将弹幕中的 `\n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
将弹幕中的 `\n` 或 `/n` 替换为真实的换行, 注意这可能导致原先不重叠的弹幕发生重叠.
|
||||||
|
|||||||
@ -5,13 +5,39 @@ export const component = defineComponentMetadata({
|
|||||||
name: 'unescapeDanmaku',
|
name: 'unescapeDanmaku',
|
||||||
displayName: '弹幕转义',
|
displayName: '弹幕转义',
|
||||||
tags: [componentsTags.video],
|
tags: [componentsTags.video],
|
||||||
entry: () => {
|
options: {
|
||||||
const newLineRegex = /\\n/g
|
backSlash: {
|
||||||
|
defaultValue: true,
|
||||||
|
displayName: '对 \\n 转义',
|
||||||
|
},
|
||||||
|
forwardSlash: {
|
||||||
|
defaultValue: true,
|
||||||
|
displayName: '对 /n 转义',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
entry: ({ settings }) => {
|
||||||
|
const newLineRegex = (() => {
|
||||||
|
if (settings.options.backSlash && settings.options.forwardSlash) {
|
||||||
|
return /\\n|\/n/g
|
||||||
|
}
|
||||||
|
if (settings.options.backSlash) {
|
||||||
|
return /\\n/g
|
||||||
|
}
|
||||||
|
return /\/n/g
|
||||||
|
})()
|
||||||
|
const setText = (element: Element, text: string): void => {
|
||||||
|
const children = [...element.children]
|
||||||
|
if (children.length > 0) {
|
||||||
|
children.forEach(child => setText(child, text))
|
||||||
|
}
|
||||||
|
const textNodes = [...element.childNodes].filter(it => it.nodeType === Node.TEXT_NODE)
|
||||||
|
textNodes.forEach(node => (node.textContent = text))
|
||||||
|
}
|
||||||
forEachVideoDanmaku({
|
forEachVideoDanmaku({
|
||||||
added: danmaku => {
|
added: danmaku => {
|
||||||
if (newLineRegex.test(danmaku.text)) {
|
if (newLineRegex.test(danmaku.text)) {
|
||||||
const newText = danmaku.text.replace(newLineRegex, '\n')
|
const newText = danmaku.text.replace(newLineRegex, '\n')
|
||||||
danmaku.element.textContent = newText
|
setText(danmaku.element, newText)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Toast } from '@/core/toast'
|
import { Toast } from '@/core/toast'
|
||||||
import { formatFileSize, formatPercent } from '@/core/utils/formatters'
|
import { formatFileSize, formatPercent } from '@/core/utils/formatters'
|
||||||
import { getOrLoad, storeNames } from './database'
|
import { getOrLoad, storeNames } from './database'
|
||||||
|
import { RuntimeLibraryDefinition, RuntimeLibrary } from '@/core/runtime-library'
|
||||||
|
|
||||||
type OnProgress = (received: number, total: number) => void
|
type OnProgress = (received: number, total: number) => void
|
||||||
|
|
||||||
@ -56,8 +57,21 @@ export async function httpGet(url: string, onprogress: OnProgress) {
|
|||||||
return chunksAll
|
return chunksAll
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCacheOrFetch(key: string, url: string, loading: OnProgress) {
|
export async function getCacheOrFetch(
|
||||||
return getOrLoad(storeNames.cache, key, async () => httpGet(url, loading))
|
key: string,
|
||||||
|
library: RuntimeLibraryDefinition,
|
||||||
|
loading: OnProgress,
|
||||||
|
) {
|
||||||
|
return getOrLoad(storeNames.cache, key, async () => {
|
||||||
|
const content = await httpGet(library.url, loading)
|
||||||
|
const sha256 = await RuntimeLibrary.sha256(content)
|
||||||
|
if (sha256 !== library.sha256) {
|
||||||
|
throw new Error(
|
||||||
|
`Check integrity failed from ${library.url}, expected = ${library.sha256}, actual = ${sha256}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toBlobUrl(buffer: Uint8Array, mimeType: string) {
|
export function toBlobUrl(buffer: Uint8Array, mimeType: string) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "2.9.1",
|
"version": "2.9.2",
|
||||||
"author": "Grant Howard, Coulomb-G",
|
"author": "Grant Howard, Coulomb-G",
|
||||||
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -38,7 +38,7 @@
|
|||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
"require": [
|
"require": [
|
||||||
"[altCdn.library.lodash]"
|
"[altCdn.library.lodash.url]#sha256=[altCdn.library.lodash.sha256]"
|
||||||
],
|
],
|
||||||
"icon": "[altCdn.smallLogo]",
|
"icon": "[altCdn.smallLogo]",
|
||||||
"icon64": "[altCdn.logo]"
|
"icon64": "[altCdn.logo]"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
import { contentLoaded, fullyLoaded } from '@/core/life-cycle'
|
import { contentLoaded, fullyLoaded } from '@/core/life-cycle'
|
||||||
import { select } from '@/core/spin-query'
|
import { select } from '@/core/spin-query'
|
||||||
|
import { setupContainerQueryFeatureDetection } from '@/core/container-query'
|
||||||
|
|
||||||
export const compatibilityPatch = () => {
|
export const compatibilityPatch = () => {
|
||||||
contentLoaded(async () => {
|
contentLoaded(async () => {
|
||||||
@ -22,6 +23,8 @@ export const compatibilityPatch = () => {
|
|||||||
const { playerPolyfill } = await import('@/components/video/player-adaptor')
|
const { playerPolyfill } = await import('@/components/video/player-adaptor')
|
||||||
playerPolyfill()
|
playerPolyfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await setupContainerQueryFeatureDetection()
|
||||||
})
|
})
|
||||||
fullyLoaded(() => {
|
fullyLoaded(() => {
|
||||||
select('meta[name=spm_prefix]').then(spm => {
|
select('meta[name=spm_prefix]').then(spm => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<VIcon
|
<VIcon
|
||||||
class="switch-icon"
|
class="switch-icon"
|
||||||
icon="mdi-checkbox-marked-circle-outline"
|
icon="mdi-checkbox-marked-circle-outline"
|
||||||
:size="smallSize ? 16 : 22"
|
:size="smallSize ? 16 : 24"
|
||||||
></VIcon>
|
></VIcon>
|
||||||
{{ options.optionDisplayName }}
|
{{ options.optionDisplayName }}
|
||||||
</VButton>
|
</VButton>
|
||||||
@ -128,6 +128,7 @@ export default Vue.extend({
|
|||||||
}
|
}
|
||||||
.switch-icon {
|
.switch-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
opacity: 0.75;
|
||||||
transform: scale(0.9);
|
transform: scale(0.9);
|
||||||
}
|
}
|
||||||
.dim {
|
.dim {
|
||||||
|
|||||||
@ -60,16 +60,16 @@ const optionsMetadata = defineOptionsMetadata({
|
|||||||
|
|
||||||
export type Options = OptionsOfMetadata<typeof optionsMetadata>
|
export type Options = OptionsOfMetadata<typeof optionsMetadata>
|
||||||
|
|
||||||
const entry: ComponentEntry<Options> = async ({ settings: { options: opt } }) => {
|
const entry: ComponentEntry<Options> = async ({ settings: { options } }) => {
|
||||||
if (isIframe()) {
|
if (isIframe()) {
|
||||||
return checkerMethods
|
return checkerMethods
|
||||||
}
|
}
|
||||||
const now = Number(new Date())
|
const now = Number(new Date())
|
||||||
const duration = now - opt.lastUpdateCheck
|
const duration = now - options.lastUpdateCheck
|
||||||
|
|
||||||
const isDurationExceeded = duration >= opt.minimumDuration
|
const isDurationExceeded = duration >= options.minimumDuration
|
||||||
const isVersionOutdated = new Version(meta.version).greaterThan(
|
const isVersionOutdated = new Version(meta.version).greaterThan(
|
||||||
new Version(opt.lastInstalledVersion),
|
new Version(options.lastInstalledVersion),
|
||||||
)
|
)
|
||||||
if (isDurationExceeded) {
|
if (isDurationExceeded) {
|
||||||
coreApis.lifeCycle.fullyLoaded(() => silentCheckUpdate())
|
coreApis.lifeCycle.fullyLoaded(() => silentCheckUpdate())
|
||||||
|
|||||||
@ -249,7 +249,9 @@ const newSwitchEntry = <O extends UnknownOptions, N extends string, S extends st
|
|||||||
addComponentListener(
|
addComponentListener(
|
||||||
`${component.name}.${key}`,
|
`${component.name}.${key}`,
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
document.body.classList.toggle(`${component.name}-${key}`, value)
|
const id = `${component.name}-${key}`
|
||||||
|
document.body.classList.toggle(id, value)
|
||||||
|
document.documentElement.style.setProperty(`--${id}`, value.toString())
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -163,6 +163,21 @@ export type ComponentEntry<O extends UnknownOptions = UnknownOptions, T = unknow
|
|||||||
context: ComponentEntryContext<O>,
|
context: ComponentEntryContext<O>,
|
||||||
) => T | Promise<T>
|
) => T | Promise<T>
|
||||||
|
|
||||||
|
export interface InstantStyleDefinition {
|
||||||
|
/** 样式ID */
|
||||||
|
name: string
|
||||||
|
/** 样式内容, 可以是一个导入样式的函数 */
|
||||||
|
style: string | (() => Promise<{ default: string }>)
|
||||||
|
}
|
||||||
|
export interface DomInstantStyleDefinition extends InstantStyleDefinition {
|
||||||
|
/** 设为 `true` 则注入到 `document.body` 末尾, 否则注入到 `document.head` 末尾 */
|
||||||
|
important?: boolean
|
||||||
|
}
|
||||||
|
export interface ShadowDomInstantStyleDefinition extends InstantStyleDefinition {
|
||||||
|
/** 设为 `true` 则注入到 Shadow DOM 中 */
|
||||||
|
shadowDom?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** 带有函数/复杂对象的组件信息 */
|
/** 带有函数/复杂对象的组件信息 */
|
||||||
export interface FunctionalMetadata<O extends UnknownOptions = UnknownOptions> {
|
export interface FunctionalMetadata<O extends UnknownOptions = UnknownOptions> {
|
||||||
/** 主入口, 重新开启时不会再运行 */
|
/** 主入口, 重新开启时不会再运行 */
|
||||||
@ -170,14 +185,7 @@ export interface FunctionalMetadata<O extends UnknownOptions = UnknownOptions> {
|
|||||||
/** 导出小组件 */
|
/** 导出小组件 */
|
||||||
widget?: Omit<Widget, 'name'>
|
widget?: Omit<Widget, 'name'>
|
||||||
/** 首屏样式, 会尽快注入 (before DCL) */
|
/** 首屏样式, 会尽快注入 (before DCL) */
|
||||||
instantStyles?: {
|
instantStyles?: (DomInstantStyleDefinition | ShadowDomInstantStyleDefinition)[]
|
||||||
/** 样式ID */
|
|
||||||
name: string
|
|
||||||
/** 样式内容, 可以是一个导入样式的函数 */
|
|
||||||
style: string | (() => Promise<{ default: string }>)
|
|
||||||
/** 设为`true`则注入到`document.body`末尾, 否则注入到`document.head`末尾 */
|
|
||||||
important?: boolean
|
|
||||||
}[]
|
|
||||||
/** 重新开启时执行 */
|
/** 重新开启时执行 */
|
||||||
reload?: Executable
|
reload?: Executable
|
||||||
/** 关闭时执行 */
|
/** 关闭时执行 */
|
||||||
|
|||||||
@ -91,8 +91,8 @@ export const uninstallComponent = async (nameOrDisplayName: string) => {
|
|||||||
// 移除可能的 instantStyles
|
// 移除可能的 instantStyles
|
||||||
const { instantStyles } = components[index]
|
const { instantStyles } = components[index]
|
||||||
if (instantStyles) {
|
if (instantStyles) {
|
||||||
const { removeStyle } = await import('@/core/style')
|
const { removeInstantStyle } = await import('@/core/style')
|
||||||
instantStyles.forEach(s => removeStyle(s.name))
|
instantStyles.forEach(s => removeInstantStyle(s))
|
||||||
}
|
}
|
||||||
// 移除可能的 widgets
|
// 移除可能的 widgets
|
||||||
componentSettings.enabled = false
|
componentSettings.enabled = false
|
||||||
|
|||||||
@ -1,27 +1,18 @@
|
|||||||
import { childListSubtree } from '@/core/observer'
|
|
||||||
import type { CommentItem } from '../comment-item'
|
import type { CommentItem } from '../comment-item'
|
||||||
import type { CommentCallbackInput, CommentCallbackPair, CommentItemCallback } from '../types'
|
|
||||||
import type { CommentReplyItem } from '../reply-item'
|
import type { CommentReplyItem } from '../reply-item'
|
||||||
import { getRandomId } from '@/core/utils'
|
import type { CommentCallbackInput, CommentCallbackPair, CommentItemCallback } from '../types'
|
||||||
|
|
||||||
/** 表示一个评论区 */
|
|
||||||
export abstract class CommentArea {
|
export abstract class CommentArea {
|
||||||
/** 对应元素 */
|
|
||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
/** 评论列表 */
|
|
||||||
items: CommentItem[] = []
|
items: CommentItem[] = []
|
||||||
/** 与之关联的 MutationObserver */
|
|
||||||
protected observer?: MutationObserver
|
|
||||||
protected itemCallbacks: CommentCallbackPair<CommentItemCallback>[] = []
|
protected itemCallbacks: CommentCallbackPair<CommentItemCallback>[] = []
|
||||||
protected static replyItemClasses = ['list-item.reply-wrap', 'reply-item']
|
|
||||||
protected static replyItemSelector = CommentArea.replyItemClasses.map(c => `.${c}`).join(',')
|
|
||||||
|
|
||||||
constructor(element: HTMLElement) {
|
constructor(element: HTMLElement) {
|
||||||
this.element = element
|
this.element = element
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract parseCommentItem(element: HTMLElement): CommentItem
|
abstract observe(): void | Promise<void>
|
||||||
abstract getCommentId(element: HTMLElement): string
|
abstract disconnect(): void | Promise<void>
|
||||||
abstract addMenuItem(
|
abstract addMenuItem(
|
||||||
item: CommentReplyItem,
|
item: CommentReplyItem,
|
||||||
config: {
|
config: {
|
||||||
@ -29,80 +20,7 @@ export abstract class CommentArea {
|
|||||||
text: string
|
text: string
|
||||||
action: (e: MouseEvent) => void
|
action: (e: MouseEvent) => void
|
||||||
},
|
},
|
||||||
): void
|
): void | Promise<void>
|
||||||
|
|
||||||
protected isCommentItem(n: Node): n is HTMLElement {
|
|
||||||
return n instanceof HTMLElement && n.matches(CommentArea.replyItemSelector)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 在每一轮 CommentItem 解析前调用 */
|
|
||||||
protected beforeParse(elements: HTMLElement[]) {
|
|
||||||
return lodash.noop(elements)
|
|
||||||
}
|
|
||||||
|
|
||||||
observeItems() {
|
|
||||||
if (this.observer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
performance.mark('observeItems start')
|
|
||||||
const elements = dqa(this.element, CommentArea.replyItemSelector) as HTMLElement[]
|
|
||||||
if (elements.length > 0) {
|
|
||||||
this.beforeParse(elements)
|
|
||||||
this.items = elements.map(it => this.parseCommentItem(it as HTMLElement))
|
|
||||||
}
|
|
||||||
this.items.forEach(item => {
|
|
||||||
this.itemCallbacks.forEach(c => c.added?.(item))
|
|
||||||
})
|
|
||||||
;[this.observer] = childListSubtree(this.element, records => {
|
|
||||||
const observerCallId = getRandomId()
|
|
||||||
performance.mark(`observeItems subtree start ${observerCallId}`)
|
|
||||||
const addedCommentElements: HTMLElement[] = []
|
|
||||||
const removedCommentElements: HTMLElement[] = []
|
|
||||||
records.forEach(r => {
|
|
||||||
r.addedNodes.forEach(n => {
|
|
||||||
if (this.isCommentItem(n)) {
|
|
||||||
addedCommentElements.push(n)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
r.removedNodes.forEach(n => {
|
|
||||||
if (this.isCommentItem(n)) {
|
|
||||||
removedCommentElements.push(n)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if (addedCommentElements.length > 0) {
|
|
||||||
this.beforeParse(addedCommentElements)
|
|
||||||
}
|
|
||||||
addedCommentElements.forEach(n => {
|
|
||||||
const commentItem = this.parseCommentItem(n)
|
|
||||||
this.items.push(commentItem)
|
|
||||||
this.itemCallbacks.forEach(c => c.added?.(commentItem))
|
|
||||||
})
|
|
||||||
removedCommentElements.forEach(n => {
|
|
||||||
const id = this.getCommentId(n)
|
|
||||||
const index = this.items.findIndex(item => item.id === id)
|
|
||||||
if (index !== -1) {
|
|
||||||
const [commentItem] = this.items.splice(index, 1)
|
|
||||||
this.itemCallbacks.forEach(c => c.removed?.(commentItem))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
performance.mark(`observeItems subtree end ${observerCallId}`)
|
|
||||||
performance.measure(
|
|
||||||
`observeItems subtree ${observerCallId}`,
|
|
||||||
`observeItems subtree start ${observerCallId}`,
|
|
||||||
`observeItems subtree end ${observerCallId}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
performance.mark('observeItems end')
|
|
||||||
performance.measure('observeItems', 'observeItems start', 'observeItems end')
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.observer?.disconnect()
|
|
||||||
this.items.forEach(item => {
|
|
||||||
this.itemCallbacks.forEach(pair => pair.removed?.(item))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static resolveCallbackPair<T extends (...args: unknown[]) => void>(
|
static resolveCallbackPair<T extends (...args: unknown[]) => void>(
|
||||||
input: CommentCallbackInput<T>,
|
input: CommentCallbackInput<T>,
|
||||||
|
|||||||
87
src/components/utils/comment/areas/dom.ts
Normal file
87
src/components/utils/comment/areas/dom.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { CommentArea } from './base'
|
||||||
|
import { childListSubtree } from '@/core/observer'
|
||||||
|
import type { CommentItem } from '../comment-item'
|
||||||
|
import { getRandomId } from '@/core/utils'
|
||||||
|
|
||||||
|
/** 表示一个基于常规 DOM 结构的评论区 */
|
||||||
|
export abstract class DomCommentArea extends CommentArea {
|
||||||
|
protected observer?: MutationObserver
|
||||||
|
protected static replyItemClasses = ['list-item.reply-wrap', 'reply-item']
|
||||||
|
protected static replyItemSelector = DomCommentArea.replyItemClasses.map(c => `.${c}`).join(',')
|
||||||
|
|
||||||
|
abstract parseCommentItem(element: HTMLElement): CommentItem
|
||||||
|
abstract getCommentId(element: HTMLElement): string
|
||||||
|
|
||||||
|
protected isCommentItem(n: Node): n is HTMLElement {
|
||||||
|
return n instanceof HTMLElement && n.matches(DomCommentArea.replyItemSelector)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在每一轮 CommentItem 解析前调用 */
|
||||||
|
protected beforeParse(elements: HTMLElement[]) {
|
||||||
|
return lodash.noop(elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
observe() {
|
||||||
|
if (this.observer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
performance.mark('observeItems start')
|
||||||
|
const elements = dqa(this.element, DomCommentArea.replyItemSelector) as HTMLElement[]
|
||||||
|
if (elements.length > 0) {
|
||||||
|
this.beforeParse(elements)
|
||||||
|
this.items = elements.map(it => this.parseCommentItem(it as HTMLElement))
|
||||||
|
}
|
||||||
|
this.items.forEach(item => {
|
||||||
|
this.itemCallbacks.forEach(c => c.added?.(item))
|
||||||
|
})
|
||||||
|
;[this.observer] = childListSubtree(this.element, records => {
|
||||||
|
const observerCallId = getRandomId()
|
||||||
|
performance.mark(`observeItems subtree start ${observerCallId}`)
|
||||||
|
const addedCommentElements: HTMLElement[] = []
|
||||||
|
const removedCommentElements: HTMLElement[] = []
|
||||||
|
records.forEach(r => {
|
||||||
|
r.addedNodes.forEach(n => {
|
||||||
|
if (this.isCommentItem(n)) {
|
||||||
|
addedCommentElements.push(n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
r.removedNodes.forEach(n => {
|
||||||
|
if (this.isCommentItem(n)) {
|
||||||
|
removedCommentElements.push(n)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (addedCommentElements.length > 0) {
|
||||||
|
this.beforeParse(addedCommentElements)
|
||||||
|
}
|
||||||
|
addedCommentElements.forEach(n => {
|
||||||
|
const commentItem = this.parseCommentItem(n)
|
||||||
|
this.items.push(commentItem)
|
||||||
|
this.itemCallbacks.forEach(c => c.added?.(commentItem))
|
||||||
|
})
|
||||||
|
removedCommentElements.forEach(n => {
|
||||||
|
const id = this.getCommentId(n)
|
||||||
|
const index = this.items.findIndex(item => item.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
const [commentItem] = this.items.splice(index, 1)
|
||||||
|
this.itemCallbacks.forEach(c => c.removed?.(commentItem))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
performance.mark(`observeItems subtree end ${observerCallId}`)
|
||||||
|
performance.measure(
|
||||||
|
`observeItems subtree ${observerCallId}`,
|
||||||
|
`observeItems subtree start ${observerCallId}`,
|
||||||
|
`observeItems subtree end ${observerCallId}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
performance.mark('observeItems end')
|
||||||
|
performance.measure('observeItems', 'observeItems start', 'observeItems end')
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.observer?.disconnect()
|
||||||
|
this.items.forEach(item => {
|
||||||
|
this.itemCallbacks.forEach(pair => pair.removed?.(item))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import { childList } from '@/core/observer'
|
import { childList } from '@/core/observer'
|
||||||
import { CommentItem } from '../comment-item'
|
import { CommentItem } from '../comment-item'
|
||||||
import { CommentReplyItem } from '../reply-item'
|
import { CommentReplyItem } from '../reply-item'
|
||||||
import { CommentArea } from './base'
|
import { DomCommentArea } from './dom'
|
||||||
|
|
||||||
export class CommentAreaV1 extends CommentArea {
|
export class CommentAreaV1 extends DomCommentArea {
|
||||||
addMenuItem(
|
addMenuItem(
|
||||||
item: CommentReplyItem,
|
item: CommentReplyItem,
|
||||||
config: { className: string; text: string; action: (e: MouseEvent) => void },
|
config: { className: string; text: string; action: (e: MouseEvent) => void },
|
||||||
@ -44,7 +44,7 @@ export class CommentAreaV1 extends CommentArea {
|
|||||||
content: replyElement.querySelector('.text-con').textContent,
|
content: replyElement.querySelector('.text-con').textContent,
|
||||||
timeText: replyElement.querySelector('.info .time, .info .time-location').textContent,
|
timeText: replyElement.querySelector('.info .time, .info .time-location').textContent,
|
||||||
likes: parseInt(replyElement.querySelector('.info .like span').textContent),
|
likes: parseInt(replyElement.querySelector('.info .like span').textContent),
|
||||||
vueProps: undefined,
|
frameworkSpecificProps: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const item = new CommentItem({
|
const item = new CommentItem({
|
||||||
@ -56,7 +56,7 @@ export class CommentAreaV1 extends CommentArea {
|
|||||||
timeText: element.querySelector('.con .info .time, .info .time-location').textContent,
|
timeText: element.querySelector('.con .info .time, .info .time-location').textContent,
|
||||||
likes: parseInt(element.querySelector('.con .like span').textContent),
|
likes: parseInt(element.querySelector('.con .like span').textContent),
|
||||||
replies: [],
|
replies: [],
|
||||||
vueProps: undefined,
|
frameworkSpecificProps: undefined,
|
||||||
})
|
})
|
||||||
if (dq(element, '.reply-box .view-more')) {
|
if (dq(element, '.reply-box .view-more')) {
|
||||||
const replyBox = dq(element, '.reply-box') as HTMLElement
|
const replyBox = dq(element, '.reply-box') as HTMLElement
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable no-underscore-dangle */
|
/* eslint-disable no-underscore-dangle */
|
||||||
import { childList } from '@/core/observer'
|
import { childList } from '@/core/observer'
|
||||||
import { CommentItem } from '../comment-item'
|
import { CommentItem } from '../comment-item'
|
||||||
import { CommentArea } from './base'
|
import { DomCommentArea } from './dom'
|
||||||
import { CommentReplyItem } from '../reply-item'
|
import { CommentReplyItem } from '../reply-item'
|
||||||
import { HTMLElementWithVue, VNodeManager } from '../vnode-manager'
|
import { HTMLElementWithVue, VNodeManager } from '../vnode-manager'
|
||||||
|
|
||||||
export class CommentAreaV2 extends CommentArea {
|
export class CommentAreaV2 extends DomCommentArea {
|
||||||
private vnodeManager: VNodeManager
|
private vnodeManager: VNodeManager
|
||||||
|
|
||||||
constructor(element: HTMLElement) {
|
constructor(element: HTMLElement) {
|
||||||
@ -94,7 +94,7 @@ export class CommentAreaV2 extends CommentArea {
|
|||||||
content: r.content.message,
|
content: r.content.message,
|
||||||
time: r.ctime * 1000,
|
time: r.ctime * 1000,
|
||||||
likes: r.like,
|
likes: r.like,
|
||||||
vueProps: r,
|
frameworkSpecificProps: r,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ export class CommentAreaV2 extends CommentArea {
|
|||||||
return img.img_src
|
return img.img_src
|
||||||
}),
|
}),
|
||||||
replies: parseReplies(),
|
replies: parseReplies(),
|
||||||
vueProps,
|
frameworkSpecificProps: vueProps,
|
||||||
})
|
})
|
||||||
if (item.replies.length < vueProps.rcount) {
|
if (item.replies.length < vueProps.rcount) {
|
||||||
const replyBox = dq(element, '.sub-reply-list')
|
const replyBox = dq(element, '.sub-reply-list')
|
||||||
|
|||||||
202
src/components/utils/comment/areas/v3.ts
Normal file
202
src/components/utils/comment/areas/v3.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { ShadowDomObserver, shadowDomObserver, ShadowRootEvents } from '@/core/shadow-root'
|
||||||
|
import { CommentReplyItem } from '../reply-item'
|
||||||
|
import { CommentArea } from './base'
|
||||||
|
import { ShadowDomEntry } from '@/core/shadow-root/dom-entry'
|
||||||
|
import { CommentItem } from '../comment-item'
|
||||||
|
import { deleteValue } from '@/core/utils'
|
||||||
|
import { select } from '@/core/spin-query'
|
||||||
|
|
||||||
|
export class CommentAreaV3 extends CommentArea {
|
||||||
|
protected static commentItemSelectors = 'bili-comment-thread-renderer'
|
||||||
|
protected static commentReplyItemSelectors = 'bili-comment-reply-renderer'
|
||||||
|
protected static commentActionsSelectors = 'bili-comment-action-buttons-renderer'
|
||||||
|
protected static commentMenuSelectors = 'bili-comment-menu'
|
||||||
|
protected static MenuItemConfigSymbol = Symbol.for('CommentAreaV3.MenuItemConfigSymbol')
|
||||||
|
protected static getLitData(entry: ShadowDomEntry) {
|
||||||
|
const host = entry.element as HTMLElement & { __data: any }
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
return host.__data
|
||||||
|
}
|
||||||
|
|
||||||
|
static isV3Area(element: HTMLElement) {
|
||||||
|
return element.tagName.toLowerCase() === 'bili-comments'
|
||||||
|
}
|
||||||
|
|
||||||
|
public commentAreaEntry: ShadowDomEntry
|
||||||
|
|
||||||
|
protected shadowDomObserver: ShadowDomObserver
|
||||||
|
protected itemEntryMap = new Map<ShadowDomEntry, CommentItem>()
|
||||||
|
|
||||||
|
private handleEntryAdded: (e: CustomEvent<ShadowDomEntry>) => void
|
||||||
|
private handleEntryRemoved: (e: CustomEvent<ShadowDomEntry>) => void
|
||||||
|
private areaObserverDisposer: () => void
|
||||||
|
|
||||||
|
constructor(element: HTMLElement) {
|
||||||
|
super(element)
|
||||||
|
shadowDomObserver.observe()
|
||||||
|
this.shadowDomObserver = shadowDomObserver
|
||||||
|
}
|
||||||
|
|
||||||
|
protected matchChildEntryByReplyId(
|
||||||
|
parent: ShadowDomEntry,
|
||||||
|
childSelectors: string,
|
||||||
|
replyId: string,
|
||||||
|
) {
|
||||||
|
const children = parent.querySelectorAllAsEntry(childSelectors)
|
||||||
|
return children.find(r => CommentAreaV3.getLitData(r).rpid_str === replyId)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getReplyItemElement(parent: ShadowDomEntry, replyId: string): HTMLElement {
|
||||||
|
return this.matchChildEntryByReplyId(parent, CommentAreaV3.commentReplyItemSelectors, replyId)
|
||||||
|
?.element as HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseCommentReplyItem(replyEntry: ShadowDomEntry): CommentReplyItem {
|
||||||
|
const replyLitData = CommentAreaV3.getLitData(replyEntry)
|
||||||
|
return new CommentReplyItem({
|
||||||
|
id: replyLitData.rpid_str,
|
||||||
|
element: replyEntry.element as HTMLElement,
|
||||||
|
userId: replyLitData.member.mid,
|
||||||
|
userName: replyLitData.member.uname,
|
||||||
|
content: replyLitData.content.message,
|
||||||
|
time: replyLitData.ctime * 1000,
|
||||||
|
likes: replyLitData.like,
|
||||||
|
frameworkSpecificProps: replyLitData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseCommentItem(entry: ShadowDomEntry): CommentItem {
|
||||||
|
const litData = CommentAreaV3.getLitData(entry)
|
||||||
|
const getReplies = () =>
|
||||||
|
entry
|
||||||
|
.querySelectorAllAsEntry(CommentAreaV3.commentReplyItemSelectors)
|
||||||
|
.map(replyEntry => this.parseCommentReplyItem(replyEntry))
|
||||||
|
const item = new CommentItem({
|
||||||
|
id: litData.rpid_str,
|
||||||
|
element: entry.element as HTMLElement,
|
||||||
|
userId: litData.member.mid,
|
||||||
|
userName: litData.member.uname,
|
||||||
|
content: litData.content.message,
|
||||||
|
time: litData.ctime * 1000,
|
||||||
|
likes: litData.like,
|
||||||
|
pictures: litData.content?.pictures?.map((img: { img_src: string }) => {
|
||||||
|
return img.img_src
|
||||||
|
}),
|
||||||
|
replies: getReplies(),
|
||||||
|
frameworkSpecificProps: litData,
|
||||||
|
})
|
||||||
|
entry.addEventListener(
|
||||||
|
ShadowRootEvents.Updated,
|
||||||
|
lodash.debounce(() => {
|
||||||
|
const newReplies = getReplies()
|
||||||
|
const hasUpdate =
|
||||||
|
item.replies.length !== newReplies.length ||
|
||||||
|
!item.replies.every(r => newReplies.some(newReply => newReply.id === r.id))
|
||||||
|
if (hasUpdate) {
|
||||||
|
item.replies = newReplies
|
||||||
|
item.dispatchRepliesUpdate(item.replies)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addCommentItem(entry: ShadowDomEntry) {
|
||||||
|
if (this.itemEntryMap.has(entry)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const commentItem = this.parseCommentItem(entry)
|
||||||
|
this.itemEntryMap.set(entry, commentItem)
|
||||||
|
this.items.push(commentItem)
|
||||||
|
this.itemCallbacks.forEach(c => c.added?.(commentItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeCommentItem(entry: ShadowDomEntry) {
|
||||||
|
const itemToRemove = this.items.find(it => it.element === entry.element)
|
||||||
|
this.itemEntryMap.delete(entry)
|
||||||
|
deleteValue(this.items, it => it === itemToRemove)
|
||||||
|
this.itemCallbacks.forEach(c => c.removed?.(itemToRemove))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected observeCommentItems() {
|
||||||
|
const entries = this.commentAreaEntry.querySelectorAllAsEntry(
|
||||||
|
CommentAreaV3.commentItemSelectors,
|
||||||
|
)
|
||||||
|
entries.forEach(entry => this.addCommentItem(entry))
|
||||||
|
|
||||||
|
this.handleEntryAdded = (e: CustomEvent<ShadowDomEntry>) => {
|
||||||
|
const entry = e.detail
|
||||||
|
if (entry.element.matches(CommentAreaV3.commentItemSelectors)) {
|
||||||
|
this.addCommentItem(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.commentAreaEntry.addEventListener(ShadowRootEvents.Added, this.handleEntryAdded)
|
||||||
|
this.handleEntryRemoved = (e: CustomEvent<ShadowDomEntry>) => {
|
||||||
|
const entry = e.detail
|
||||||
|
if (!this.itemEntryMap.has(entry)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.removeCommentItem(entry)
|
||||||
|
}
|
||||||
|
this.commentAreaEntry.addEventListener(ShadowRootEvents.Removed, this.handleEntryRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
|
observe() {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
this.areaObserverDisposer = this.shadowDomObserver.watchShadowDom({
|
||||||
|
added: shadowDom => {
|
||||||
|
if (shadowDom.element !== this.element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.commentAreaEntry = shadowDom
|
||||||
|
this.observeCommentItems()
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.commentAreaEntry.removeEventListener(ShadowRootEvents.Added, this.handleEntryAdded)
|
||||||
|
this.commentAreaEntry.removeEventListener(ShadowRootEvents.Removed, this.handleEntryRemoved)
|
||||||
|
this.areaObserverDisposer?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMenuItem(
|
||||||
|
item: CommentReplyItem,
|
||||||
|
config: { className: string; text: string; action: (e: MouseEvent) => void },
|
||||||
|
) {
|
||||||
|
const itemEntry = item.shadowDomEntry
|
||||||
|
if (itemEntry === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = await select(() =>
|
||||||
|
this.matchChildEntryByReplyId(itemEntry, CommentAreaV3.commentActionsSelectors, item.id),
|
||||||
|
)
|
||||||
|
if (!actions) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = actions.querySelectorAsEntry(CommentAreaV3.commentMenuSelectors)
|
||||||
|
if (!menu) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = menu.querySelector('#options')
|
||||||
|
const alreadyAdded = list.querySelector(`li.${config.className}`)
|
||||||
|
if (alreadyAdded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItem = document.createElement('li')
|
||||||
|
listItem.innerHTML = config.text
|
||||||
|
listItem.className = config.className
|
||||||
|
listItem[CommentAreaV3.MenuItemConfigSymbol] = config
|
||||||
|
listItem.addEventListener('click', e => {
|
||||||
|
config.action(e)
|
||||||
|
;(menu.element as HTMLElement).style.setProperty('--bili-comment-menu-display', null)
|
||||||
|
})
|
||||||
|
list.appendChild(listItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ export class CommentAreaManager {
|
|||||||
commentAreas: CommentArea[] = []
|
commentAreas: CommentArea[] = []
|
||||||
commentAreaCallbacks: CommentCallbackPair<CommentAreaCallback>[] = []
|
commentAreaCallbacks: CommentCallbackPair<CommentAreaCallback>[] = []
|
||||||
|
|
||||||
protected static commentAreaClasses = ['bili-comment', 'bb-comment']
|
protected static commentAreaSelectors = '.bili-comment, .bb-comment, bili-comments'
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
allMutations(records => {
|
allMutations(records => {
|
||||||
@ -22,19 +22,14 @@ export class CommentAreaManager {
|
|||||||
r.addedNodes.forEach(n => this.observeAreas(n))
|
r.addedNodes.forEach(n => this.observeAreas(n))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
dqa(CommentAreaManager.commentAreaClasses.map(c => `.${c}`).join(',')).forEach(it =>
|
dqa(CommentAreaManager.commentAreaSelectors).forEach(it => this.observeAreas(it))
|
||||||
this.observeAreas(it),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
observeAreas(node: Node) {
|
async observeAreas(node: Node) {
|
||||||
if (
|
if (node instanceof HTMLElement && node.matches(CommentAreaManager.commentAreaSelectors)) {
|
||||||
node instanceof HTMLElement &&
|
|
||||||
CommentAreaManager.commentAreaClasses.some(c => node.classList.contains(c))
|
|
||||||
) {
|
|
||||||
const area = getCommentArea(node)
|
const area = getCommentArea(node)
|
||||||
this.commentAreas.push(area)
|
this.commentAreas.push(area)
|
||||||
area.observeItems()
|
await area.observe()
|
||||||
this.commentAreaCallbacks.forEach(c => c.added?.(area))
|
this.commentAreaCallbacks.forEach(c => c.added?.(area))
|
||||||
const [observer] = childList(area.element.parentElement, records => {
|
const [observer] = childList(area.element.parentElement, records => {
|
||||||
records.forEach(r => {
|
records.forEach(r => {
|
||||||
@ -42,7 +37,7 @@ export class CommentAreaManager {
|
|||||||
if (removedNode === area.element) {
|
if (removedNode === area.element) {
|
||||||
deleteValue(this.commentAreas, a => a === area)
|
deleteValue(this.commentAreas, a => a === area)
|
||||||
this.commentAreaCallbacks.forEach(c => c.removed?.(area))
|
this.commentAreaCallbacks.forEach(c => c.removed?.(area))
|
||||||
area.destroy()
|
area.disconnect()
|
||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import type { CommentArea } from './areas/base'
|
import type { CommentArea } from './areas/base'
|
||||||
import { CommentAreaV1 } from './areas/v1'
|
import { CommentAreaV1 } from './areas/v1'
|
||||||
import { CommentAreaV2 } from './areas/v2'
|
import { CommentAreaV2 } from './areas/v2'
|
||||||
|
import { CommentAreaV3 } from './areas/v3'
|
||||||
|
|
||||||
export const getCommentArea = (element: HTMLElement): CommentArea => {
|
export const getCommentArea = (element: HTMLElement): CommentArea => {
|
||||||
|
if (CommentAreaV3.isV3Area(element)) {
|
||||||
|
return new CommentAreaV3(element)
|
||||||
|
}
|
||||||
if (CommentAreaV2.isV2Area(element)) {
|
if (CommentAreaV2.isV2Area(element)) {
|
||||||
return new CommentAreaV2(element)
|
return new CommentAreaV2(element)
|
||||||
}
|
}
|
||||||
@ -12,3 +16,4 @@ export const getCommentArea = (element: HTMLElement): CommentArea => {
|
|||||||
export * from './areas/base'
|
export * from './areas/base'
|
||||||
export * from './areas/v1'
|
export * from './areas/v1'
|
||||||
export * from './areas/v2'
|
export * from './areas/v2'
|
||||||
|
export * from './areas/v3'
|
||||||
|
|||||||
@ -13,7 +13,9 @@ export class CommentItem extends CommentReplyItem {
|
|||||||
/** 回复 */
|
/** 回复 */
|
||||||
replies: CommentReplyItem[]
|
replies: CommentReplyItem[]
|
||||||
|
|
||||||
constructor(initParams: Omit<CommentItem, keyof EventTarget | 'dispatchRepliesUpdate'>) {
|
constructor(
|
||||||
|
initParams: Omit<CommentItem, keyof EventTarget | 'dispatchRepliesUpdate' | 'shadowDomEntry'>,
|
||||||
|
) {
|
||||||
super(initParams)
|
super(initParams)
|
||||||
this.pictures = initParams.pictures ?? []
|
this.pictures = initParams.pictures ?? []
|
||||||
this.replies = initParams.replies
|
this.replies = initParams.replies
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { ShadowDomEntry, ShadowDomEntrySymbol } from '@/core/shadow-root/dom-entry'
|
||||||
|
|
||||||
/** 表示一条评论回复 */
|
/** 表示一条评论回复 */
|
||||||
export class CommentReplyItem extends EventTarget {
|
export class CommentReplyItem extends EventTarget {
|
||||||
/** 对应元素 */
|
/** 对应元素 */
|
||||||
@ -16,10 +18,10 @@ export class CommentReplyItem extends EventTarget {
|
|||||||
time?: number
|
time?: number
|
||||||
/** 点赞数 */
|
/** 点赞数 */
|
||||||
likes: number
|
likes: number
|
||||||
/** 对应的 Vue Props */
|
/** 对应的框架特定 Props (可能是 Vue 或者 Lit) */
|
||||||
vueProps: any
|
frameworkSpecificProps: any
|
||||||
|
|
||||||
constructor(initParams: Omit<CommentReplyItem, keyof EventTarget>) {
|
constructor(initParams: Omit<CommentReplyItem, keyof EventTarget | 'shadowDomEntry'>) {
|
||||||
super()
|
super()
|
||||||
this.element = initParams.element
|
this.element = initParams.element
|
||||||
this.id = initParams.id
|
this.id = initParams.id
|
||||||
@ -29,6 +31,11 @@ export class CommentReplyItem extends EventTarget {
|
|||||||
this.timeText = initParams.timeText
|
this.timeText = initParams.timeText
|
||||||
this.time = initParams.time
|
this.time = initParams.time
|
||||||
this.likes = initParams.likes
|
this.likes = initParams.likes
|
||||||
this.vueProps = initParams.vueProps
|
this.frameworkSpecificProps = initParams.frameworkSpecificProps
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 对应元素的 ShadowDomEntry */
|
||||||
|
get shadowDomEntry(): ShadowDomEntry {
|
||||||
|
return this.element[ShadowDomEntrySymbol]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/core/container-query.ts
Normal file
15
src/core/container-query.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const setupContainerQueryFeatureDetection = async () => {
|
||||||
|
document.documentElement.style.setProperty('--container-query-feature-detection', 'true')
|
||||||
|
const bodyStyle = new CSSStyleSheet()
|
||||||
|
await bodyStyle.replace(
|
||||||
|
'@container style(--container-query-feature-detection) { body { --container-query-supported: true } }',
|
||||||
|
)
|
||||||
|
document.adoptedStyleSheets.push(bodyStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isContainerStyleQuerySupported = lodash.once(() => {
|
||||||
|
return (
|
||||||
|
window.getComputedStyle(document.body).getPropertyValue('--container-query-supported') ===
|
||||||
|
'true'
|
||||||
|
)
|
||||||
|
})
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import * as ajax from '@/core/ajax'
|
import * as ajax from '@/core/ajax'
|
||||||
import * as cdnTypes from '@/core/cdn-types'
|
import * as cdnTypes from '@/core/cdn-types'
|
||||||
|
import * as containerQuery from '@/core/container-query'
|
||||||
import * as download from '@/core/download'
|
import * as download from '@/core/download'
|
||||||
import * as dialog from '@/core/dialog'
|
import * as dialog from '@/core/dialog'
|
||||||
import * as externalInput from '@/core/external-input'
|
import * as externalInput from '@/core/external-input'
|
||||||
@ -17,7 +18,7 @@ import * as spinQuery from '@/core/spin-query'
|
|||||||
import * as style from '@/core/style'
|
import * as style from '@/core/style'
|
||||||
import * as textColor from '@/core/text-color'
|
import * as textColor from '@/core/text-color'
|
||||||
import * as settings from '@/core/settings'
|
import * as settings from '@/core/settings'
|
||||||
import * as shadowDom from '@/core/shadow-dom'
|
import * as shadowRoot from '@/core/shadow-root'
|
||||||
import * as userInfo from '@/core/user-info'
|
import * as userInfo from '@/core/user-info'
|
||||||
import * as version from '@/core/version'
|
import * as version from '@/core/version'
|
||||||
import * as commonUtils from '@/core/utils'
|
import * as commonUtils from '@/core/utils'
|
||||||
@ -41,6 +42,7 @@ import { pluginApis } from '@/plugins/api'
|
|||||||
export const coreApis = {
|
export const coreApis = {
|
||||||
ajax,
|
ajax,
|
||||||
cdnTypes,
|
cdnTypes,
|
||||||
|
containerQuery,
|
||||||
download,
|
download,
|
||||||
dialog,
|
dialog,
|
||||||
externalInput,
|
externalInput,
|
||||||
@ -60,7 +62,7 @@ export const coreApis = {
|
|||||||
userInfo,
|
userInfo,
|
||||||
version,
|
version,
|
||||||
settings,
|
settings,
|
||||||
shadowDom,
|
shadowRoot,
|
||||||
toast,
|
toast,
|
||||||
themeColor,
|
themeColor,
|
||||||
utils: {
|
utils: {
|
||||||
@ -83,6 +85,7 @@ export type CoreApis = typeof coreApis
|
|||||||
export const externalApis = {
|
export const externalApis = {
|
||||||
ajax,
|
ajax,
|
||||||
...cdnTypes,
|
...cdnTypes,
|
||||||
|
...containerQuery,
|
||||||
...download,
|
...download,
|
||||||
...dialog,
|
...dialog,
|
||||||
...externalInput,
|
...externalInput,
|
||||||
@ -101,7 +104,7 @@ export const externalApis = {
|
|||||||
...textColor,
|
...textColor,
|
||||||
...userInfo,
|
...userInfo,
|
||||||
...version,
|
...version,
|
||||||
...shadowDom,
|
...shadowRoot,
|
||||||
settingsApis: settings,
|
settingsApis: settings,
|
||||||
get settings() {
|
get settings() {
|
||||||
return settings.settings
|
return settings.settings
|
||||||
|
|||||||
@ -5,26 +5,54 @@ import type StreamSaverType from 'streamsaver'
|
|||||||
import { monkey } from './ajax'
|
import { monkey } from './ajax'
|
||||||
import { meta } from './meta'
|
import { meta } from './meta'
|
||||||
|
|
||||||
|
export type RuntimeLibraryDefinition = { url: string; sha256: string }
|
||||||
export interface RuntimeLibraryConfig<LibraryType> {
|
export interface RuntimeLibraryConfig<LibraryType> {
|
||||||
url: string
|
library: RuntimeLibraryDefinition
|
||||||
getModule: (window: Window) => LibraryType
|
getModule: (window: Window) => LibraryType
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RuntimeLibrary<LibraryType> implements PromiseLike<LibraryType> {
|
export class RuntimeLibrary<LibraryType> implements PromiseLike<LibraryType> {
|
||||||
private modulePromise: Promise<LibraryType>
|
private modulePromise: Promise<LibraryType>
|
||||||
|
|
||||||
constructor(public config: RuntimeLibraryConfig<LibraryType>) {}
|
constructor(public config: RuntimeLibraryConfig<LibraryType>) {}
|
||||||
|
|
||||||
|
static async sha256(content: string | BufferSource) {
|
||||||
|
const hashBuffer = await window.crypto.subtle.digest(
|
||||||
|
'SHA-256',
|
||||||
|
typeof content === 'string' ? new TextEncoder().encode(content) : content,
|
||||||
|
)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
return hashHex
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkIntegrity(content: string | BufferSource) {
|
||||||
|
const sha256 = await RuntimeLibrary.sha256(content)
|
||||||
|
if (sha256 !== this.config.library.sha256) {
|
||||||
|
throw new Error(
|
||||||
|
`[RuntimeLibrary] Check integrity failed from ${this.config.library.url}, expected = ${this.config.library.sha256}, actual = ${sha256}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`[Runtime Library] Checked integrity from ${this.config.library.url}, hash = ${sha256}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async then<Resolve = LibraryType, Reject = never>(
|
async then<Resolve = LibraryType, Reject = never>(
|
||||||
resolve?: (value: LibraryType) => Resolve | PromiseLike<Resolve>,
|
resolve?: (value: LibraryType) => Resolve | PromiseLike<Resolve>,
|
||||||
reject?: (reason: any) => Reject | PromiseLike<Reject>,
|
reject?: (reason: any) => Reject | PromiseLike<Reject>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { url, getModule } = this.config
|
const {
|
||||||
|
library: { url },
|
||||||
|
getModule,
|
||||||
|
} = this.config
|
||||||
if (!this.modulePromise) {
|
if (!this.modulePromise) {
|
||||||
this.modulePromise = (async () => {
|
this.modulePromise = (async () => {
|
||||||
console.log(`[Runtime Library] Start download from ${url}`)
|
console.log(`[Runtime Library] Start download from ${url}`)
|
||||||
const code: string = await monkey({ url })
|
const code: string = await monkey({ url })
|
||||||
console.log(`[Runtime Library] Downloaded from ${url} , length = ${code.length}`)
|
console.log(`[Runtime Library] Downloaded from ${url}, length = ${code.length}`)
|
||||||
|
await this.checkIntegrity(code)
|
||||||
;(function runEval() {
|
;(function runEval() {
|
||||||
return eval(code)
|
return eval(code)
|
||||||
// eslint-disable-next-line no-extra-bind
|
// eslint-disable-next-line no-extra-bind
|
||||||
@ -41,18 +69,18 @@ export class RuntimeLibrary<LibraryType> implements PromiseLike<LibraryType> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const protobufLibrary = new RuntimeLibrary<typeof protobufType>({
|
export const protobufLibrary = new RuntimeLibrary<typeof protobufType>({
|
||||||
url: meta.compilationInfo.altCdn.library.protobuf,
|
library: meta.compilationInfo.altCdn.library.protobuf,
|
||||||
getModule: window => window.protobuf,
|
getModule: window => window.protobuf,
|
||||||
})
|
})
|
||||||
export const JSZipLibrary = new RuntimeLibrary<typeof JSZipType>({
|
export const JSZipLibrary = new RuntimeLibrary<typeof JSZipType>({
|
||||||
url: meta.compilationInfo.altCdn.library.jszip,
|
library: meta.compilationInfo.altCdn.library.jszip,
|
||||||
getModule: window => window.JSZip,
|
getModule: window => window.JSZip,
|
||||||
})
|
})
|
||||||
export const SortableJSLibrary = new RuntimeLibrary<typeof SortableJSType>({
|
export const SortableJSLibrary = new RuntimeLibrary<typeof SortableJSType>({
|
||||||
url: meta.compilationInfo.altCdn.library.sortable,
|
library: meta.compilationInfo.altCdn.library.sortable,
|
||||||
getModule: window => window.Sortable,
|
getModule: window => window.Sortable,
|
||||||
})
|
})
|
||||||
export const StreamSaverLibrary = new RuntimeLibrary<typeof StreamSaverType>({
|
export const StreamSaverLibrary = new RuntimeLibrary<typeof StreamSaverType>({
|
||||||
url: meta.compilationInfo.altCdn.library.streamsaver,
|
library: meta.compilationInfo.altCdn.library.streamsaver,
|
||||||
getModule: window => window.streamSaver,
|
getModule: window => window.streamSaver,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,192 +0,0 @@
|
|||||||
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<ShadowRoot>) => callbacks?.added(e.detail)
|
|
||||||
const removedListener = (e: CustomEvent<ShadowRoot>) => 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
150
src/core/shadow-root/dom-entry.ts
Normal file
150
src/core/shadow-root/dom-entry.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import { childListSubtree } from '../observer'
|
||||||
|
import { deleteValue } from '../utils'
|
||||||
|
import type { ShadowDomObserver } from './dom-observer'
|
||||||
|
import { ShadowRootObserver } from './root-observer'
|
||||||
|
import { ShadowRootEvents } from './types'
|
||||||
|
|
||||||
|
export const ShadowDomEntrySymbol = Symbol.for('ShadowDomEntry')
|
||||||
|
export type ShadowDomParent = ShadowDomEntry | ShadowDomObserver
|
||||||
|
export type ShadowDomCallback = (shadowDom: ShadowDomEntry) => void
|
||||||
|
export class ShadowDomEntry extends ShadowRootObserver {
|
||||||
|
readonly shadowRoot: ShadowRoot
|
||||||
|
readonly parent: ShadowDomParent | null = null
|
||||||
|
readonly children: ShadowDomEntry[] = []
|
||||||
|
protected observer: MutationObserver
|
||||||
|
protected callbacksMap = new Map()
|
||||||
|
|
||||||
|
constructor(shadowRoot: ShadowRoot, parent: ShadowDomParent) {
|
||||||
|
super()
|
||||||
|
this.shadowRoot = shadowRoot
|
||||||
|
this.parent = parent
|
||||||
|
this.element[ShadowDomEntrySymbol] = this
|
||||||
|
ShadowRootObserver.queryAllShadowRoots(shadowRoot)
|
||||||
|
.filter(it => it !== shadowRoot)
|
||||||
|
.map(it => this.addChild(it))
|
||||||
|
this.observe()
|
||||||
|
}
|
||||||
|
|
||||||
|
get element() {
|
||||||
|
return this.shadowRoot.host
|
||||||
|
}
|
||||||
|
|
||||||
|
get elementName() {
|
||||||
|
return this.element.tagName.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispatchEvent(event: Event): boolean {
|
||||||
|
return super.dispatchEvent(event) && this.parent.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected mutationHandler(records: MutationRecord[]) {
|
||||||
|
if (records.length > 0) {
|
||||||
|
this.dispatchEvent(new CustomEvent(ShadowRootEvents.Updated, { detail: records }))
|
||||||
|
}
|
||||||
|
records.forEach(record => {
|
||||||
|
record.removedNodes.forEach(node => {
|
||||||
|
if (node instanceof Element) {
|
||||||
|
ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => {
|
||||||
|
const child = this.children.find(it => it.shadowRoot === shadowRoot)
|
||||||
|
if (child === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.removeChild(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
record.addedNodes.forEach(node => {
|
||||||
|
if (node instanceof Element) {
|
||||||
|
ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => {
|
||||||
|
this.addChild(shadowRoot)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addChild(childShadowRoot: ShadowRoot) {
|
||||||
|
const match = this.children.find(child => child.shadowRoot === childShadowRoot)
|
||||||
|
if (match) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
const child = new ShadowDomEntry(childShadowRoot, this)
|
||||||
|
this.children.push(child)
|
||||||
|
this.dispatchEvent(new CustomEvent(ShadowRootEvents.Added, { detail: child }))
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
removeChild(child: ShadowDomEntry) {
|
||||||
|
child.disconnect()
|
||||||
|
deleteValue(this.children, it => it === child)
|
||||||
|
this.dispatchEvent(new CustomEvent(ShadowRootEvents.Removed, { detail: child }))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected queryThroughChildren<T>(predicate: (current: ShadowDomEntry) => T | null): {
|
||||||
|
entry: ShadowDomEntry | null
|
||||||
|
result: T | null
|
||||||
|
} {
|
||||||
|
const selfResult = predicate(this)
|
||||||
|
if (selfResult !== null) {
|
||||||
|
return {
|
||||||
|
entry: this,
|
||||||
|
result: selfResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const child of this.children) {
|
||||||
|
const childResult = child.queryThroughChildren(predicate)
|
||||||
|
if (childResult.result !== null) {
|
||||||
|
return childResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
entry: null,
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelectorAsEntry(selectors: string): ShadowDomEntry | null {
|
||||||
|
const { entry } = this.queryThroughChildren(current => {
|
||||||
|
if (current === this) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (current.element.matches(selectors)) {
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelectorAllAsEntry(selectors: string): ShadowDomEntry[] {
|
||||||
|
const currentMatch = this.children.filter(child => child.element.matches(selectors))
|
||||||
|
const childrenMatch = this.children.flatMap(child => child.querySelectorAllAsEntry(selectors))
|
||||||
|
return [...currentMatch, ...childrenMatch]
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelector(selectors: string): Element | null {
|
||||||
|
const { result } = this.queryThroughChildren(current =>
|
||||||
|
current.shadowRoot.querySelector(selectors),
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
querySelectorAll(selectors: string): Element[] {
|
||||||
|
return [
|
||||||
|
...this.shadowRoot.querySelectorAll(selectors),
|
||||||
|
...this.children.flatMap(child => child.querySelectorAll(selectors)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
observe() {
|
||||||
|
const [observer] = childListSubtree(this.shadowRoot, records => {
|
||||||
|
this.mutationHandler(records)
|
||||||
|
})
|
||||||
|
this.observer = observer
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.children.forEach(child => this.removeChild(child))
|
||||||
|
this.observer?.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/core/shadow-root/dom-observer.ts
Normal file
126
src/core/shadow-root/dom-observer.ts
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { childListSubtree } from '../observer'
|
||||||
|
import { deleteValue } from '../utils'
|
||||||
|
import { ShadowDomCallback, ShadowDomEntry } from './dom-entry'
|
||||||
|
import { ShadowRootObserver } from './root-observer'
|
||||||
|
import { ShadowRootEvents } from './types'
|
||||||
|
|
||||||
|
export class ShadowDomObserver extends ShadowRootObserver {
|
||||||
|
static enforceOpenRoot() {
|
||||||
|
const originalAttachShadow = Element.prototype.attachShadow
|
||||||
|
Element.prototype.attachShadow = function attachShadow(options: ShadowRootInit) {
|
||||||
|
return originalAttachShadow.call(this, {
|
||||||
|
...options,
|
||||||
|
mode: 'open',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private observing = false
|
||||||
|
private rootObserver: MutationObserver | undefined = undefined
|
||||||
|
|
||||||
|
entries: ShadowDomEntry[] = []
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected mutationHandler(records: MutationRecord[]) {
|
||||||
|
if (records.length > 0) {
|
||||||
|
this.dispatchEvent(new CustomEvent(ShadowRootEvents.Updated, { detail: records }))
|
||||||
|
}
|
||||||
|
records.forEach(record => {
|
||||||
|
record.removedNodes.forEach(node => {
|
||||||
|
if (node instanceof Element) {
|
||||||
|
ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => {
|
||||||
|
this.removeEntryByShadowRoot(shadowRoot)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
record.addedNodes.forEach(node => {
|
||||||
|
if (node instanceof Element) {
|
||||||
|
ShadowRootObserver.queryAllShadowRoots(node).forEach(shadowRoot => {
|
||||||
|
this.addEntry(shadowRoot)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addEntry(shadowRoot: ShadowRoot) {
|
||||||
|
const match = this.entries.find(e => e.shadowRoot === shadowRoot)
|
||||||
|
if (match !== undefined) {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadowDomEntry = new ShadowDomEntry(shadowRoot, this)
|
||||||
|
shadowDomEntry.observe()
|
||||||
|
this.entries.push(shadowDomEntry)
|
||||||
|
this.dispatchEvent(new CustomEvent(ShadowRootEvents.Added, { detail: shadowDomEntry }))
|
||||||
|
|
||||||
|
return shadowDomEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeEntry(entry: ShadowDomEntry) {
|
||||||
|
const match = this.entries.find(it => it === entry)
|
||||||
|
if (match === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
match.children.forEach(child => this.removeEntry(child))
|
||||||
|
match.disconnect()
|
||||||
|
deleteValue(this.entries, it => it === match)
|
||||||
|
this.dispatchEvent(new CustomEvent(ShadowRootEvents.Removed, { detail: match }))
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeEntryByShadowRoot(shadowRoot: ShadowRoot) {
|
||||||
|
const match = this.entries.find(it => it.shadowRoot === shadowRoot)
|
||||||
|
if (match === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.removeEntry(match)
|
||||||
|
}
|
||||||
|
|
||||||
|
forEachShadowDom(callback: ShadowDomCallback) {
|
||||||
|
const callCurrentAndNextLevel = (currentEntry: ShadowDomEntry) => {
|
||||||
|
callback(currentEntry)
|
||||||
|
currentEntry.children.forEach(child => {
|
||||||
|
callCurrentAndNextLevel(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entries.forEach(entry => {
|
||||||
|
callCurrentAndNextLevel(entry)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watchShadowDom(callbacks: { added?: ShadowDomCallback; removed?: ShadowDomCallback }) {
|
||||||
|
this.forEachShadowDom(it => callbacks.added?.(it))
|
||||||
|
const addedListener = (e: CustomEvent<ShadowDomEntry>) => callbacks?.added?.(e.detail)
|
||||||
|
const removedListener = (e: CustomEvent<ShadowDomEntry>) => 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 = ShadowRootObserver.queryAllShadowRoots()
|
||||||
|
existingRoots.forEach(root => this.addEntry(root))
|
||||||
|
;[this.rootObserver] = childListSubtree(document.body, records => this.mutationHandler(records))
|
||||||
|
this.observing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.rootObserver.disconnect()
|
||||||
|
this.entries.forEach(entry => entry.disconnect())
|
||||||
|
this.entries = []
|
||||||
|
this.observing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shadowDomObserver = new ShadowDomObserver()
|
||||||
3
src/core/shadow-root/index.ts
Normal file
3
src/core/shadow-root/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './dom-observer'
|
||||||
|
export * from './styles'
|
||||||
|
export * from './types'
|
||||||
36
src/core/shadow-root/root-observer.ts
Normal file
36
src/core/shadow-root/root-observer.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { ShadowRootEvents } from './types'
|
||||||
|
|
||||||
|
export abstract class ShadowRootObserver extends EventTarget {
|
||||||
|
static 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, ...ShadowRootObserver.queryAllShadowRoots(e.shadowRoot)]
|
||||||
|
}
|
||||||
|
return [e.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract observe(): void
|
||||||
|
abstract disconnect(): void
|
||||||
|
}
|
||||||
84
src/core/shadow-root/styles.ts
Normal file
84
src/core/shadow-root/styles.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { addComponentListener, removeComponentListener } from '../settings'
|
||||||
|
import { deleteValue, getRandomId } from '../utils'
|
||||||
|
import { ShadowDomEntry } from './dom-entry'
|
||||||
|
import { ShadowDomObserver, shadowDomObserver } from './dom-observer'
|
||||||
|
|
||||||
|
export interface ShadowRootStyleEntry {
|
||||||
|
id: string
|
||||||
|
styleSheet: CSSStyleSheet
|
||||||
|
adoptedShadowDoms: ShadowDomEntry[]
|
||||||
|
remove: () => void
|
||||||
|
}
|
||||||
|
export interface ShadowRootStyleDefinition {
|
||||||
|
id?: string
|
||||||
|
style: string
|
||||||
|
}
|
||||||
|
export class ShadowRootStyles {
|
||||||
|
observer: ShadowDomObserver = shadowDomObserver
|
||||||
|
protected stylesMap = new Map<string, ShadowRootStyleEntry>()
|
||||||
|
|
||||||
|
protected addEntry(id: string, entry: ShadowRootStyleEntry) {
|
||||||
|
this.stylesMap.set(id, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected removeEntry(id: string) {
|
||||||
|
if (this.stylesMap.has(id)) {
|
||||||
|
this.stylesMap.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addStyle(definition: ShadowRootStyleDefinition) {
|
||||||
|
const { id, style } = definition
|
||||||
|
this.observer.observe()
|
||||||
|
const entryId = `shadow-dom-style-${id !== undefined ? lodash.kebabCase(id) : getRandomId()}`
|
||||||
|
const styleSheet = new CSSStyleSheet()
|
||||||
|
await styleSheet.replace(style)
|
||||||
|
|
||||||
|
const adoptedShadowDoms: ShadowDomEntry[] = []
|
||||||
|
const destroy = this.observer.watchShadowDom({
|
||||||
|
added: async shadowDom => {
|
||||||
|
shadowDom.shadowRoot.adoptedStyleSheets.push(styleSheet)
|
||||||
|
adoptedShadowDoms.push(shadowDom)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const entry: ShadowRootStyleEntry = {
|
||||||
|
id: entryId,
|
||||||
|
styleSheet,
|
||||||
|
adoptedShadowDoms,
|
||||||
|
remove: () => {
|
||||||
|
destroy()
|
||||||
|
adoptedShadowDoms.forEach(it =>
|
||||||
|
deleteValue(it.shadowRoot.adoptedStyleSheets, sheet => sheet === styleSheet),
|
||||||
|
)
|
||||||
|
this.removeEntry(id)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
this.addEntry(id, entry)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
removeStyle(id: string) {
|
||||||
|
if (this.stylesMap.has(id)) {
|
||||||
|
const style = this.stylesMap.get(id)
|
||||||
|
style.remove()
|
||||||
|
this.removeEntry(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleWithComponent(path: string, definition: ShadowRootStyleDefinition) {
|
||||||
|
let entry: ShadowRootStyleEntry | undefined
|
||||||
|
const handler = async (rawValue: unknown) => {
|
||||||
|
const value = Boolean(rawValue)
|
||||||
|
if (value) {
|
||||||
|
entry = await this.addStyle(definition)
|
||||||
|
} else if (entry !== undefined) {
|
||||||
|
this.removeStyle(entry.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addComponentListener(path, handler, true)
|
||||||
|
return () => removeComponentListener(path, handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shadowRootStyles = new ShadowRootStyles()
|
||||||
5
src/core/shadow-root/types.ts
Normal file
5
src/core/shadow-root/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum ShadowRootEvents {
|
||||||
|
Added = 'shadowRootAdded',
|
||||||
|
Removed = 'shadowRootRemoved',
|
||||||
|
Updated = 'shadowRootUpdated',
|
||||||
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
import { ComponentMetadata } from '@/components/types'
|
import {
|
||||||
|
ComponentMetadata,
|
||||||
|
DomInstantStyleDefinition,
|
||||||
|
ShadowDomInstantStyleDefinition,
|
||||||
|
} from '@/components/types'
|
||||||
import { contentLoaded } from './life-cycle'
|
import { contentLoaded } from './life-cycle'
|
||||||
|
import { shadowRootStyles } from './shadow-root'
|
||||||
|
|
||||||
/** 为`<style>`获取默认的ID (camelCase转为kebab-case) */
|
/** 为`<style>`获取默认的ID (camelCase转为kebab-case) */
|
||||||
export const getDefaultStyleID = (name: string) =>
|
export const getDefaultStyleID = (name: string) =>
|
||||||
@ -53,21 +58,48 @@ export const loadInstantStyle = async (
|
|||||||
} = { head: document.head, body: document.body },
|
} = { head: document.head, body: document.body },
|
||||||
) => {
|
) => {
|
||||||
component.instantStyles?.forEach(async it => {
|
component.instantStyles?.forEach(async it => {
|
||||||
|
const styleContent = await (async () => {
|
||||||
|
if (typeof it.style === 'string') {
|
||||||
|
return it.style
|
||||||
|
}
|
||||||
|
const module = await it.style()
|
||||||
|
return module.default
|
||||||
|
})()
|
||||||
|
|
||||||
|
if ((it as ShadowDomInstantStyleDefinition).shadowDom) {
|
||||||
|
shadowRootStyles.addStyle({
|
||||||
|
id: it.name,
|
||||||
|
style: styleContent,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.id = getDefaultStyleID(it.name)
|
style.id = getDefaultStyleID(it.name)
|
||||||
if (typeof it.style === 'string') {
|
style.textContent = styleContent
|
||||||
style.textContent = it.style
|
if ((it as DomInstantStyleDefinition).important) {
|
||||||
} else {
|
|
||||||
const { default: styleContent } = await it.style()
|
|
||||||
style.textContent = styleContent
|
|
||||||
}
|
|
||||||
if (it.important) {
|
|
||||||
fragments.body.appendChild(style)
|
fragments.body.appendChild(style)
|
||||||
} else {
|
} else {
|
||||||
fragments.head.appendChild(style)
|
fragments.head.appendChild(style)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除首屏样式
|
||||||
|
*/
|
||||||
|
export const removeInstantStyle = (
|
||||||
|
...instantStyles: (DomInstantStyleDefinition | ShadowDomInstantStyleDefinition)[]
|
||||||
|
) => {
|
||||||
|
instantStyles.forEach(style => {
|
||||||
|
if ((style as ShadowDomInstantStyleDefinition).shadowDom) {
|
||||||
|
shadowRootStyles.removeStyle(style.name)
|
||||||
|
} else {
|
||||||
|
removeStyle(style.name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注入组件中定义的首屏样式, MDI图标样式, 以及主题颜色样式
|
* 注入组件中定义的首屏样式, MDI图标样式, 以及主题颜色样式
|
||||||
*
|
*
|
||||||
@ -94,7 +126,7 @@ export const preloadStyles = lodash.once(async () => {
|
|||||||
if (enabled) {
|
if (enabled) {
|
||||||
return loadInstantStyle(component)
|
return loadInstantStyle(component)
|
||||||
}
|
}
|
||||||
return component.instantStyles?.forEach(style => removeStyle(style.name))
|
return component.instantStyles?.forEach(style => removeInstantStyle(style))
|
||||||
}
|
}
|
||||||
addComponentListener(component.name, listener)
|
addComponentListener(component.name, listener)
|
||||||
if (isUserComponent(component)) {
|
if (isUserComponent(component)) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { FeatureBase } from '@/components/types'
|
import { FeatureBase } from '@/components/types'
|
||||||
import { loadFeatureCode } from './external-input'
|
import { loadFeatureCode } from './external-input'
|
||||||
import { meta } from './meta'
|
import { meta } from './meta'
|
||||||
|
import { useScopedConsole } from './utils/log'
|
||||||
|
|
||||||
export enum CompareResult {
|
export enum CompareResult {
|
||||||
Less = -1,
|
Less = -1,
|
||||||
@ -50,6 +51,7 @@ export class Version {
|
|||||||
*/
|
*/
|
||||||
export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
|
export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
|
||||||
try {
|
try {
|
||||||
|
const console = useScopedConsole('isFeatureAcceptable')
|
||||||
if (typeof feature === 'string') {
|
if (typeof feature === 'string') {
|
||||||
feature = loadFeatureCode(feature) as FeatureBase
|
feature = loadFeatureCode(feature) as FeatureBase
|
||||||
}
|
}
|
||||||
@ -59,6 +61,7 @@ export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
|
|||||||
}
|
}
|
||||||
const { version: currentVersionText } = meta.compilationInfo
|
const { version: currentVersionText } = meta.compilationInfo
|
||||||
const { coreVersion: requiredVersionText } = feature
|
const { coreVersion: requiredVersionText } = feature
|
||||||
|
console.log('currentVersion =', currentVersionText, ', requiredVersion =', requiredVersionText)
|
||||||
// 没有版本信息, 按旧版行为默认通过检测
|
// 没有版本信息, 按旧版行为默认通过检测
|
||||||
if (!requiredVersionText || !currentVersionText) {
|
if (!requiredVersionText || !currentVersionText) {
|
||||||
return true
|
return true
|
||||||
@ -67,7 +70,7 @@ export const isFeatureAcceptable = async (feature: FeatureBase | string) => {
|
|||||||
const requiredVersion = new Version(requiredVersionText)
|
const requiredVersion = new Version(requiredVersionText)
|
||||||
return currentVersion.equals(requiredVersion) || currentVersion.greaterThan(requiredVersion)
|
return currentVersion.equals(requiredVersion) || currentVersion.greaterThan(requiredVersion)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[isFeatureAcceptable] check failed, feature =', feature)
|
console.warn('check failed, feature =', feature)
|
||||||
// 版本号异常, 跳过检测
|
// 版本号异常, 跳过检测
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,12 +10,26 @@ export const github: CdnConfig = {
|
|||||||
stableClient: `https://${host}/${owner}/Bilibili-Evolved/master/dist/bilibili-evolved.user.js`,
|
stableClient: `https://${host}/${owner}/Bilibili-Evolved/master/dist/bilibili-evolved.user.js`,
|
||||||
previewClient: `https://${host}/${owner}/Bilibili-Evolved/preview/dist/bilibili-evolved.preview.user.js`,
|
previewClient: `https://${host}/${owner}/Bilibili-Evolved/preview/dist/bilibili-evolved.preview.user.js`,
|
||||||
library: {
|
library: {
|
||||||
lodash: `https://${host}/lodash/lodash/4.17.21/dist/lodash.min.js`,
|
lodash: {
|
||||||
protobuf: `https://${host}/protobufjs/protobuf.js/v6.10.1/dist/light/protobuf.min.js`,
|
url: `https://${host}/lodash/lodash/4.17.21/dist/lodash.min.js`,
|
||||||
jszip: `https://${host}/Stuk/jszip/v3.7.1/dist/jszip.min.js`,
|
sha256: 'a9705dfc47c0763380d851ab1801be6f76019f6b67e40e9b873f8b4a0603f7a9',
|
||||||
sortable: `https://${host}/SortableJS/Sortable/1.14.0/Sortable.min.js`,
|
},
|
||||||
mdi: `https://${owner}.github.io/Bilibili-Evolved/static/mdi/mdi.css`,
|
protobuf: {
|
||||||
streamsaver: `https://${host}/jimmywarting/StreamSaver.js/2.0.6/StreamSaver.js`,
|
url: `https://${host}/protobufjs/protobuf.js/v6.10.1/dist/light/protobuf.min.js`,
|
||||||
|
sha256: '8978daf871b02d683ecaee371861702a6f31d0a4c52925b7db2bb1655a8bc7d1',
|
||||||
|
},
|
||||||
|
jszip: {
|
||||||
|
url: `https://${host}/Stuk/jszip/v3.7.1/dist/jszip.min.js`,
|
||||||
|
sha256: 'c9e4a52bac18aee4f3f90d05fbca603f5b0f5bf1ce8c45e60bb4ed3a2cb2ed86',
|
||||||
|
},
|
||||||
|
sortable: {
|
||||||
|
url: `https://${host}/SortableJS/Sortable/1.14.0/Sortable.min.js`,
|
||||||
|
sha256: '0ea5a6fbfbf5434b606878533cb7a66bcf700f0f08afe908335d0978fb63ad94',
|
||||||
|
},
|
||||||
|
streamsaver: {
|
||||||
|
url: `https://${host}/jimmywarting/StreamSaver.js/2.0.6/StreamSaver.js`,
|
||||||
|
sha256: 'a110f78e0b092481dc372901c4d57ae50681d773bc9d55e62356f9a22f17e24b',
|
||||||
|
},
|
||||||
// https://github.com/the1812/Bilibili-Evolved/pull/4521#discussion_r1402084486
|
// https://github.com/the1812/Bilibili-Evolved/pull/4521#discussion_r1402084486
|
||||||
ffmpeg: jsDelivr.library.ffmpeg,
|
ffmpeg: jsDelivr.library.ffmpeg,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,16 +9,39 @@ export const jsDelivr: CdnConfig = {
|
|||||||
stableClient: `https://${host}/gh/${owner}/Bilibili-Evolved@master/dist/bilibili-evolved.user.js`,
|
stableClient: `https://${host}/gh/${owner}/Bilibili-Evolved@master/dist/bilibili-evolved.user.js`,
|
||||||
previewClient: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/dist/bilibili-evolved.preview.user.js`,
|
previewClient: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/dist/bilibili-evolved.preview.user.js`,
|
||||||
library: {
|
library: {
|
||||||
lodash: `https://${host}/npm/lodash@4.17.21/lodash.min.js`,
|
lodash: {
|
||||||
protobuf: `https://${host}/npm/protobufjs@6.10.1/dist/light/protobuf.min.js`,
|
url: `https://${host}/npm/lodash@4.17.21/lodash.min.js`,
|
||||||
jszip: `https://${host}/npm/jszip@3.7.1/dist/jszip.min.js`,
|
sha256: 'a9705dfc47c0763380d851ab1801be6f76019f6b67e40e9b873f8b4a0603f7a9',
|
||||||
sortable: `https://${host}/npm/sortablejs@1.14.0/Sortable.min.js`,
|
},
|
||||||
mdi: `https://${host}/gh/${owner}/Bilibili-Evolved@master/docs/static/mdi/mdi.css`,
|
protobuf: {
|
||||||
streamsaver: `https://${host}/npm/streamsaver@2.0.6/StreamSaver.min.js`,
|
url: `https://${host}/npm/protobufjs@6.10.1/dist/light/protobuf.min.js`,
|
||||||
|
sha256: '8978daf871b02d683ecaee371861702a6f31d0a4c52925b7db2bb1655a8bc7d1',
|
||||||
|
},
|
||||||
|
jszip: {
|
||||||
|
url: `https://${host}/npm/jszip@3.7.1/dist/jszip.min.js`,
|
||||||
|
sha256: 'c9e4a52bac18aee4f3f90d05fbca603f5b0f5bf1ce8c45e60bb4ed3a2cb2ed86',
|
||||||
|
},
|
||||||
|
sortable: {
|
||||||
|
url: `https://${host}/npm/sortablejs@1.14.0/Sortable.min.js`,
|
||||||
|
sha256: '0ea5a6fbfbf5434b606878533cb7a66bcf700f0f08afe908335d0978fb63ad94',
|
||||||
|
},
|
||||||
|
streamsaver: {
|
||||||
|
url: `https://${host}/npm/streamsaver@2.0.6/StreamSaver.min.js`,
|
||||||
|
sha256: '64f465e51e5992be894c5d42330b781544eda5462069fe6be4c7421f02d28c92',
|
||||||
|
},
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
worker: `https://${host}/npm/@ffmpeg/ffmpeg@0.12.4/dist/umd/814.ffmpeg.js`,
|
worker: {
|
||||||
core: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.js`,
|
url: `https://${host}/npm/@ffmpeg/ffmpeg@0.12.4/dist/umd/814.ffmpeg.js`,
|
||||||
wasm: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.wasm`,
|
sha256: 'baf19437171b1bccae4416e4da69fb40455b8e67142f79c8ec9da36b1de7fd8a',
|
||||||
|
},
|
||||||
|
core: {
|
||||||
|
url: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.js`,
|
||||||
|
sha256: '6af6b8cd8c878dec6f61f3cd6be16e88f9391dd265e51f20afea5c0f718bfba0',
|
||||||
|
},
|
||||||
|
wasm: {
|
||||||
|
url: `https://${host}/npm/@ffmpeg/core@0.12.4/dist/umd/ffmpeg-core.wasm`,
|
||||||
|
sha256: '925bd7ef35d4e0f715254cd650f4cfc68c4ec6ecebf293face72b92da904ddda',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
smallLogo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo-small.png`,
|
smallLogo: `https://${host}/gh/${owner}/Bilibili-Evolved@preview/images/logo-small.png`,
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export type ExternalLibrary = { url: string; sha256: string }
|
||||||
export interface CdnConfig {
|
export interface CdnConfig {
|
||||||
name: string
|
name: string
|
||||||
owner: string
|
owner: string
|
||||||
@ -5,16 +6,15 @@ export interface CdnConfig {
|
|||||||
stableClient: string
|
stableClient: string
|
||||||
previewClient: string
|
previewClient: string
|
||||||
library: {
|
library: {
|
||||||
lodash: string
|
lodash: ExternalLibrary
|
||||||
protobuf: string
|
protobuf: ExternalLibrary
|
||||||
jszip: string
|
jszip: ExternalLibrary
|
||||||
sortable: string
|
sortable: ExternalLibrary
|
||||||
mdi: string
|
streamsaver: ExternalLibrary
|
||||||
streamsaver: string
|
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
worker: string
|
worker: ExternalLibrary
|
||||||
core: string
|
core: ExternalLibrary
|
||||||
wasm: string
|
wasm: ExternalLibrary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
smallLogo: string
|
smallLogo: string
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user