Bilibili-Evolved/registry/lib/components/utils/bigger-video-preview/index.ts
2025-06-29 14:56:21 +08:00

304 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Vue from 'vue'
import { ComponentEntry } from '@/components/types'
import { defineComponentMetadata } from '@/components/define'
import { childListSubtree } from '@/core/observer'
import { select } from '@/core/spin-query'
import { useScopedConsole } from '@/core/utils/log'
import PreviewButton from './PreviewButton.vue'
const logger = useScopedConsole('biggerVideoPreview')
const entry: ComponentEntry = async ({ settings }) => {
// 预览容器
let videoContainer = null
// #region functions
/**
* 初始化预览容器
*/
async function initPreviewContainer() {
const VideoContainerModule = await import('./VideoContainer.vue')
const VideoContainer = VideoContainerModule.default
const ModalClass = Vue.extend(VideoContainer)
videoContainer = new ModalClass()
videoContainer.$mount()
document.body.appendChild(videoContainer.$el)
}
/**
* 创建预览按钮的容器元素(由 Vue 渲染)
* @param {string} className - 按钮的类名
* @returns {HTMLElement} 预览按钮的容器元素
*/
function createPreviewButton(className: string): HTMLElement {
// #region 局部定义
/**
* 切换预览按钮图标
* @param show 是否显示预览
* @param btn 预览按钮实例
*/
const toggleButtonIcon = (show: boolean, btn: any) => {
btn.enlarged = show
}
/**
* 鼠标移出事件处理函数
* @param {MouseEvent} event - 鼠标事件
*/
const onMouseExitHandler = (event: MouseEvent) => {
event.stopPropagation()
}
/**
* 切换鼠标移出事件监听
* @param {boolean} show - 是否显示预览
* @param {Element} movingDom - 预览元素的 DOM
*/
const toggleMouseExitHandler = (show: boolean, movingDom: Element) => {
if (settings.options.blockMouseExitHandler && show) {
movingDom.addEventListener('mouseleave', onMouseExitHandler, true)
} else {
movingDom.removeEventListener('mouseleave', onMouseExitHandler, true)
// 触发mouseleave事件停止预览
const mouseLeaveEvent = new MouseEvent('mouseleave')
movingDom.dispatchEvent(mouseLeaveEvent)
}
}
/**
* 切换视频控件显示
* @param {Element} movingDom - 预览元素的 DOM
* @param {boolean} show - 是否显示控件
*/
const toggleVideoControls = (movingDom: Element, show: boolean) => {
const video = movingDom.querySelector('video')
// 首页视频卡片的时间条
const controlWrap = movingDom.querySelector('.bpx-player-control-wrap') as HTMLElement
if (video) {
if (settings.options.showVideoControls && show) {
video.controls = true
video.style.pointerEvents = 'visible'
if (controlWrap) {
controlWrap.style.display = 'none'
}
} else {
video.controls = false
video.style.pointerEvents = 'none'
if (controlWrap) {
controlWrap.style.display = 'unset'
}
}
}
}
/**
* 视频事件更新事件处理函数
* @param {Event} event - 事件对象
*/
const onTimeUpdateHandler = (event: Event) => {
event.stopImmediatePropagation()
}
/**
* 切换预览时长限制
* @param {Element} movingDom - 预览元素的 DOM
*/
const togglePreviewTimeLimit = (movingDom: Element) => {
if (settings.options.removePreviewTimeLimit) {
const video = movingDom.querySelector('video')
if (video) {
video.removeEventListener('timeupdate', onTimeUpdateHandler, true)
video.addEventListener('timeupdate', onTimeUpdateHandler, true)
}
} else {
const video = movingDom.querySelector('video')
if (video) {
video.removeEventListener('timeupdate', onTimeUpdateHandler, true)
}
}
}
// 提升 popupChangeHandler 到外部作用域,确保引用一致
let popupChangeHandler: (show: boolean) => void
// #endregion
// 创建 Vue 实例
const ComponentClass = Vue.extend(PreviewButton)
const instance = new ComponentClass({
propsData: {
btnClass: className,
btnOnClickCallback: (e: MouseEvent) => {
e.preventDefault()
if (!videoContainer) {
return
}
const movingDom = instance.$el.parentElement.closest(
'.bili-video-card__image--wrap,.pic-box',
)
// 监听弹窗状态变化事件
if (!popupChangeHandler) {
popupChangeHandler = (show: boolean) => {
// 切换按钮图标
toggleButtonIcon(show, instance)
// 切换鼠标移出事件监听
toggleMouseExitHandler(show, movingDom)
// 切换视频控件显示
toggleVideoControls(movingDom, show)
// 切换预览时长限制
togglePreviewTimeLimit(movingDom)
if (!show) {
videoContainer.$off('popup-change', popupChangeHandler)
}
}
}
// 根据当前状态切换预览
if (instance.enlarged) {
videoContainer.closePopup()
} else {
videoContainer.$el.style.width = `${settings.options.popupWidth}%`
videoContainer.$off('popup-change', popupChangeHandler)
videoContainer.$on('popup-change', popupChangeHandler)
videoContainer.openPopup(movingDom)
}
},
},
}) as Vue & { enlarged: boolean }
instance.$mount()
return instance.$el as HTMLElement
}
/**
* 插入预览放大按钮
* @param {HTMLElement} node - 要插入按钮的节点
* @param {string} className - 按钮的类名
*/
function insertPreviewButton(node: HTMLElement, className: string) {
const wrap = node.querySelectorAll('.v-inline-player,.v-recommend-inline-player')
wrap.forEach(it => {
// 检查父元素下是否已经有该按钮,避免重复插入
if (it.parentElement.querySelector(`.${className}`)) {
return
}
const div = createPreviewButton(className)
it.parentElement.appendChild(div)
})
}
/**
* 初始化预览放大按钮(通用)
* @param btnClassName 按钮类名
* @param videoClassName 视频卡片类名
* @param containerSelector 容器选择器
* @param getInsertNode 获取插入按钮的节点
*/
async function initPreviewButton(
btnClassName: string,
videoClassName: string,
containerSelector: string,
getInsertNode: (element: HTMLElement) => HTMLElement,
) {
const containerNode = (await select(containerSelector)) as HTMLElement
if (!containerNode) {
logger.warn('未找到容器节点:', containerSelector)
return
}
const wrap = containerNode.querySelectorAll(`.${videoClassName}`)
logger.debug(`初始化已有卡片: `, wrap.length)
// 初始化已有卡片
insertPreviewButton(containerNode, btnClassName)
// 监听容器变化
childListSubtree(containerNode, records => {
records.forEach(record => {
record.addedNodes.forEach(n => {
if (n.nodeType !== Node.ELEMENT_NODE) {
return
}
const element = n as HTMLElement
if (
!element.classList.contains(videoClassName) &&
element.querySelector(`.${videoClassName}`) === null
) {
return
}
logger.debug('初始化卡片')
// 初始化卡片
insertPreviewButton(getInsertNode(element), btnClassName)
})
})
})
}
// #endregion
// 初始化预览容器
logger.debug('初始化预览容器')
await initPreviewContainer()
// 初始化预览放大按钮
if (document.URL.replace(window.location.search, '') === 'https://www.bilibili.com/') {
logger.debug('初始化首页预览放大按钮')
// 首页
initPreviewButton(
'bigger-video-preview-button-index',
'v-inline-player',
'.container',
element => element,
)
} else if (document.URL.startsWith('https://www.bilibili.com/video/')) {
logger.debug('初始化视频页预览放大按钮')
// 视频页
initPreviewButton(
'bigger-video-preview-button-video',
'v-recommend-inline-player',
'.recommend-list-v1',
element => element.parentElement,
)
}
}
export const component = defineComponentMetadata({
name: 'biggerVideoPreview',
displayName: '视频预览放大',
entry,
tags: [componentsTags.utils, componentsTags.video],
options: {
popupWidth: {
displayName: '弹窗宽度(%',
defaultValue: 90,
slider: {
min: 10,
max: 100,
step: 1,
},
},
blockMouseExitHandler: {
displayName: '鼠标移出弹窗不停止预览',
defaultValue: true,
},
showVideoControls: {
displayName: '显示视频控件',
defaultValue: true,
},
removePreviewTimeLimit: {
displayName: '解除5分钟预览限制弹幕会停止',
defaultValue: false,
},
},
})