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
af2906e641
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<VButton :class="['bigger-video-preview-button', btnClass]" style="padding: 0" @click="onClick">
|
||||
<VIcon :icon="enlarged ? 'mdi-magnify-minus' : 'mdi-magnify-plus'" :size="16"></VIcon>
|
||||
</VButton>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { VIcon, VButton } from '@/ui'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PreviewButton',
|
||||
components: {
|
||||
VIcon,
|
||||
VButton,
|
||||
},
|
||||
props: {
|
||||
btnClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
btnOnClickCallback: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
enlarged: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(event: MouseEvent) {
|
||||
this.btnOnClickCallback(event)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// 通用放大按钮样式
|
||||
.bigger-video-preview-button {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
background-color: aqua;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.bili-video-card__image--wrap:hover .bigger-video-preview-button-index {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 首页视频卡片预览放大按钮
|
||||
.bigger-video-preview-button-index {
|
||||
@extend .bigger-video-preview-button;
|
||||
}
|
||||
|
||||
.pic-box:hover .bigger-video-preview-button-video {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// 视频页视频卡片预览放大按钮
|
||||
.bigger-video-preview-button-video {
|
||||
@extend .bigger-video-preview-button;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<VPopup
|
||||
ref="popup"
|
||||
v-model="showPopup"
|
||||
class="bigger-video-preview-video-container"
|
||||
@popup-change="onPopupChange"
|
||||
>
|
||||
</VPopup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { VPopup } from '@/ui'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideoContainer',
|
||||
components: {
|
||||
VPopup,
|
||||
},
|
||||
model: {
|
||||
prop: 'showPopup',
|
||||
event: 'popup-change',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPopup: false,
|
||||
movedDom: null as HTMLElement | null,
|
||||
originalParent: null as Node | null,
|
||||
originalNextSibling: null as Node | null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openPopup(dom: HTMLElement) {
|
||||
this.restoreDom()
|
||||
if (!dom) {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存原父节点和下一个兄弟节点
|
||||
this.originalParent = dom.parentNode
|
||||
this.originalNextSibling = dom.nextSibling
|
||||
|
||||
this.togglePopup()
|
||||
// 移动到popup
|
||||
this.$nextTick(() => {
|
||||
const popup = this.$refs.popup as any
|
||||
if (popup && dom.parentNode !== popup) {
|
||||
popup.$el.appendChild(dom)
|
||||
this.movedDom = dom
|
||||
}
|
||||
})
|
||||
},
|
||||
closePopup() {
|
||||
this.restoreDom()
|
||||
this.togglePopup()
|
||||
},
|
||||
restoreDom() {
|
||||
if (this.movedDom && this.originalParent) {
|
||||
if (this.originalNextSibling) {
|
||||
this.originalParent.insertBefore(this.movedDom, this.originalNextSibling)
|
||||
} else {
|
||||
this.originalParent.appendChild(this.movedDom)
|
||||
}
|
||||
this.movedDom = null
|
||||
this.originalParent = null
|
||||
this.originalNextSibling = null
|
||||
}
|
||||
},
|
||||
onPopupChange(val: boolean) {
|
||||
if (!val) {
|
||||
this.restoreDom()
|
||||
}
|
||||
this.$emit('popup-change', val)
|
||||
},
|
||||
togglePopup() {
|
||||
// 调用VPopup的toggle方法
|
||||
const popup = this.$refs.popup as any
|
||||
if (popup && typeof popup.toggle === 'function') {
|
||||
popup.toggle()
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.bigger-video-preview-video-container {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 99999;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,4 @@
|
||||
放大鼠标悬停于视频卡片时播放的5分钟预览
|
||||
|
||||
- 点击放大镜图标放大预览
|
||||
- 点击缩小图标或者预览框外空白处关闭预览
|
||||
303
registry/lib/components/utils/bigger-video-preview/index.ts
Normal file
303
registry/lib/components/utils/bigger-video-preview/index.ts
Normal file
@ -0,0 +1,303 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -133,9 +133,9 @@ export default Vue.extend({
|
||||
const eventTypes = ['mousedown', 'touchstart']
|
||||
eventTypes.forEach(type => {
|
||||
if (this.open) {
|
||||
document.documentElement.addEventListener(type, e => this.openHandler(e))
|
||||
document.documentElement.addEventListener(type, this.openHandler)
|
||||
} else {
|
||||
document.documentElement.removeEventListener(type, e => this.openHandler(e))
|
||||
document.documentElement.removeEventListener(type, this.openHandler)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user