import { childList } from '@/core/observer' import { descendingStringSort } from '@/core/utils/sort' import { pascalCase, getVue2Data } from '@/core/utils' import { createNodeValidator, FeedsCardsManager, FeedsCardsManagerEventType } from './base' import { FeedsCard, FeedsCardType, feedsCardTypes, isRepostType } from '../types' import { selectAll } from '@/core/spin-query' /** b 站的动态卡片 type 标记 -> FeedsCard.type */ const feedsCardTypeMap = { DynamicTypeForward: feedsCardTypes.repost, DynamicTypeAv: feedsCardTypes.video, DynamicTypeDraw: feedsCardTypes.textWithImages, DynamicTypeWord: feedsCardTypes.text, DynamicTypePgc: feedsCardTypes.bangumi, DynamicTypeArticle: feedsCardTypes.column, DynamicTypeMusic: feedsCardTypes.audio, DynamicTypeLiveRcmd: feedsCardTypes.liveRecord, DynamicTypeCoursesSeason: feedsCardTypes.courses, DynamicTypeOpus: feedsCardTypes.textWithImages, DynamicTypeLive: feedsCardTypes.live, DynamicTypeMedialist: feedsCardTypes.mediaList, DynamicTypeSubscription: feedsCardTypes.mediaList, DynamicTypeUgcSeason: feedsCardTypes.ugcSeason, } const combineText = (...texts: string[]) => texts .filter(it => Boolean(it)) .join('\n') .trim() const getType = (rawType: string): FeedsCardType => feedsCardTypeMap[pascalCase(rawType)] ?? feedsCardTypes.unknown const getText = (dynamicModule: any, cardType: FeedsCardType) => { const isOpusModule = Object.hasOwn(dynamicModule, 'paragraphs') if (isOpusModule) { const paragraphs = dynamicModule.paragraphs as any[] const textParagraph = paragraphs.find(it => it.para_type === 1) const text = (textParagraph.text.nodes as any[]) .map(node => { if (node.type === 'TEXT_NODE_TYPE_WORD') { return lodash.get(node, 'word.words') } if (node.type === 'TEXT_NODE_TYPE_RICH') { return lodash.get(node, 'rich.orig_text') } return '' }) .join('') return text } const { desc: mainDesc, major } = dynamicModule const mainText = (() => { if (major?.opus) { return lodash.get(major.opus, 'summary.text') } return mainDesc?.text ?? '' })() const typeText = (() => { switch (cardType) { default: { return '' } case feedsCardTypes.bangumi: case feedsCardTypes.column: case feedsCardTypes.video: { const target = major.archive ?? major.pgc ?? major.article if (target) { const { title, desc } = target return combineText(title, desc) } if (major.opus) { const { title, summary } = major.opus return combineText(title, summary.text) } return '' } } })() return combineText(mainText, typeText) } const parseCard = async (element: HTMLElement): Promise => { const vueData = getVue2Data(element) const parseModules = (rawModules: any) => { if (Array.isArray(rawModules)) { return Object.fromEntries( rawModules.map(it => { const [key] = Object.keys(it).filter(k => k !== 'module_type') if (key === 'module_content') { return ['module_dynamic', it[key]] } return [key, it[key]] }), ) } return rawModules } const { modules: rawModules, id_str, type } = vueData.data const modules = parseModules(rawModules) const { name } = modules.module_author const { like, forward, comment } = modules.module_stat const cardType = getType(type) element.dataset.type = cardType.id.toString() const card = { id: id_str, username: name, likes: like.count, reposts: forward.count, comments: comment.count, text: '', type: cardType, element, get presented() { return document.body.contains(element) }, async getText() { return getText(modules.module_dynamic, cardType) }, } if (isRepostType(card)) { const currentUsername = card.username const { module_author: { name: repostUsername }, module_dynamic: repostDynamicModule, } = parseModules(vueData.data.orig.modules) const repostCardType = getType(vueData.data.orig.type) card.repostUsername = repostUsername card.repostText = getText(repostDynamicModule, repostCardType) if (repostUsername === currentUsername) { element.setAttribute('data-self-repost', 'true') } card.getText = async () => combineText(getText(modules.module_dynamic, cardType), getText(repostDynamicModule, cardType)) card.repostId = vueData.data.orig.id_str } card.text = await card.getText() card.element.setAttribute('data-did', card.id) // 等待第一次 Vue 渲染完成 await selectAll(() => element.querySelectorAll('.bili-dyn-item *'), { queryInterval: 50 }) return card } const isNodeValid = createNodeValidator('.bili-dyn-list__item, .bili-dyn-item, .bili-opus-view') /** 新版动态卡片管理器实现 */ export class FeedsCardsManagerV2 extends FeedsCardsManager { readonly managerType = 'v2' async addCard(node: Node) { if (!isNodeValid(node)) { return } const card = await parseCard(node) if (!card.presented) { return } this.cards.push(card) this.cards.sort(descendingStringSort(c => c.id)) this.dispatchCardEvent(FeedsCardsManagerEventType.AddCard, card) } async removeCard(node: Node) { if (!isNodeValid(node)) { return } const vueData = getVue2Data(node) if (!vueData) { return } const id = vueData.data?.id_str ?? '0' const index = this.cards.findIndex(c => c.id === id) if (index === -1) { return } const [card] = this.cards.splice(index, 1) this.dispatchCardEvent(FeedsCardsManagerEventType.RemoveCard, card) } updateCards(cardsList: HTMLElement) { const selector = '.bili-dyn-list__item, :not(.bili-dyn-list__item) > .bili-dyn-item, .bili-opus-view' const cards = dqa(cardsList, selector) cards.forEach(it => this.addCard(it)) const getCardNode = (node: Node) => { if (!isNodeValid(node)) { return null } if (node.matches(selector)) { return node as HTMLElement } return node.querySelector(selector) as HTMLElement } return childList(cardsList, records => { records.forEach(record => { record.addedNodes.forEach(node => this.addCard(getCardNode(node))) record.removedNodes.forEach(node => this.removeCard(getCardNode(node))) }) this.cleanUpCards() }) } }