-
+
@@ -40,7 +47,15 @@ export default Vue.extend({
this.$emit('discard')
},
save() {
- this.$refs.link.click()
+ const link = this.$refs.link as HTMLAnchorElement
+ link.addEventListener(
+ 'click',
+ e => {
+ e.stopPropagation()
+ },
+ { capture: true, once: true },
+ )
+ link.click()
this.discard()
},
},
diff --git a/registry/lib/plugins/video/download/wasm-output/database.ts b/registry/lib/plugins/video/download/wasm-output/database.ts
index 091fbee6e5..0be2ae1911 100644
--- a/registry/lib/plugins/video/download/wasm-output/database.ts
+++ b/registry/lib/plugins/video/download/wasm-output/database.ts
@@ -6,7 +6,7 @@ export const storeNames = {
} as const
async function database() {
- return new Promise((reslove: (db: IDBDatabase) => void, reject) => {
+ return new Promise((resolve: (db: IDBDatabase) => void, reject) => {
const req = unsafeWindow.indexedDB.open(DB_NAME, DB_VERSION)
req.onerror = reject
req.onupgradeneeded = () => {
@@ -18,34 +18,34 @@ async function database() {
db.createObjectStore(v)
})
}
- req.onsuccess = () => reslove(req.result)
+ req.onsuccess = () => resolve(req.result)
})
}
async function objectStore(name: string, mode?: IDBTransactionMode) {
return database().then(
db =>
- new Promise((reslove: (db: IDBObjectStore) => void, reject) => {
+ new Promise((resolve: (db: IDBObjectStore) => void, reject) => {
const tr = db.transaction(name, mode)
- reslove(tr.objectStore(name))
+ resolve(tr.objectStore(name))
tr.onerror = reject
}),
)
}
async function get(store: IDBObjectStore, key: IDBValidKey | IDBKeyRange) {
- return new Promise((reslove: (db: any) => void, reject) => {
+ return new Promise((resolve: (db: any) => void, reject) => {
const res = store.get(key)
res.onerror = reject
- res.onsuccess = () => reslove(res.result)
+ res.onsuccess = () => resolve(res.result)
})
}
async function put(store: IDBObjectStore, value: any, key?: IDBValidKey) {
- return new Promise((reslove: (db: IDBValidKey) => void, reject) => {
+ return new Promise((resolve: (db: IDBValidKey) => void, reject) => {
const res = store.put(value, key)
res.onerror = reject
- res.onsuccess = () => reslove(res.result)
+ res.onsuccess = () => resolve(res.result)
})
}
diff --git a/registry/lib/plugins/video/download/wasm-output/handler.ts b/registry/lib/plugins/video/download/wasm-output/handler.ts
index d4353e233f..1c48e12fd3 100644
--- a/registry/lib/plugins/video/download/wasm-output/handler.ts
+++ b/registry/lib/plugins/video/download/wasm-output/handler.ts
@@ -1,12 +1,17 @@
import { DownloadPackage } from '@/core/download'
import { meta } from '@/core/meta'
+import { getComponentSettings } from '@/core/settings'
import { Toast } from '@/core/toast'
+import { title as pluginTitle } from '.'
+import type { Options } from '../../../../components/video/download'
+import { DownloadVideoAction } from '../../../../components/video/download/types'
import { FFmpeg } from './ffmpeg'
import { getCacheOrGet, httpGet, toastProgress, toBlobUrl } from './utils'
const ffmpeg = new FFmpeg()
-async function load(toast: Toast) {
+async function loadFFmpeg() {
+ const toast = Toast.info('正在加载 FFmpeg', `${pluginTitle} - 初始化`)
await ffmpeg.load({
workerLoadURL: toBlobUrl(
await getCacheOrGet(
@@ -33,24 +38,26 @@ async function load(toast: Toast) {
'application/wasm',
),
})
+ toast.message = '完成!'
+ toast.close()
}
-export async function run(
+
+async function single(
name: string,
- toast: Toast,
videoUrl: string,
audioUrl: string,
isFlac: boolean,
+ pageIndex = 1,
+ totalPages = 1,
) {
- if (!ffmpeg.loaded) {
- await load(toast)
- }
+ const toast = Toast.info('', `${pluginTitle} - ${pageIndex} / ${totalPages}`)
ffmpeg.writeFile('video', await httpGet(videoUrl, toastProgress(toast, '正在下载视频流')))
ffmpeg.writeFile('audio', await httpGet(audioUrl, toastProgress(toast, '正在下载音频流')))
- toast.message = '混流中……'
+ toast.message = '混流中……'
const outputExt = isFlac ? 'mkv' : 'mp4'
- name = isFlac ? name.replace(/.[^/.]+$/, `.${outputExt}`) : name
+ name = name.replace(/.[^/.]+$/, `.${outputExt}`)
await ffmpeg.exec([
'-i',
'video',
@@ -59,7 +66,7 @@ export async function run(
'-c:v',
'copy',
'-c:a',
- isFlac ? 'flac' : 'copy',
+ 'copy',
'-f',
isFlac ? 'matroska' : 'mp4',
`output.${outputExt}`,
@@ -71,7 +78,40 @@ export async function run(
})
toast.message = '完成!'
- toast.duration = 1500
+ toast.duration = 1000
await DownloadPackage.single(name, outputBlob)
}
+
+export async function run(action: DownloadVideoAction) {
+ if (!ffmpeg.loaded) {
+ await loadFFmpeg()
+ }
+
+ const { dashAudioExtension, dashFlacAudioExtension, dashVideoExtension } =
+ getComponentSettings
('downloadVideo').options
+
+ const pages = action.infos
+ for (let i = 0; i < pages.length; i++) {
+ const page = pages[i]
+ const [video, audio] = page.titledFragments
+ if (
+ !(
+ page.fragments.length === 2 &&
+ video.extension === dashVideoExtension &&
+ (audio.extension === dashAudioExtension || audio.extension === dashFlacAudioExtension)
+ )
+ ) {
+ throw new Error('仅支持 DASH 格式视频和音频')
+ }
+
+ await single(
+ video.title,
+ video.url,
+ audio.url,
+ audio.extension === dashFlacAudioExtension,
+ i + 1,
+ pages.length,
+ )
+ }
+}
diff --git a/registry/lib/plugins/video/download/wasm-output/index.ts b/registry/lib/plugins/video/download/wasm-output/index.ts
index 12ad5fe00c..3cde08f64c 100644
--- a/registry/lib/plugins/video/download/wasm-output/index.ts
+++ b/registry/lib/plugins/video/download/wasm-output/index.ts
@@ -1,12 +1,10 @@
-import { Toast, ToastType } from '@/core/toast'
+import { Toast } from '@/core/toast'
import { PluginMetadata } from '@/plugins/plugin'
import { DownloadVideoOutput } from '../../../../components/video/download/types'
import { run } from './handler'
-import { getComponentSettings } from '@/core/settings'
-import type { Options } from '../../../../components/video/download'
-const title = 'WASM 混流输出'
-const desc = '使用 WASM 在浏览器中下载并合并音视频'
+export const title = 'WASM 混流输出'
+const desc = '使用 WASM 在浏览器中下载并合并音视频, 支持批量下载'
export const plugin: PluginMetadata = {
name: 'downloadVideo.outputs.wasm',
@@ -23,40 +21,10 @@ export const plugin: PluginMetadata = {
displayName: 'WASM',
description: `${desc},运行过程中请勿关闭页面,初次使用或清除缓存后需要加载约 30 MB 的 WASM 文件`,
runAction: async action => {
- const fragments = action.infos.flatMap(it => it.titledFragments)
- const { dashAudioExtension, dashFlacAudioExtension, dashVideoExtension } =
- getComponentSettings('downloadVideo').options
-
- if (
- fragments.length > 2 ||
- (fragments.length === 2 &&
- !(
- fragments[0].extension === dashVideoExtension &&
- (fragments[1].extension === dashAudioExtension ||
- fragments[1].extension === dashFlacAudioExtension)
- ))
- ) {
- Toast.error('仅支持单个视频下载', title).duration = 1500
- } else {
- const toast = Toast.info('正在加载', title)
- try {
- if (fragments.length === 2) {
- await run(
- fragments[0].title,
- toast,
- fragments[0].url,
- fragments[1].url,
- fragments[1].extension === dashFlacAudioExtension,
- ) // [dash]
- } else {
- toast.type = ToastType.Error
- toast.message = '仅支持 dash 格式视频下载'
- toast.duration = 1500
- }
- } catch (error) {
- toast.close()
- Toast.error(String(error), title)
- }
+ try {
+ await run(action)
+ } catch (error) {
+ Toast.error(String(error), title)
}
},
})
diff --git a/src/client/common.meta.json b/src/client/common.meta.json
index 79de1e9bc1..285af13e06 100644
--- a/src/client/common.meta.json
+++ b/src/client/common.meta.json
@@ -1,5 +1,5 @@
{
- "version": "2.8.10",
+ "version": "2.8.11",
"author": "Grant Howard, Coulomb-G",
"copyright": "[year], Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)",
"license": "MIT",
diff --git a/src/components/launch-bar/LaunchBar.vue b/src/components/launch-bar/LaunchBar.vue
index 8d0a7d145c..b1a9050df7 100644
--- a/src/components/launch-bar/LaunchBar.vue
+++ b/src/components/launch-bar/LaunchBar.vue
@@ -28,9 +28,9 @@
v-for="(a, index) of actions"
:key="a.key"
:action="a"
- @previous-item="previousItem($event, index)"
- @next-item="nextItem($event, index)"
- @delete-item="onDeleteItem($event, index)"
+ @previous-item="previousItem()"
+ @next-item="nextItem()"
+ @delete-item="onDeleteItem()"
@action="
index === actions.length - 1 && onClearHistory()
onAction(a)
@@ -49,12 +49,12 @@
class="suggest-item disabled"
>
@@ -66,7 +66,9 @@ import Fuse from 'fuse.js'
import { VIcon, VLoading, VEmpty } from '@/ui'
import { registerAndGetData } from '@/plugins/data'
import { select } from '@/core/spin-query'
+import { ascendingSort } from '@/core/utils/sort'
import { matchUrlPattern } from '@/core/utils'
+import { urlChange } from '@/core/observer'
import ActionItem from './ActionItem.vue'
import {
LaunchBarActionProviders,
@@ -75,7 +77,7 @@ import {
} from './launch-bar-action'
import { searchProvider, search } from './search-provider'
import { historyProvider } from './history-provider'
-import { ascendingSort } from '@/core/utils/sort'
+import { FocusTarget } from './focus-target'
const [actionProviders] = registerAndGetData(LaunchBarActionProviders, [
searchProvider,
@@ -144,10 +146,12 @@ export default Vue.extend({
ActionItem,
},
data() {
+ const focusTarget = new FocusTarget(0)
return {
recommended,
actions: [],
keyword: '',
+ focusTarget,
noActions: false,
}
},
@@ -160,30 +164,35 @@ export default Vue.extend({
keyword() {
this.getActions()
},
+ actions() {
+ this.focusTarget.reset(this.actions.length)
+ },
},
async mounted() {
- this.getActions()
- if (!matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
- return
+ await this.getActions()
+ if (matchUrlPattern(/^https?:\/\/search\.bilibili\.com/)) {
+ await this.setupSearchPageSync()
}
- select('#search-keyword, .search-input-el').then((input: HTMLInputElement) => {
- if (!input) {
- return
- }
- this.keyword = input.value
- document.addEventListener('change', e => {
- if (!(e.target instanceof HTMLInputElement)) {
- return
- }
- if (e.target.id === 'search-keyword') {
- this.keyword = e.target.value
- }
- })
+ this.focusTarget.addEventListener('index-change', () => {
+ this.handleIndexUpdate()
})
},
methods: {
getOnlineActions: lodash.debounce(getOnlineActions, 200),
getActions,
+ async setupSearchPageSync() {
+ const selector = '#search-keyword, .search-input-el'
+ const input = (await select(selector)) as HTMLInputElement
+ if (!input) {
+ return
+ }
+ this.keyword = input.value
+ urlChange(url => {
+ const params = new URLSearchParams(url)
+ this.keyword = params.get('keyword')
+ })
+ await this.$nextTick()
+ },
handleSelect() {
this.$emit('close')
this.getActions()
@@ -211,47 +220,47 @@ export default Vue.extend({
if (e.isComposing) {
return
}
- this.$refs.list.querySelector('.suggest-item:last-child').focus()
+ this.focusTarget.previous()
e.preventDefault()
},
handleDown(e: KeyboardEvent) {
if (e.isComposing) {
return
}
- this.$refs.list.querySelector('.suggest-item').focus()
+ this.focusTarget.next()
e.preventDefault()
},
- previousItem(element: HTMLElement, index: number) {
- if (index === 0) {
- this.focus()
- } else {
- ;(element.previousElementSibling as HTMLElement).focus()
+ handleIndexUpdate() {
+ if (!this.focusTarget.hasFocus) {
+ this.focusInput()
+ return
}
+ this.focusSuggestItem(this.focusTarget.index + 1)
},
- nextItem(element: HTMLElement, index: number) {
- const lastItemIndex = this.actions.length - 1
- if (index !== lastItemIndex) {
- ;(element.nextElementSibling as HTMLElement).focus()
- } else {
- this.focus()
- }
+ previousItem() {
+ this.focusTarget.previous()
+ },
+ nextItem() {
+ this.focusTarget.next()
},
search,
- onDeleteItem(element: HTMLElement, index: number) {
- this.previousItem(element, index)
+ onDeleteItem() {
+ this.focusTarget.previous()
this.getActions()
},
onClearHistory() {
- this.focus()
+ this.focusInput()
this.getActions()
},
onAction() {
- // this.focus()
this.handleSelect()
},
- focus() {
+ focusInput() {
this.$refs.input.focus()
},
+ focusSuggestItem(nth: number) {
+ this.$refs.list.querySelector(`.suggest-item:nth-child(${nth})`)?.focus()
+ },
},
})
diff --git a/src/components/launch-bar/focus-target.ts b/src/components/launch-bar/focus-target.ts
new file mode 100644
index 0000000000..6207ecffd9
--- /dev/null
+++ b/src/components/launch-bar/focus-target.ts
@@ -0,0 +1,38 @@
+export class FocusTarget extends EventTarget {
+ /**
+ * -1: Input Focus
+ * > -1: Item Focus
+ */
+ private itemIndex = -1
+ private itemLength = 0
+
+ constructor(length: number, index = -1) {
+ super()
+ this.itemLength = length
+ this.index = index
+ }
+
+ get index() {
+ return this.itemIndex
+ }
+ private set index(value: number) {
+ this.itemIndex = lodash.clamp(value, -1, this.itemLength - 1)
+ this.dispatchEvent(new CustomEvent('index-change', { detail: this }))
+ }
+ get hasFocus() {
+ return this.itemIndex > -1
+ }
+
+ reset(length: number, index = this.index) {
+ this.itemLength = length
+ this.index = index
+ }
+ next() {
+ this.index += 1
+ console.log(this.index)
+ }
+ previous() {
+ this.index -= 1
+ console.log(this.index)
+ }
+}
diff --git a/src/components/launch-bar/search-provider.ts b/src/components/launch-bar/search-provider.ts
index b7085a7416..8b6cb81126 100644
--- a/src/components/launch-bar/search-provider.ts
+++ b/src/components/launch-bar/search-provider.ts
@@ -26,11 +26,13 @@ export const searchProvider: LaunchBarActionProvider = {
content: async () =>
Vue.extend({
render: h => {
- const content = h('div', {
- domProps: {
- innerHTML: /* html */ `
${input}`,
+ const content = h(
+ 'div',
+ {
+ class: 'suggest-highlight',
},
- })
+ [input],
+ )
return content
},
}),
diff --git a/src/components/settings-panel/WidgetsPanel.vue b/src/components/settings-panel/WidgetsPanel.vue
index 228671cb80..da6c9c0783 100644
--- a/src/components/settings-panel/WidgetsPanel.vue
+++ b/src/components/settings-panel/WidgetsPanel.vue
@@ -63,6 +63,7 @@ export default Vue.extend({
},
watch: {
allWidgets() {
+ this.widgets = []
this.allWidgets.forEach(async (w: Widget) => {
const add = await widgetFilter(w)
if (add) {
@@ -93,22 +94,10 @@ export default Vue.extend({
@include v-center();
justify-content: flex-start;
align-items: flex-start;
- flex-wrap: wrap;
padding: 16px;
padding-right: 20px;
- // background-color: #f8f8f8;
- // border-radius: 0 8px 8px 0;
- // border: 1px solid #e8e8e8;
- // border-left-width: 0;
- // box-sizing: content-box;
- // overflow: auto;
@include popup();
@include text-color();
- // @include shadow();
- // body.dark & {
- // background-color: #1a1a1a;
- // border-color: #333;
- // }
&-header {
flex: 0 0 auto;
@include h-center();
diff --git a/src/components/utils/comment/areas/base.ts b/src/components/utils/comment/areas/base.ts
index 3130a07bbf..1535690d19 100644
--- a/src/components/utils/comment/areas/base.ts
+++ b/src/components/utils/comment/areas/base.ts
@@ -1,6 +1,6 @@
import { childListSubtree } from '@/core/observer'
import type { CommentItem } from '../comment-item'
-import type { CommentItemCallback } from '../types'
+import type { CommentCallbackInput, CommentCallbackPair, CommentItemCallback } from '../types'
import type { CommentReplyItem } from '../reply-item'
import { getRandomId } from '@/core/utils'
@@ -12,8 +12,7 @@ export abstract class CommentArea {
items: CommentItem[] = []
/** 与之关联的 MutationObserver */
protected observer?: MutationObserver
- protected itemAddedCallbacks: CommentItemCallback[] = []
- protected itemRemovedCallbacks: CommentItemCallback[] = []
+ protected itemCallbacks: CommentCallbackPair
[] = []
protected static replyItemClasses = ['list-item.reply-wrap', 'reply-item']
protected static replyItemSelector = CommentArea.replyItemClasses.map(c => `.${c}`).join(',')
@@ -52,7 +51,7 @@ export abstract class CommentArea {
this.items = elements.map(it => this.parseCommentItem(it as HTMLElement))
}
this.items.forEach(item => {
- this.itemAddedCallbacks.forEach(c => c(item))
+ this.itemCallbacks.forEach(c => c.added?.(item))
})
;[this.observer] = childListSubtree(this.element, records => {
const observerCallId = getRandomId()
@@ -77,14 +76,14 @@ export abstract class CommentArea {
addedCommentElements.forEach(n => {
const commentItem = this.parseCommentItem(n)
this.items.push(commentItem)
- this.itemAddedCallbacks.forEach(c => c(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.itemRemovedCallbacks.forEach(c => c(commentItem))
+ this.itemCallbacks.forEach(c => c.removed?.(commentItem))
}
})
performance.mark(`observeItems subtree end ${observerCallId}`)
@@ -98,14 +97,25 @@ export abstract class CommentArea {
performance.measure('observeItems', 'observeItems start', 'observeItems end')
}
- forEachCommentItem(callbacks: { added?: CommentItemCallback; removed?: CommentItemCallback }) {
- const { added, removed } = callbacks
- if (added) {
- this.items.forEach(item => added(item))
- this.itemAddedCallbacks.push(added)
- }
- if (removed) {
- this.itemRemovedCallbacks.push(removed)
+ destroy() {
+ this.observer?.disconnect()
+ this.items.forEach(item => {
+ this.itemCallbacks.forEach(pair => pair.removed?.(item))
+ })
+ }
+
+ static resolveCallbackPair void>(
+ input: CommentCallbackInput,
+ ): CommentCallbackPair {
+ if (typeof input === 'function') {
+ return { added: input }
}
+ return input
+ }
+
+ forEachCommentItem(input: CommentCallbackInput) {
+ const pair = CommentArea.resolveCallbackPair(input)
+ this.items.forEach(item => pair.added?.(item))
+ this.itemCallbacks.push(pair)
}
}
diff --git a/src/components/utils/comment/areas/v1.ts b/src/components/utils/comment/areas/v1.ts
index 2e1861c874..60090ece72 100644
--- a/src/components/utils/comment/areas/v1.ts
+++ b/src/components/utils/comment/areas/v1.ts
@@ -44,6 +44,7 @@ export class CommentAreaV1 extends CommentArea {
content: replyElement.querySelector('.text-con').textContent,
timeText: replyElement.querySelector('.info .time, .info .time-location').textContent,
likes: parseInt(replyElement.querySelector('.info .like span').textContent),
+ vueProps: undefined,
})
}
const item = new CommentItem({
@@ -55,6 +56,7 @@ export class CommentAreaV1 extends CommentArea {
timeText: element.querySelector('.con .info .time, .info .time-location').textContent,
likes: parseInt(element.querySelector('.con .like span').textContent),
replies: [],
+ vueProps: undefined,
})
if (dq(element, '.reply-box .view-more')) {
const replyBox = dq(element, '.reply-box') as HTMLElement
diff --git a/src/components/utils/comment/areas/v2.ts b/src/components/utils/comment/areas/v2.ts
index 5401eaacd2..0df277f7a7 100644
--- a/src/components/utils/comment/areas/v2.ts
+++ b/src/components/utils/comment/areas/v2.ts
@@ -31,7 +31,7 @@ export class CommentAreaV2 extends CommentArea {
}
/** 获取 Vue 数据 (评论 / 回复) */
- protected getReplyFromVueData(element: HTMLElement) {
+ protected getReplyFromVueProps(element: HTMLElement) {
const props = (element as HTMLElementWithVue)._vnode?.props
return props?.reply ?? props?.subReply
}
@@ -39,7 +39,7 @@ export class CommentAreaV2 extends CommentArea {
/** 获取回复对应的元素 */
protected getReplyItemElement(parent: HTMLElement, replyId: string) {
const [replyElement] = dqa(parent, '.sub-reply-item').filter(
- (it: HTMLElement) => this.getReplyFromVueData(it).rpid_str === replyId,
+ (it: HTMLElement) => this.getReplyFromVueProps(it).rpid_str === replyId,
)
return replyElement as HTMLElement
}
@@ -77,15 +77,15 @@ export class CommentAreaV2 extends CommentArea {
}
parseCommentItem(element: HTMLElement): CommentItem {
- const vueData = this.getReplyFromVueData(element)
- if (!vueData) {
+ const vueProps = this.getReplyFromVueProps(element)
+ if (!vueProps) {
throw new Error('Invalid comment item')
}
const parseReplies = () => {
- if (!vueData.replies) {
+ if (!vueProps.replies) {
return []
}
- return vueData.replies.map((r: any): CommentReplyItem => {
+ return vueProps.replies.map((r: any): CommentReplyItem => {
return new CommentReplyItem({
id: r.rpid_str,
element: this.getReplyItemElement(element, r.rpid_str),
@@ -94,23 +94,25 @@ export class CommentAreaV2 extends CommentArea {
content: r.content.message,
time: r.ctime * 1000,
likes: r.like,
+ vueProps,
})
})
}
const item = new CommentItem({
- id: vueData.rpid_str,
+ id: vueProps.rpid_str,
element,
- userId: vueData.member.mid,
- userName: vueData.member.uname,
- content: vueData.content.message,
- time: vueData.ctime * 1000,
- likes: vueData.like,
- pictures: vueData.content?.pictures?.map((img: any) => {
+ userId: vueProps.member.mid,
+ userName: vueProps.member.uname,
+ content: vueProps.content.message,
+ time: vueProps.ctime * 1000,
+ likes: vueProps.like,
+ pictures: vueProps.content?.pictures?.map((img: any) => {
return img.img_src
}),
replies: parseReplies(),
+ vueProps,
})
- if (item.replies.length < vueData.rcount) {
+ if (item.replies.length < vueProps.rcount) {
const replyBox = dq(element, '.sub-reply-list')
childList(replyBox, records => {
performance.mark(`parseReplies start ${item.id}`)
@@ -141,11 +143,11 @@ export class CommentAreaV2 extends CommentArea {
return item
}
getCommentId(element: HTMLElement): string {
- const vueData = this.getReplyFromVueData(element)
- if (!vueData?.rpid_str) {
+ const vueProps = this.getReplyFromVueProps(element)
+ if (!vueProps?.rpid_str) {
throw new Error('Invalid comment item')
}
- return vueData.rpid_str
+ return vueProps.rpid_str
}
static isV2Area(element: HTMLElement) {
diff --git a/src/components/utils/comment/comment-area-manager.ts b/src/components/utils/comment/comment-area-manager.ts
index 2f8a32fe51..0873659fc8 100644
--- a/src/components/utils/comment/comment-area-manager.ts
+++ b/src/components/utils/comment/comment-area-manager.ts
@@ -1,12 +1,18 @@
-import { allMutations } from '@/core/observer'
+import { allMutations, childList } from '@/core/observer'
import { getCommentArea } from './comment-area'
-import { CommentAreaCallback, CommentItemCallback } from './types'
+import {
+ CommentAreaCallback,
+ CommentCallbackInput,
+ CommentCallbackPair,
+ CommentItemCallback,
+} from './types'
import { CommentArea } from './areas/base'
+import { deleteValue } from '@/core/utils'
export class CommentAreaManager {
/** 当前页面所有的评论区列表 */
commentAreas: CommentArea[] = []
- commentAreaCallbacks: CommentAreaCallback[] = []
+ commentAreaCallbacks: CommentCallbackPair[] = []
protected static commentAreaClasses = ['bili-comment', 'bb-comment']
@@ -29,16 +35,29 @@ export class CommentAreaManager {
const area = getCommentArea(node)
this.commentAreas.push(area)
area.observeItems()
- this.commentAreaCallbacks.forEach(c => c(area))
+ this.commentAreaCallbacks.forEach(c => c.added?.(area))
+ const [observer] = childList(area.element.parentElement, records => {
+ records.forEach(r => {
+ r.removedNodes.forEach(removedNode => {
+ if (removedNode === area.element) {
+ deleteValue(this.commentAreas, a => a === area)
+ this.commentAreaCallbacks.forEach(c => c.removed?.(area))
+ area.destroy()
+ observer.disconnect()
+ }
+ })
+ })
+ })
}
}
- forEachCommentArea(callback: CommentAreaCallback) {
- this.commentAreas.forEach(it => callback(it))
- this.commentAreaCallbacks.push(callback)
+ forEachCommentArea(input: CommentCallbackInput) {
+ const pair = CommentArea.resolveCallbackPair(input)
+ this.commentAreas.forEach(it => pair.added?.(it))
+ this.commentAreaCallbacks.push(pair)
}
- forEachCommentItem(callbacks: { added?: CommentItemCallback; removed?: CommentItemCallback }) {
+ forEachCommentItem(callbacks: CommentCallbackPair) {
this.forEachCommentArea(area => area.forEachCommentItem(callbacks))
}
}
diff --git a/src/components/utils/comment/reply-item.ts b/src/components/utils/comment/reply-item.ts
index 3f73f9f35d..c15bbc2411 100644
--- a/src/components/utils/comment/reply-item.ts
+++ b/src/components/utils/comment/reply-item.ts
@@ -16,6 +16,8 @@ export class CommentReplyItem extends EventTarget {
time?: number
/** 点赞数 */
likes: number
+ /** 对应的 Vue Props */
+ vueProps: any
constructor(initParams: Omit) {
super()
@@ -27,5 +29,6 @@ export class CommentReplyItem extends EventTarget {
this.timeText = initParams.timeText
this.time = initParams.time
this.likes = initParams.likes
+ this.vueProps = initParams.vueProps
}
}
diff --git a/src/components/utils/comment/types.ts b/src/components/utils/comment/types.ts
index c11dddfc53..af8d781b13 100644
--- a/src/components/utils/comment/types.ts
+++ b/src/components/utils/comment/types.ts
@@ -8,3 +8,7 @@ export const RepliesUpdateEventType = 'repliesUpdate'
export type RepliesUpdateEventCallback = (event: CustomEvent) => void
export type CommentItemCallback = (item: CommentItem) => void
export type CommentAreaCallback = (area: CommentArea) => void
+export type CommentCallbackPair void> = { added?: T; removed?: T }
+export type CommentCallbackInput void> =
+ | CommentCallbackPair
+ | T