Add feeds cards manager v2

This commit is contained in:
the1812 2022-04-10 15:38:49 +08:00
parent 512cf8e218
commit 3be42acde2
6 changed files with 160 additions and 83 deletions

View File

@ -3,7 +3,7 @@ import { childList, childListSubtree } from '@/core/observer'
import { select } from '@/core/spin-query'
import { liveUrls } from '@/core/utils/urls'
import { addData } from '@/plugins/data'
import { FeedsCardsManager } from '.'
import { FeedsCardsManager } from './base'
/** 表示一个动态卡片列表适配器, 可以对在不同页面的地方里的动态列表提供支持 */
export interface FeedsCardsListAdaptor {
@ -133,7 +133,7 @@ addData(ListAdaptorKey, (adaptors: FeedsCardsListAdaptor[]) => {
'https://t.bilibili.com/',
],
watchCardsList: async manager => {
const list = await select('.feed-card .content, .detail-content .detail-card') as HTMLElement
const list = await select('.feed-card .content, .detail-content .detail-card, .bili-dyn-list__items') as HTMLElement
if (!list) {
return false
}

View File

@ -0,0 +1,81 @@
import { matchUrlPattern } from '@/core/utils'
import { registerAndGetData } from '@/plugins/data'
import { FeedsCardCallback, FeedsCard } from '../types'
import { ListAdaptorKey, FeedsCardsListAdaptor } from './adaptor'
export const feedsCardCallbacks: Required<FeedsCardCallback>[] = []
// eslint-disable-next-line no-underscore-dangle
export const getVueData = (el: any) => el.__vue__ || el.parentElement.__vue__
export const createNodeValidator = (className: string) => (node: Node): node is HTMLElement => (
node && node instanceof HTMLElement && node.parentNode && node.classList.contains(className)
)
/** 动态卡片管理器支持的自定义事件 */
export enum FeedsCardsManagerEventType {
/** 新增动态卡片 */
AddCard = 'addCard',
/** 移除动态卡片 */
RemoveCard = 'removeCard',
}
/** 动态卡片管理器, 封装了动态卡片的解析与监测 */
export abstract class FeedsCardsManager extends EventTarget {
/** 是否已在监测中 */
watching = false
/** 监测到的动态卡片列表 */
cards: FeedsCard[] = []
/** 监听动态卡片的增加/移除 */
addEventListener(
type: FeedsCardsManagerEventType,
listener: (event: CustomEvent<FeedsCard>) => void,
options?: boolean | AddEventListenerOptions | undefined,
): void {
super.addEventListener(type, listener, options)
}
/** 取消监听动态卡片的增加/移除 */
removeEventListener(
type: FeedsCardsManagerEventType,
callback: (event: CustomEvent<FeedsCard>) => void,
options?: boolean | AddEventListenerOptions | undefined,
): void {
super.removeEventListener(type, callback, options)
}
/** 触发自定义事件 */
dispatchCardEvent(type: FeedsCardsManagerEventType, card: FeedsCard) {
const event = new CustomEvent(type, { detail: card })
this.dispatchEvent(event)
feedsCardCallbacks.forEach(c => c[type === FeedsCardsManagerEventType.AddCard ? 'added' : 'removed'](card))
}
/** 对当前页面开始监测 */
async startWatching() {
if (this.watching) {
return true
}
this.watching = true
const [adaptors] = registerAndGetData(ListAdaptorKey, [] as FeedsCardsListAdaptor[])
const adaptor = adaptors.find(a => a.match.some(p => matchUrlPattern(p)))
if (!adaptor) {
console.warn('[FeedsCardsManager] No adaptor found', adaptors)
return false
}
return adaptor.watchCardsList(this)
}
/**
*
* @param node
*/
abstract addCard(node: Node): Promise<void>
/**
*
* @param node
*/
abstract removeCard(node: Node): Promise<void>
/**
*
* @param cardsList
*/
abstract updateCards(cardsList: HTMLElement): readonly [MutationObserver, MutationObserverInit]
}

View File

@ -1,66 +1,9 @@
import { getCookieValue } from '@/core/utils'
import { FeedsCard, FeedsCardCallback } from '../types'
import { FeedsCardCallback } from '../types'
import { feedsCardCallbacks } from './base'
import { FeedsCardsManagerV1 } from './v1'
import { FeedsCardsManagerV2 } from './v2'
const feedsCardCallbacks: Required<FeedsCardCallback>[] = []
/** 动态卡片管理器支持的自定义事件 */
export const enum FeedsCardsManagerEventType {
/** 新增动态卡片 */
AddCard = 'addCard',
/** 移除动态卡片 */
RemoveCard = 'removeCard',
}
/** 动态卡片管理器, 封装了动态卡片的解析与监测 */
export abstract class FeedsCardsManager extends EventTarget {
/** 是否已在监测中 */
watching = false
/** 监测到的动态卡片列表 */
cards: FeedsCard[] = []
/** 监听动态卡片的增加/移除 */
addEventListener(
type: FeedsCardsManagerEventType,
listener: (event: CustomEvent<FeedsCard>) => void,
options?: boolean | AddEventListenerOptions | undefined,
): void {
super.addEventListener(type, listener, options)
}
/** 取消监听动态卡片的增加/移除 */
removeEventListener(
type: FeedsCardsManagerEventType,
callback: (event: CustomEvent<FeedsCard>) => void,
options?: boolean | AddEventListenerOptions | undefined,
): void {
super.removeEventListener(type, callback, options)
}
/** 触发自定义事件 */
dispatchCardEvent(type: FeedsCardsManagerEventType, card: FeedsCard) {
const event = new CustomEvent(type, { detail: card })
this.dispatchEvent(event)
feedsCardCallbacks.forEach(c => c[type === FeedsCardsManagerEventType.AddCard ? 'added' : 'removed'](card))
}
/**
*
* @param node
*/
abstract addCard(node: Node): Promise<void>
/**
*
* @param node
*/
abstract removeCard(node: Node): Promise<void>
/**
*
* @param cardsList
*/
abstract updateCards(cardsList: HTMLElement): readonly [MutationObserver, MutationObserverInit]
/** 对当前页面开始监测 */
abstract startWatching(): Promise<boolean>
}
export const feedsCardsManager = (() => {
const isV2Feeds = parseInt(getCookieValue('hit-dyn-v2')) > 0
if (isV2Feeds) {

View File

@ -1,10 +1,7 @@
import { childList } from '@/core/observer'
import { sq } from '@/core/spin-query'
import { matchUrlPattern } from '@/core/utils'
import { registerAndGetData } from '@/plugins/data'
import { FeedsCardsManager, FeedsCardsManagerEventType } from '.'
import { FeedsCardsManager, FeedsCardsManagerEventType, getVueData } from './base'
import { feedsCardTypes, FeedsCard, RepostFeedsCard, FeedsCardType } from '../types'
import { FeedsCardsListAdaptor, ListAdaptorKey } from './adaptor'
const getFeedsCardType = (element: HTMLElement) => {
if (element.querySelector('.repost')) {
@ -45,8 +42,6 @@ const isRepostType = (card: FeedsCard): card is RepostFeedsCard => (
* @param element
*/
const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
// eslint-disable-next-line no-underscore-dangle
const getVueData = (el: any) => el.__vue__ || el.parentElement.__vue__
const getSimpleText = async (selector: string) => {
const subElement = await sq(
() => element.querySelector(selector),
@ -234,17 +229,4 @@ export class FeedsCardsManagerV1 extends FeedsCardsManager {
})
})
}
async startWatching() {
if (this.watching) {
return true
}
this.watching = true
const [adaptors] = registerAndGetData(ListAdaptorKey, [] as FeedsCardsListAdaptor[])
const adaptor = adaptors.find(a => a.match.some(p => matchUrlPattern(p)))
if (!adaptor) {
console.warn('[FeedsCardsManager] No adaptor found', adaptors)
return false
}
return adaptor.watchCardsList(this)
}
}

View File

@ -1,6 +1,77 @@
import { FeedsCardsManager } from '.'
import { childList } from '@/core/observer'
import { descendingStringSort } from '@/core/utils/sort'
import { createNodeValidator, FeedsCardsManager, FeedsCardsManagerEventType, getVueData } from './base'
import { FeedsCard, feedsCardTypes } from '../types'
/** 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,
}
const parseCard = async (element: HTMLElement): Promise<FeedsCard> => {
const vueData = getVueData(element)
const { modules, id_str, type } = vueData.data
const { desc } = modules.module_dynamic
const { name } = modules.module_author
const { like, forward, comment } = modules.module_stat
return {
id: id_str,
username: name,
likes: like.count,
reposts: forward.count,
comments: comment.count,
text: desc.text,
type: feedsCardTypeMap[lodash.startCase(type)] ?? feedsCardTypeMap.DynamicTypeWord,
element,
get presented() { return element.parentNode !== null },
async getText() {
return getVueData(element).data.modules.module_dynamic.desc.text
},
}
}
const isNodeValid = createNodeValidator('bili-dyn-item')
/** 新版动态卡片管理器实现 */
export class FeedsCardsManagerV2 extends FeedsCardsManager {
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 = getVueData(node)
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-item'
const cards = dqa(cardsList, selector)
cards.forEach(it => this.addCard(it))
return childList(cardsList, records => {
records.forEach(record => {
record.addedNodes.forEach(node => this.addCard(node))
record.removedNodes.forEach(node => this.removeCard(node))
})
})
}
}

View File

@ -245,7 +245,7 @@ export const formData = (obj: Record<string, any>, config?: { encode?: boolean }
/**
*
* @param target
* @param property
* @param predicate
*/
export const deleteValue = <ItemType> (
target: ItemType[],