mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
fix: #2561
This commit is contained in:
parent
72dc861245
commit
cdcb7b4b73
@ -1,393 +1,299 @@
|
|||||||
/* Author: JLoeve (https://github.com/LonelySteve) */
|
|
||||||
|
|
||||||
import { allMutationsOn, videoChange } from '@/core/observer'
|
|
||||||
import { getComponentSettings } from '@/core/settings'
|
|
||||||
import { select } from '@/core/spin-query'
|
|
||||||
import { ascendingSort } from '@/core/utils/sort'
|
|
||||||
import { playerAgent } from '@/components/video/player-agent'
|
import { playerAgent } from '@/components/video/player-agent'
|
||||||
|
import { videoChange } from '@/core/observer'
|
||||||
|
import {
|
||||||
|
addListener,
|
||||||
|
calcOrder,
|
||||||
|
enabled,
|
||||||
|
extendedAgent,
|
||||||
|
formatSpeedText,
|
||||||
|
getAid,
|
||||||
|
getExtendedSupportedRates,
|
||||||
|
getSupportedRates,
|
||||||
|
nativeSupportedRates,
|
||||||
|
options,
|
||||||
|
removeListeners,
|
||||||
|
trimLeadingDot,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
export const minValue = 0.0625
|
const activeClassName = trimLeadingDot(extendedAgent.custom.active.selector)
|
||||||
export const maxValue = 16
|
const showClassName = trimLeadingDot(extendedAgent.custom.show.selector)
|
||||||
export const stepValue = 0.5
|
|
||||||
export const errorMessageDuration = 2000
|
|
||||||
|
|
||||||
export const calcOrder = (value: number) => ((maxValue - value) * 10000).toString()
|
// 原生倍速,应该与官方播放器内部维护的值保持一致
|
||||||
const getAid = (aid = unsafeWindow.aid) => {
|
let nativeSpeed: number
|
||||||
if (!aid) {
|
// 分 P 切换时共享前一个倍速,这里指定初始倍速可以是 undefined,不需要是 1
|
||||||
throw new Error('aid is unknown')
|
let sharedPreviousSpeed: number
|
||||||
|
// 分 P 切换时共享同一个倍速,这里指定初始倍速可以是 undefined,不需要是 1
|
||||||
|
let sharedSpeed: number
|
||||||
|
// 分 P 切换时共享同一个原生倍速值,初始值设置为 1
|
||||||
|
let sharedNativeSpeed = 1
|
||||||
|
|
||||||
|
let containerElement: HTMLElement
|
||||||
|
let menuListElement: HTMLElement
|
||||||
|
let videoElement: HTMLVideoElement
|
||||||
|
let nameBtn: HTMLButtonElement
|
||||||
|
|
||||||
|
const getRememberSpeed = (aid?: string) => {
|
||||||
|
for (const [level, aids] of Object.entries(options.individualRememberList)) {
|
||||||
|
if (aids.some(aid_ => aid_.toString() === getAid(aid).toString())) {
|
||||||
|
return parseFloat(level)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return aid
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const rememberVideoSpeed = getComponentSettings('rememberVideoSpeed')
|
const getFallbackVideoSpeed = () => {
|
||||||
const options = rememberVideoSpeed.options as {
|
// 如果组件被启用才使用存储的后备值
|
||||||
speed: string
|
if (enabled) {
|
||||||
extend: boolean
|
return parseFloat(options.speed)
|
||||||
extendList: number[]
|
}
|
||||||
individualRemember: boolean
|
return null
|
||||||
individualRememberList: Record<string, string[]>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const extendedAgent = playerAgent.provideCustomQuery({
|
/**
|
||||||
video: {
|
* 忘记对指定 aid 记忆的倍速,返回值表示指定的 aid 之前是否被记忆
|
||||||
speedMenuList: 'bilibili-player-video-btn-speed-menu',
|
*
|
||||||
speedMenuItem: 'bilibili-player-video-btn-speed-menu-list',
|
* @param aid 要忘记的 aid,若不指定则从页面中自动获取
|
||||||
speedNameBtn: 'bilibili-player-video-btn-speed-name',
|
*/
|
||||||
speedContainer: 'bilibili-player-video-btn-speed',
|
const forgetSpeed = (aid?: string) => {
|
||||||
active: 'bilibili-player-active',
|
aid = getAid(aid)
|
||||||
show: 'bilibili-player-speed-show',
|
|
||||||
},
|
|
||||||
bangumi: {
|
|
||||||
speedMenuList: 'squirtle-speed-select-list',
|
|
||||||
speedMenuItem: 'squirtle-select-item',
|
|
||||||
speedNameBtn: 'squirtle-speed-select-result',
|
|
||||||
speedContainer: 'squirtle-speed-wrap',
|
|
||||||
active: 'active',
|
|
||||||
// bangumi 那边没有这种 class, 随便填一个就行了
|
|
||||||
show: 'bilibili-player-speed-show',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const trimLeadingDot = (selector: string) => selector.replace(/^\./, '')
|
|
||||||
|
|
||||||
export class VideoSpeedController {
|
let aidOldIndex = -1
|
||||||
static readonly classNameMap = {
|
for (const aids of Object.values(options.individualRememberList)) {
|
||||||
speedMenuList: trimLeadingDot(extendedAgent.custom.speedMenuList.selector),
|
aidOldIndex = aids.indexOf(aid)
|
||||||
speedMenuItem: trimLeadingDot(extendedAgent.custom.speedMenuItem.selector),
|
if (aidOldIndex !== -1) {
|
||||||
speedNameBtn: trimLeadingDot(extendedAgent.custom.speedNameBtn.selector),
|
aids.splice(aidOldIndex, 1)
|
||||||
speedContainer: trimLeadingDot(extendedAgent.custom.speedContainer.selector),
|
break
|
||||||
active: trimLeadingDot(extendedAgent.custom.active.selector),
|
|
||||||
show: trimLeadingDot(extendedAgent.custom.show.selector),
|
|
||||||
video: trimLeadingDot(extendedAgent.query.video.container.selector),
|
|
||||||
}
|
|
||||||
static readonly nativeSupportedRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
|
|
||||||
static instanceMap = new WeakMap<HTMLElement, VideoSpeedController>()
|
|
||||||
// 从低到高去重排序
|
|
||||||
static get extendedSupportedRates() {
|
|
||||||
return Array.from(new Set(options.extendList)).sort(ascendingSort())
|
|
||||||
}
|
|
||||||
static get supportedRates() {
|
|
||||||
if (options.extend) {
|
|
||||||
return [
|
|
||||||
...VideoSpeedController.nativeSupportedRates,
|
|
||||||
...VideoSpeedController.extendedSupportedRates,
|
|
||||||
].sort(ascendingSort())
|
|
||||||
}
|
}
|
||||||
return VideoSpeedController.nativeSupportedRates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return aidOldIndex !== -1
|
||||||
* 获取后备默认速度
|
}
|
||||||
*/
|
|
||||||
static get fallbackVideoSpeed() {
|
/**
|
||||||
// 向下兼容,原本 settings.defaultVideoSpeed 被设计为 string 类型,用于存储全局倍速
|
* 为指定 aid 记忆指定倍速
|
||||||
if (rememberVideoSpeed.enabled) {
|
*
|
||||||
return parseFloat(options.speed)
|
* @param speed 要记忆的倍速
|
||||||
}
|
* @param force 对于之前没有被记忆的 aid,**如果不将此参数设置为 `true`,调用完成也不会将相应的倍速记忆到设置中的**
|
||||||
return null
|
* @param aid 要记忆的 aid,若不指定则从页面中自动获取
|
||||||
|
*/
|
||||||
|
const rememberSpeed = (speed: number, force = false, aid?: string) => {
|
||||||
|
aid = getAid(aid)
|
||||||
|
const remembered = forgetSpeed(aid)
|
||||||
|
// 对于没有被记忆的 aid,并且 force 参数为假就直接返回
|
||||||
|
if (!remembered && !force) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
// 为新的速度值初始化相应的 aid 数组
|
||||||
static formatSpeedText(speed: number) {
|
if (!options.individualRememberList[speed]) {
|
||||||
if (speed === 1) {
|
options.individualRememberList[speed] = []
|
||||||
return '倍速'
|
|
||||||
}
|
|
||||||
return Math.trunc(speed) === speed ? `${speed}.0x` : `${speed}x`
|
|
||||||
}
|
}
|
||||||
|
// 追加记忆值
|
||||||
|
options.individualRememberList[speed].push(aid)
|
||||||
|
}
|
||||||
|
|
||||||
static getRememberSpeed(aid?: string) {
|
const getSpeedMenuItem = (speed: number) => menuListElement.querySelector(`${extendedAgent.custom.speedMenuItem.selector}[data-value="${speed}"]`) as (HTMLElement | null)
|
||||||
for (const [level, aids] of Object.entries(options.individualRememberList)) {
|
|
||||||
if (aids.some(aid_ => aid_ === getAid(aid))) {
|
/**
|
||||||
return parseFloat(level)
|
* 设置视频倍速值,将返回当前的倍速
|
||||||
}
|
*
|
||||||
}
|
* @param speed 欲设置的倍速值
|
||||||
return null
|
*/
|
||||||
|
const videoSpeed = (speed?: number) => {
|
||||||
|
if (speed) {
|
||||||
|
getSpeedMenuItem(speed)?.click()
|
||||||
|
return speed
|
||||||
}
|
}
|
||||||
|
return videoElement.playbackRate
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
const setVideoSpeed = videoSpeed
|
||||||
* 忘记对指定 aid 记忆的倍速,返回值表示指定的 aid 之前是否被记忆
|
|
||||||
*
|
|
||||||
* @param aid 要忘记的 aid,若不指定则从页面中自动获取
|
|
||||||
*/
|
|
||||||
static forgetSpeed(aid?: string) {
|
|
||||||
aid = getAid(aid)
|
|
||||||
|
|
||||||
let aidOldIndex = -1
|
/**
|
||||||
for (const aids of Object.values(options.individualRememberList)) {
|
* 重置视频倍速
|
||||||
aidOldIndex = aids.indexOf(aid)
|
*
|
||||||
if (aidOldIndex !== -1) {
|
* @param withForget 是否为附带清除视频记忆的重置倍速操作
|
||||||
aids.splice(aidOldIndex, 1)
|
*/
|
||||||
break
|
const resetVideoSpeed = (withForget = false) => {
|
||||||
}
|
if (withForget) {
|
||||||
}
|
const fallbackVideoSpeed = getFallbackVideoSpeed()
|
||||||
|
// 如果 fallbackVideoSpeed 是 undefined,那么意味着没有开启记忆倍速功能
|
||||||
return aidOldIndex !== -1
|
// 考虑到与清除视频级别的倍速记忆功能的相关性,这里会忽略设定
|
||||||
}
|
// 简单地说,如果没有开启记忆倍速的功能,就无法清除视频级别的倍速记忆
|
||||||
|
if (!fallbackVideoSpeed) {
|
||||||
/**
|
|
||||||
* 为指定 aid 记忆指定倍速
|
|
||||||
*
|
|
||||||
* @param speed 要记忆的倍速
|
|
||||||
* @param force 对于之前没有被记忆的 aid,**如果不将此参数设置为 `true`,调用完成也不会将相应的倍速记忆到设置中的**
|
|
||||||
* @param aid 要记忆的 aid,若不指定则从页面中自动获取
|
|
||||||
*/
|
|
||||||
static rememberSpeed(speed: number, force = false, aid?: string) {
|
|
||||||
aid = getAid(aid)
|
|
||||||
const remembered = VideoSpeedController.forgetSpeed(aid)
|
|
||||||
// 对于没有被记忆的 aid,并且 force 参数为假就直接返回
|
|
||||||
if (!remembered && !force) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 为新的速度值初始化相应的 aid 数组
|
forgetSpeed()
|
||||||
if (!options.individualRememberList[speed]) {
|
setVideoSpeed(fallbackVideoSpeed)
|
||||||
options.individualRememberList[speed] = []
|
} else {
|
||||||
}
|
setVideoSpeed(1)
|
||||||
// 追加记忆值
|
|
||||||
options.individualRememberList[speed].push(aid)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static async getInstance(previousSpeed?: number, nativeSpeed?: number) {
|
/**
|
||||||
const containerElement = await select(`.${VideoSpeedController.classNameMap.speedContainer}`) as HTMLElement
|
* 切换当前倍速
|
||||||
const videoElement = await playerAgent.query.video.element() as HTMLVideoElement
|
*
|
||||||
|
* 根据`mode`参数的不同有着不同的行为:
|
||||||
|
*
|
||||||
|
* - `mode === "smart"`(默认):当前倍速等于 1.0x 时,切换到上次不同的视频倍速,否则重置倍速为 1.0x
|
||||||
|
* - `mode === "classic"`:无论当前倍速如何,均切换到上次不同的视频倍速
|
||||||
|
*
|
||||||
|
* 重置倍速的行为可由 `reset()` 方法同款参数 `forget` 来控制
|
||||||
|
*
|
||||||
|
* @param forget 指示是否为清除视频记忆的重置倍速操作
|
||||||
|
*/
|
||||||
|
const toggleVideoSpeed = (mode: 'smart' | 'classic' = 'smart', forget = false) => {
|
||||||
|
switch (mode) {
|
||||||
|
case 'smart':
|
||||||
|
videoSpeed() === 1 ? videoSpeed(sharedPreviousSpeed) : resetVideoSpeed(forget)
|
||||||
|
break
|
||||||
|
case 'classic':
|
||||||
|
setVideoSpeed(sharedPreviousSpeed)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!containerElement) {
|
const forceUpdate = (value: number) => {
|
||||||
|
menuListElement.querySelector(`${extendedAgent.custom.speedMenuItem.selector}[data-value="${videoSpeed()}"]`)?.classList.remove(activeClassName)
|
||||||
|
videoElement.playbackRate = value
|
||||||
|
menuListElement.querySelector(`${extendedAgent.custom.speedMenuItem.selector}[data-value="${value}"]`)?.classList.add(activeClassName)
|
||||||
|
containerElement.classList.remove(showClassName)
|
||||||
|
nameBtn.innerText = formatSpeedText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setExtendedVideoSpeed = (speed: number) => {
|
||||||
|
if (nativeSupportedRates.includes(speed)) {
|
||||||
|
getSpeedMenuItem(speed)?.click()
|
||||||
|
} else {
|
||||||
|
forceUpdate(speed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendList = async () => {
|
||||||
|
const { getExtraSpeedMenuItemElements } = await import('./extend')
|
||||||
|
menuListElement.prepend(...await getExtraSpeedMenuItemElements())
|
||||||
|
// 为所有原生倍速菜单项设置 Order
|
||||||
|
menuListElement.querySelectorAll(`${extendedAgent.custom.speedMenuItem.selector}[data-value]:not(.extended)`).forEach(
|
||||||
|
(it: HTMLLIElement) => { it.style.order = calcOrder(parseFloat(it.getAttribute('data-value') ?? '1')) },
|
||||||
|
)
|
||||||
|
// 如果开启了扩展倍速,存在一种场景使倍速设置会失效:
|
||||||
|
// 1. 用户从原生支持的倍速切换到扩展倍速
|
||||||
|
// 2. 用户从扩展倍速切换到之前选中的原生倍速
|
||||||
|
// 这是因为播放器内部实现维护了一个速度值,但是在切换到扩展倍速时没法更新,因此切换回来的时候被判定没有发生变化
|
||||||
|
// 为了解决这个问题,需要通过 forceUpdate 方法替官方更新元素,并为视频设置正确的倍速,最后关闭菜单
|
||||||
|
addListener(menuListElement, ['click', ev => {
|
||||||
|
const option = (ev.target as HTMLElement)
|
||||||
|
const value = parseFloat(option.dataset.value as string)
|
||||||
|
if ((ev.target as HTMLElement).classList.contains('extended')) {
|
||||||
|
setExtendedVideoSpeed(value)
|
||||||
|
}
|
||||||
|
// 从扩展倍速切换到之前选中的原生倍速:强制更新!
|
||||||
|
if (getExtendedSupportedRates().includes(videoSpeed()) && nativeSpeed === value) {
|
||||||
|
forceUpdate(value)
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
const extendListIfAllow = async () => {
|
||||||
|
// 1. 根据 options 判断是否允许扩展倍速列表
|
||||||
|
// 2. 如果已经打过标志 classname 的话,就不能再塞更多元素了
|
||||||
|
if (!options.extend || containerElement.classList.contains('extended')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await extendList()
|
||||||
|
containerElement.classList.add('extended')
|
||||||
|
}
|
||||||
|
|
||||||
|
const addSpeedChangeEventListener = (cb: (previousSpeed: number, currentSpeed: number) => void) => {
|
||||||
|
addListener(menuListElement, ['click', ev => {
|
||||||
|
cb(sharedSpeed, parseFloat((ev.target as HTMLLIElement).dataset.value ?? '1'))
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatchChangedEvent = (previousSpeed: number, currentSpeed: number) => {
|
||||||
|
const isNativeSpeed = nativeSupportedRates.includes(currentSpeed)
|
||||||
|
containerElement.dispatchEvent(new CustomEvent('changed', { detail: { speed: currentSpeed, isNativeSpeed, previousSpeed } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createController = _.once(() => {
|
||||||
|
videoChange(async () => {
|
||||||
|
// 移除所有监听器
|
||||||
|
removeListeners()
|
||||||
|
// 重新获取页面元素
|
||||||
|
const tmpContainerElement = await extendedAgent.custom.speedContainer()
|
||||||
|
const tmpVideoElement = (await playerAgent.query.video.element()) as (HTMLVideoElement | null)
|
||||||
|
if (!tmpContainerElement) {
|
||||||
throw new Error('speed container element not found!')
|
throw new Error('speed container element not found!')
|
||||||
}
|
}
|
||||||
if (!videoElement) {
|
if (!tmpVideoElement) {
|
||||||
throw new Error('video element not found!')
|
throw new Error('video element not found!')
|
||||||
}
|
}
|
||||||
|
containerElement = tmpContainerElement
|
||||||
return new VideoSpeedController(containerElement, videoElement, previousSpeed, nativeSpeed)
|
videoElement = tmpVideoElement
|
||||||
}
|
nameBtn = containerElement.querySelector(extendedAgent.custom.speedNameBtn.selector) as HTMLButtonElement
|
||||||
|
menuListElement = containerElement.querySelector(extendedAgent.custom.speedMenuList.selector) as HTMLElement
|
||||||
static init = lodash.once(() => {
|
// 试图插入扩展的倍速菜单项
|
||||||
// 分 P 切换时共享同一个倍速,这里指定初始倍速可以是 undefined,不需要是 1
|
await extendListIfAllow()
|
||||||
let sharedSpeed: number
|
// 为每一个倍速菜单项附加 dataset
|
||||||
// 分 P 切换时共享同一个原生倍速值,初始值设置为 1
|
menuListElement.querySelectorAll(extendedAgent.custom.speedMenuItem.selector).forEach(it => {
|
||||||
let sharedNativeSpeed = 1
|
|
||||||
// 持有菜单容器元素的引用,videoChange 时更换(以前缓存的 VideoController 就没有意义了)
|
|
||||||
let containerElement: HTMLElement
|
|
||||||
|
|
||||||
videoChange(async () => {
|
|
||||||
const { getExtraSpeedMenuItemElements } = await import('./extend')
|
|
||||||
// 有必要传递之前的 nativeSpeedVal,跨分 P 时原生倍速将保持一样
|
|
||||||
const controller = await VideoSpeedController.getInstance(sharedSpeed, sharedNativeSpeed)
|
|
||||||
containerElement = controller.containerElement
|
|
||||||
if (containerElement.classList.contains('extended')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (options.extend) {
|
|
||||||
controller.menuListElement.prepend(...await getExtraSpeedMenuItemElements())
|
|
||||||
// 为所有原生倍速菜单项设置 Order
|
|
||||||
controller.menuListElement.querySelectorAll(`.${VideoSpeedController.classNameMap.speedMenuItem}[data-value]:not(.extended)`).forEach(
|
|
||||||
(it: HTMLLIElement) => { it.style.order = calcOrder(parseFloat(it.getAttribute('data-value'))) },
|
|
||||||
)
|
|
||||||
// 如果开启了扩展倍速,存在一种场景使倍速设置会失效:
|
|
||||||
// 1. 用户从原生支持的倍速切换到扩展倍速
|
|
||||||
// 2. 用户从扩展倍速切换到之前选中的原生倍速
|
|
||||||
// 这是因为播放器内部实现维护了一个速度值,但是在切换到扩展倍速时没法更新,因此切换回来的时候被判定没有发生变化
|
|
||||||
// 为了解决这个问题,需要通过 forceUpdate 方法替官方更新元素,并为视频设置正确的倍速,并关闭菜单
|
|
||||||
controller.menuListElement.addEventListener('click', ev => {
|
|
||||||
const option = (ev.target as HTMLElement)
|
|
||||||
const value = parseFloat(option.dataset.value as string)
|
|
||||||
if ((ev.target as HTMLElement).classList.contains('extended')) {
|
|
||||||
controller.setExtendedVideoSpeed(value)
|
|
||||||
}
|
|
||||||
// 从扩展倍速切换到之前选中的原生倍速
|
|
||||||
if (
|
|
||||||
VideoSpeedController.extendedSupportedRates.includes(controller.playbackRate)
|
|
||||||
&& controller.nativeSpeedVal === value
|
|
||||||
) {
|
|
||||||
controller.forceUpdate(value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
controller.observe()
|
|
||||||
containerElement.addEventListener('changed', ({ detail: { speed, isNativeSpeed } }: CustomEvent) => {
|
|
||||||
sharedSpeed = speed
|
|
||||||
if (isNativeSpeed) {
|
|
||||||
sharedNativeSpeed = speed
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 首次加载可能会遇到意外情况,导致内部强制更新失效,因此延时 100 ms 再触发速度设置
|
|
||||||
setTimeout(() => {
|
|
||||||
controller.setVideoSpeed(
|
|
||||||
(
|
|
||||||
rememberVideoSpeed.enabled
|
|
||||||
&& options.individualRemember
|
|
||||||
&& VideoSpeedController.getRememberSpeed()
|
|
||||||
)
|
|
||||||
|| VideoSpeedController.fallbackVideoSpeed
|
|
||||||
|| sharedSpeed,
|
|
||||||
)
|
|
||||||
}, 100)
|
|
||||||
containerElement.classList.add('extended')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
public readonly menuListElement: HTMLElement
|
|
||||||
private nameBtn: HTMLButtonElement
|
|
||||||
// 这个值模拟原生内部记录的速度倍速,它不应该被赋值成扩展倍速的值
|
|
||||||
private nativeSpeedVal: number
|
|
||||||
// 这个值用于表示上一次(上一P)的倍速值,如果是首次播放第一P,则为 undefined
|
|
||||||
private previousSpeedVal?: number
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly containerElement: HTMLElement,
|
|
||||||
private readonly videoElement: HTMLVideoElement,
|
|
||||||
previousSpeed?: number,
|
|
||||||
nativeSpeed?: number,
|
|
||||||
) {
|
|
||||||
const controller = VideoSpeedController.instanceMap.get(containerElement)
|
|
||||||
if (controller
|
|
||||||
&& (!previousSpeed || controller.previousSpeedVal === previousSpeed)
|
|
||||||
&& (!nativeSpeed || controller.nativeSpeedVal === nativeSpeed)
|
|
||||||
) {
|
|
||||||
return controller
|
|
||||||
}
|
|
||||||
|
|
||||||
this.previousSpeedVal = previousSpeed
|
|
||||||
this.nativeSpeedVal = nativeSpeed ?? (
|
|
||||||
previousSpeed && VideoSpeedController.nativeSupportedRates.includes(previousSpeed)
|
|
||||||
? previousSpeed
|
|
||||||
: 1
|
|
||||||
)
|
|
||||||
this.nameBtn = this.containerElement.querySelector(`.${VideoSpeedController.classNameMap.speedNameBtn}`) as HTMLButtonElement
|
|
||||||
this.menuListElement = this.containerElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuList}`) as HTMLElement
|
|
||||||
this.menuListElement.querySelectorAll(`.${VideoSpeedController.classNameMap.speedMenuItem}`).forEach(it => {
|
|
||||||
if (!it.hasAttribute('data-value')) {
|
if (!it.hasAttribute('data-value')) {
|
||||||
const speed = parseFloat(it.textContent).toString()
|
const speed = parseFloat(it.textContent).toString()
|
||||||
it.setAttribute('data-value', speed)
|
it.setAttribute('data-value', speed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// 还原共享值
|
||||||
VideoSpeedController.instanceMap.set(containerElement, this)
|
nativeSpeed = sharedNativeSpeed
|
||||||
}
|
// 添加视频倍数变化监听
|
||||||
|
addSpeedChangeEventListener(dispatchChangedEvent)
|
||||||
get playbackRate() {
|
// 视频倍数变化监听处理
|
||||||
return this.videoElement.playbackRate
|
addListener(containerElement, ['changed', ({ detail: { speed, isNativeSpeed, previousSpeed } }: CustomEvent) => {
|
||||||
}
|
// 记录(共享)倍速值
|
||||||
|
sharedSpeed = speed
|
||||||
getSpeedMenuItem(speed?: number) {
|
if (isNativeSpeed) {
|
||||||
if (speed) {
|
sharedNativeSpeed = speed
|
||||||
return this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}[data-value="${speed}"]`) as HTMLElement
|
nativeSpeed = speed
|
||||||
}
|
}
|
||||||
return this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}.${VideoSpeedController.classNameMap.active}`) as HTMLElement
|
// 原生支持倍速的应用后,需要清除扩展倍速选项上的样式
|
||||||
}
|
if (options.extend && nativeSupportedRates.includes(speed)) {
|
||||||
|
menuListElement.querySelector(`${extendedAgent.custom.speedMenuItem.selector}.extended${extendedAgent.custom.active.selector}`)?.classList.remove(activeClassName)
|
||||||
observe() {
|
}
|
||||||
allMutationsOn(this.menuListElement, mutations => {
|
// 记忆
|
||||||
let [previousSpeed, currentSpeed]: [number?, number?] = [undefined, undefined]
|
// - `enabled` 表示是否启用记忆
|
||||||
// 遍历所有的 mutations,获取上一个倍速值和当前倍速值(真正意义上的)
|
// - `options.individualRemember` 表示是否启用细化到视频级别的记忆
|
||||||
mutations.forEach(mutation => {
|
if (enabled) {
|
||||||
const selectedSpeedOption = mutation.target as HTMLLIElement
|
if (options.individualRemember) {
|
||||||
|
rememberSpeed(
|
||||||
if (!selectedSpeedOption.classList.contains(VideoSpeedController.classNameMap.active)) {
|
speed,
|
||||||
previousSpeed = parseFloat(selectedSpeedOption.dataset.value ?? '1')
|
speed !== getFallbackVideoSpeed(),
|
||||||
return
|
)
|
||||||
|
} else {
|
||||||
|
options.speed = speed.toString()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
currentSpeed = parseFloat(selectedSpeedOption.dataset.value ?? '1')
|
// 刷新 sharedPreviousSpeed
|
||||||
|
|
||||||
let isNativeSpeed = false
|
|
||||||
if (VideoSpeedController.nativeSupportedRates.includes(currentSpeed)) {
|
|
||||||
this.nativeSpeedVal = currentSpeed
|
|
||||||
isNativeSpeed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
this.containerElement.dispatchEvent(new CustomEvent('changed', { detail: { speed: currentSpeed, isNativeSpeed, previousSpeed: this.previousSpeedVal } }))
|
|
||||||
// 原生支持倍速的应用后,有必要清除扩展倍速选项上的样式
|
|
||||||
if (
|
|
||||||
options.extend
|
|
||||||
&& VideoSpeedController.nativeSupportedRates.includes(currentSpeed)
|
|
||||||
) {
|
|
||||||
this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}.extended.${VideoSpeedController.classNameMap.active}`)?.classList.remove(VideoSpeedController.classNameMap.active)
|
|
||||||
}
|
|
||||||
// 记忆
|
|
||||||
// - `rememberVideoSpeed.enabled` 表示是否启用记忆
|
|
||||||
// - `options.individualRemember` 表示是否启用细化到视频级别的记忆
|
|
||||||
if (rememberVideoSpeed.enabled) {
|
|
||||||
if (options.individualRemember) {
|
|
||||||
VideoSpeedController.rememberSpeed(
|
|
||||||
currentSpeed,
|
|
||||||
currentSpeed !== VideoSpeedController.fallbackVideoSpeed,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
options.speed = currentSpeed.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 刷新 this._previousSpeedVal
|
|
||||||
// - 用户可以通过倍速菜单或者倍速快捷键造成类似 1.5x 2.0x 2.0x... 这样的倍速设定序列
|
// - 用户可以通过倍速菜单或者倍速快捷键造成类似 1.5x 2.0x 2.0x... 这样的倍速设定序列
|
||||||
// 我们不希望在第二个 2.0x 的时候刷新 this._previousSpeedVal,这样会比较死板
|
// 我们不希望在第二个 2.0x 的时候刷新 this._previousSpeedVal,这样会比较死板
|
||||||
// 判定依据在于 previousSpeed !== currentSpeed
|
// 判定依据在于 previousSpeed !== speed
|
||||||
if (previousSpeed && previousSpeed !== currentSpeed) {
|
if (previousSpeed && previousSpeed !== speed) {
|
||||||
this.previousSpeedVal = previousSpeed
|
sharedPreviousSpeed = previousSpeed
|
||||||
}
|
}
|
||||||
})
|
}])
|
||||||
|
// 恢复记忆的倍速值
|
||||||
|
// - 首次加载可能会遇到意外情况,导致内部强制更新失效,因此延时 100 ms 再触发速度设置
|
||||||
|
setTimeout(() => {
|
||||||
|
setVideoSpeed(
|
||||||
|
(
|
||||||
|
enabled
|
||||||
|
&& options.individualRemember
|
||||||
|
&& getRememberSpeed()
|
||||||
|
)
|
||||||
|
|| getFallbackVideoSpeed()
|
||||||
|
|| sharedSpeed,
|
||||||
|
)
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
getSupportedRates, getExtendedSupportedRates, setVideoSpeed, videoSpeed, getRememberSpeed, rememberSpeed, forgetSpeed, resetVideoSpeed, toggleVideoSpeed,
|
||||||
}
|
}
|
||||||
|
})
|
||||||
/**
|
|
||||||
* 切换当前倍速
|
|
||||||
*
|
|
||||||
* 根据`mode`参数的不同有着不同的行为:
|
|
||||||
*
|
|
||||||
* - `mode === "smart"`(默认):当前倍速等于 1.0x 时,切换到上次不同的视频倍速,否则重置倍速为 1.0x
|
|
||||||
* - `mode === "classic"`:无论当前倍速如何,均切换到上次不同的视频倍速
|
|
||||||
*
|
|
||||||
* 重置倍速的行为可由 `reset()` 方法同款参数 `forget` 来控制
|
|
||||||
*
|
|
||||||
* @param forget 指示是否为清除视频记忆的重置倍速操作
|
|
||||||
*/
|
|
||||||
toggleVideoSpeed(mode: 'smart' | 'classic' = 'smart', forget = false) {
|
|
||||||
switch (mode) {
|
|
||||||
case 'smart':
|
|
||||||
this.playbackRate === 1 ? this.setVideoSpeed(this.previousSpeedVal) : this.reset(forget)
|
|
||||||
break
|
|
||||||
case 'classic':
|
|
||||||
this.setVideoSpeed(this.previousSpeedVal)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置视频倍速
|
|
||||||
*
|
|
||||||
* @param forget 指示是否为清除视频记忆的重置倍速操作
|
|
||||||
*/
|
|
||||||
reset(forget = false) {
|
|
||||||
if (forget) {
|
|
||||||
const { fallbackVideoSpeed } = VideoSpeedController
|
|
||||||
// 如果 fallbackVideoSpeed 是 undefined,那么意味着没有开启记忆倍速功能
|
|
||||||
// 考虑到与清除视频级别的倍速记忆功能的相关性,这里会忽略设定
|
|
||||||
// 简单地说,如果没有开启记忆倍速的功能,就无法清除视频级别的倍速记忆
|
|
||||||
if (!fallbackVideoSpeed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
VideoSpeedController.forgetSpeed()
|
|
||||||
this.setVideoSpeed(fallbackVideoSpeed)
|
|
||||||
} else {
|
|
||||||
this.setVideoSpeed(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setVideoSpeed(speed?: number) {
|
|
||||||
speed && this.getSpeedMenuItem(speed).click()
|
|
||||||
}
|
|
||||||
|
|
||||||
private setExtendedVideoSpeed(speed: number) {
|
|
||||||
if (VideoSpeedController.nativeSupportedRates.includes(speed)) {
|
|
||||||
this.getSpeedMenuItem(speed).click()
|
|
||||||
} else {
|
|
||||||
this.forceUpdate(speed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private forceUpdate(value: number) {
|
|
||||||
this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}[data-value="${this.playbackRate}"]`)?.classList.remove(VideoSpeedController.classNameMap.active)
|
|
||||||
this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}[data-value="${value}"]`)?.classList.add(VideoSpeedController.classNameMap.active)
|
|
||||||
this.videoElement.playbackRate = value
|
|
||||||
this.containerElement.classList.remove(VideoSpeedController.classNameMap.show)
|
|
||||||
this.nameBtn.innerText = VideoSpeedController.formatSpeedText(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,123 +1,122 @@
|
|||||||
import { getComponentSettings } from '@/core/settings'
|
|
||||||
import { addStyle } from '@/core/style'
|
import { addStyle } from '@/core/style'
|
||||||
import { logError } from '@/core/utils/log'
|
import { logError } from '@/core/utils/log'
|
||||||
|
import {
|
||||||
|
calcOrder,
|
||||||
|
errorMessageDuration,
|
||||||
|
extendedAgent,
|
||||||
|
getExtendedSupportedRates,
|
||||||
|
formatSpeedText,
|
||||||
|
getUniqueAscendingSortList,
|
||||||
|
maxValue,
|
||||||
|
minValue,
|
||||||
|
nativeSupportedRates,
|
||||||
|
options,
|
||||||
|
stepValue,
|
||||||
|
getSupportedRates,
|
||||||
|
trimLeadingDot,
|
||||||
|
} from './utils'
|
||||||
|
|
||||||
const { options } = getComponentSettings('rememberVideoSpeed')
|
const getRecommendedValue = () => {
|
||||||
let extendList = options.extendList as number[]
|
const val = getSupportedRates().slice(-1)[0] + stepValue
|
||||||
export const getExtraSpeedMenuItemElements = async () => {
|
return val > maxValue ? null : val
|
||||||
const {
|
}
|
||||||
VideoSpeedController,
|
|
||||||
calcOrder,
|
|
||||||
minValue,
|
|
||||||
maxValue,
|
|
||||||
stepValue,
|
|
||||||
errorMessageDuration,
|
|
||||||
} = await import('./controller')
|
|
||||||
|
|
||||||
const getRecommendedValue = () => {
|
const speedMenuItemClassName = trimLeadingDot(extendedAgent.custom.speedMenuItem.selector)
|
||||||
const val = VideoSpeedController.supportedRates.slice(-1)[0] + stepValue
|
|
||||||
return val > maxValue ? null : val
|
|
||||||
}
|
|
||||||
|
|
||||||
const createExtendedSpeedMenuItemElement = (rate: number) => {
|
const createExtendedSpeedMenuItemElement = (rate: number) => {
|
||||||
const li = document.createElement('li')
|
const li = document.createElement('li')
|
||||||
li.innerText = VideoSpeedController.formatSpeedText(rate)
|
li.innerText = formatSpeedText(rate)
|
||||||
li.classList.add(VideoSpeedController.classNameMap.speedMenuItem, 'extended')
|
li.classList.add(speedMenuItemClassName, 'extended')
|
||||||
li.dataset.value = rate.toString()
|
li.dataset.value = rate.toString()
|
||||||
li.style.order = calcOrder(rate)
|
li.style.order = calcOrder(rate)
|
||||||
|
const i = document.createElement('i')
|
||||||
|
i.classList.add('mdi', 'mdi-close-circle')
|
||||||
|
i.addEventListener('click', () => {
|
||||||
|
lodash.pull(options.extendList, rate)
|
||||||
|
li.remove()
|
||||||
|
})
|
||||||
|
li.append(i)
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
const i = document.createElement('i')
|
const updateInput = (elem: HTMLInputElement) => {
|
||||||
i.classList.add('mdi', 'mdi-close-circle')
|
const recommendedValue = getRecommendedValue()
|
||||||
i.addEventListener('click', () => {
|
elem.setAttribute('min', recommendedValue ? (elem.value = recommendedValue.toString()) : (elem.value = '', minValue.toString()))
|
||||||
lodash.pull(extendList, rate)
|
}
|
||||||
li.remove()
|
|
||||||
})
|
|
||||||
|
|
||||||
li.append(i)
|
const createAddEntryElement = () => {
|
||||||
|
const li = document.createElement('li')
|
||||||
return li
|
li.classList.add(speedMenuItemClassName)
|
||||||
}
|
const iconElement = document.createElement('i')
|
||||||
|
iconElement.classList.add('mdi', 'mdi-playlist-plus')
|
||||||
const createAddEntryElement = () => {
|
const input = document.createElement('input')
|
||||||
const updateInput = (elem: HTMLInputElement) => {
|
input.classList.add('add-speed-entry')
|
||||||
const value = getRecommendedValue()
|
input.setAttribute('type', 'number')
|
||||||
elem.setAttribute('min', value ? (elem.value = value.toString()) : (elem.value = '', minValue.toString()))
|
input.setAttribute('max', maxValue.toString())
|
||||||
}
|
input.setAttribute('step', stepValue.toString())
|
||||||
|
input.setAttribute('title', '增加新的倍速值')
|
||||||
const li = document.createElement('li')
|
updateInput(input)
|
||||||
li.classList.add(VideoSpeedController.classNameMap.speedMenuItem)
|
input.addEventListener('keydown', ev => {
|
||||||
|
if (ev.key === 'Enter') {
|
||||||
const iconElement = document.createElement('i')
|
const value = parseFloat(input.value)
|
||||||
iconElement.classList.add('mdi', 'mdi-playlist-plus')
|
if (!isFinite(value)) {
|
||||||
|
logError('无效的倍速值', errorMessageDuration)
|
||||||
const input = document.createElement('input')
|
return false
|
||||||
input.classList.add('add-speed-entry')
|
|
||||||
input.setAttribute('type', 'number')
|
|
||||||
input.setAttribute('max', maxValue.toString())
|
|
||||||
input.setAttribute('step', stepValue.toString())
|
|
||||||
input.setAttribute('title', '增加新的倍速值')
|
|
||||||
updateInput(input)
|
|
||||||
input.addEventListener('keydown', ev => {
|
|
||||||
if (ev.key === 'Enter') {
|
|
||||||
const value = parseFloat(input.value)
|
|
||||||
if (!isFinite(value)) {
|
|
||||||
logError('无效的倍速值', errorMessageDuration)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (value < minValue) {
|
|
||||||
logError('倍速值太小了', errorMessageDuration)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (value > maxValue) {
|
|
||||||
logError('倍速值太大了', errorMessageDuration)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (VideoSpeedController.supportedRates.includes(value)) {
|
|
||||||
logError('不能重复添加已有的倍速值', errorMessageDuration)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
extendList.push(value)
|
|
||||||
options.extendList = VideoSpeedController.extendedSupportedRates
|
|
||||||
extendList = options.extendList
|
|
||||||
|
|
||||||
let afterElement = li.nextElementSibling as HTMLLIElement
|
|
||||||
while (
|
|
||||||
!afterElement.dataset.value
|
|
||||||
|| (parseFloat(afterElement.dataset.value)
|
|
||||||
> VideoSpeedController.nativeSupportedRates.slice(-1)[0]
|
|
||||||
&& value < parseFloat(afterElement.dataset.value))
|
|
||||||
) {
|
|
||||||
afterElement = afterElement.nextElementSibling as HTMLLIElement
|
|
||||||
}
|
|
||||||
afterElement.before(createExtendedSpeedMenuItemElement(value))
|
|
||||||
updateInput(input)
|
|
||||||
}
|
}
|
||||||
return true
|
if (value < minValue) {
|
||||||
})
|
logError('倍速值太小了', errorMessageDuration)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (value > maxValue) {
|
||||||
|
logError('倍速值太大了', errorMessageDuration)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (getSupportedRates().includes(value)) {
|
||||||
|
logError('不能重复添加已有的倍速值', errorMessageDuration)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
options.extendList.push(value)
|
||||||
|
options.extendList = getUniqueAscendingSortList(options.extendList)
|
||||||
|
|
||||||
li.prepend(iconElement, input)
|
let afterElement = li.nextElementSibling as HTMLLIElement
|
||||||
|
while (
|
||||||
input.style.display = 'none'
|
!afterElement.dataset.value
|
||||||
li.addEventListener('mouseenter', () => {
|
|| (parseFloat(afterElement.dataset.value)
|
||||||
|
> nativeSupportedRates.slice(-1)[0]
|
||||||
|
&& value < parseFloat(afterElement.dataset.value))
|
||||||
|
) {
|
||||||
|
afterElement = afterElement.nextElementSibling as HTMLLIElement
|
||||||
|
}
|
||||||
|
afterElement.before(createExtendedSpeedMenuItemElement(value))
|
||||||
updateInput(input)
|
updateInput(input)
|
||||||
input.style.display = 'inline'
|
}
|
||||||
iconElement.style.display = 'none'
|
return true
|
||||||
input.focus()
|
})
|
||||||
})
|
|
||||||
li.addEventListener('mouseleave', () => {
|
|
||||||
iconElement.style.display = 'inline'
|
|
||||||
input.style.display = 'none'
|
|
||||||
})
|
|
||||||
|
|
||||||
return li
|
li.prepend(iconElement, input)
|
||||||
}
|
|
||||||
|
|
||||||
|
input.style.display = 'none'
|
||||||
|
li.addEventListener('mouseenter', () => {
|
||||||
|
updateInput(input)
|
||||||
|
input.style.display = 'inline'
|
||||||
|
iconElement.style.display = 'none'
|
||||||
|
input.focus()
|
||||||
|
})
|
||||||
|
li.addEventListener('mouseleave', () => {
|
||||||
|
iconElement.style.display = 'inline'
|
||||||
|
input.style.display = 'none'
|
||||||
|
})
|
||||||
|
|
||||||
|
return li
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getExtraSpeedMenuItemElements = async () => {
|
||||||
// 应用样式
|
// 应用样式
|
||||||
addStyle(`
|
addStyle(`
|
||||||
.${VideoSpeedController.classNameMap.speedContainer} .${VideoSpeedController.classNameMap.speedMenuItem}:first-child .mdi-playlist-plus {
|
${extendedAgent.custom.speedContainer.selector} ${extendedAgent.custom.speedMenuItem.selector}:first-child .mdi-playlist-plus {
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
.${VideoSpeedController.classNameMap.speedContainer} .${VideoSpeedController.classNameMap.speedMenuItem}:first-child input {
|
${extendedAgent.custom.speedContainer.selector} ${extendedAgent.custom.speedMenuItem.selector}:first-child input {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
@ -127,17 +126,17 @@ export const getExtraSpeedMenuItemElements = async () => {
|
|||||||
border: none;
|
border: none;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.${VideoSpeedController.classNameMap.speedMenuItem} .mdi-close-circle {
|
${extendedAgent.custom.speedMenuItem.selector} .mdi-close-circle {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
}
|
}
|
||||||
.${VideoSpeedController.classNameMap.speedMenuItem}:not(.${VideoSpeedController.classNameMap.active}):hover .mdi-close-circle {
|
${extendedAgent.custom.speedMenuItem.selector}:not(${extendedAgent.custom.active.selector}):hover .mdi-close-circle {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
.${VideoSpeedController.classNameMap.speedMenuItem} .mdi-close-circle:hover {
|
${extendedAgent.custom.speedMenuItem.selector} .mdi-close-circle:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: all .3s;
|
transition: all .3s;
|
||||||
}
|
}
|
||||||
@ -152,7 +151,7 @@ export const getExtraSpeedMenuItemElements = async () => {
|
|||||||
.add-speed-entry[type=number] {
|
.add-speed-entry[type=number] {
|
||||||
-moz-appearance:textfield;
|
-moz-appearance:textfield;
|
||||||
}
|
}
|
||||||
.${VideoSpeedController.classNameMap.speedMenuList} {
|
${extendedAgent.custom.speedMenuList.selector} {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -160,7 +159,7 @@ export const getExtraSpeedMenuItemElements = async () => {
|
|||||||
}
|
}
|
||||||
`, 'extend-video-speed-style')
|
`, 'extend-video-speed-style')
|
||||||
|
|
||||||
const elements = VideoSpeedController.extendedSupportedRates
|
const elements = getExtendedSupportedRates()
|
||||||
.map(rate => createExtendedSpeedMenuItemElement(rate))
|
.map(rate => createExtendedSpeedMenuItemElement(rate))
|
||||||
.reverse()
|
.reverse()
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
|
import { importComponent } from '@/components/component'
|
||||||
import { ComponentMetadata } from '@/components/types'
|
import { ComponentMetadata } from '@/components/types'
|
||||||
import { playerUrls } from '@/core/utils/urls'
|
import { playerUrls } from '@/core/utils/urls'
|
||||||
import { KeyBindingAction, KeyBindingActionContext } from 'registry/lib/components/utils/keymap/bindings'
|
import { KeyBindingAction, KeyBindingActionContext } from 'registry/lib/components/utils/keymap/bindings'
|
||||||
|
import type { createController } from './controller'
|
||||||
|
|
||||||
|
const componentName = 'rememberVideoSpeed'
|
||||||
|
|
||||||
|
type Controller = ReturnType<typeof createController>
|
||||||
|
|
||||||
export const component: ComponentMetadata = {
|
export const component: ComponentMetadata = {
|
||||||
name: 'rememberVideoSpeed',
|
name: componentName,
|
||||||
displayName: '倍速记忆',
|
displayName: '倍速记忆',
|
||||||
author: {
|
author: {
|
||||||
name: 'JLoeve',
|
name: 'JLoeve',
|
||||||
@ -14,33 +20,31 @@ export const component: ComponentMetadata = {
|
|||||||
},
|
},
|
||||||
tags: [componentsTags.video],
|
tags: [componentsTags.video],
|
||||||
urlInclude: playerUrls,
|
urlInclude: playerUrls,
|
||||||
entry: async () => {
|
entry: async () => (await import('./controller')).createController(),
|
||||||
const { VideoSpeedController } = await import('./controller')
|
|
||||||
VideoSpeedController.init()
|
|
||||||
return VideoSpeedController
|
|
||||||
},
|
|
||||||
plugin: {
|
plugin: {
|
||||||
displayName: '倍速记忆 - 快捷键支持',
|
displayName: '倍速记忆 - 快捷键支持',
|
||||||
setup: async ({ addData }) => {
|
setup: async ({ addData }) => {
|
||||||
const { getComponentSettings } = await import('@/core/settings')
|
const { getComponentSettings } = await import('@/core/settings')
|
||||||
|
|
||||||
const videoSpeed = async (
|
const videoSpeed = async (
|
||||||
context: KeyBindingActionContext,
|
context: KeyBindingActionContext,
|
||||||
controllerAction: (
|
controllerAction: (
|
||||||
controller: InstanceType<typeof import('./controller')['VideoSpeedController']>, rates: number[]
|
controller: Controller, rates: number[]
|
||||||
) => void,
|
) => void,
|
||||||
) => {
|
) => {
|
||||||
const { VideoSpeedController } = await import('./controller')
|
// 不要提前导入,插件在组件加载之前进行加载,因此如果提前加载会取不到 entry 调用后返回的对象
|
||||||
const controller = await VideoSpeedController.getInstance()
|
const controller = importComponent(componentName) as Controller
|
||||||
controllerAction(controller, VideoSpeedController.supportedRates)
|
controllerAction(controller, controller.getSupportedRates())
|
||||||
context.showTip(`${controller.playbackRate}x`, 'mdi-fast-forward')
|
context.showTip(`${controller.videoSpeed()}x`, 'mdi-fast-forward')
|
||||||
}
|
}
|
||||||
|
|
||||||
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
|
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
|
||||||
actions.videoSpeedIncrease = {
|
actions.videoSpeedIncrease = {
|
||||||
displayName: '提高倍速',
|
displayName: '提高倍速',
|
||||||
run: context => {
|
run: context => {
|
||||||
videoSpeed(context, (controller, rates) => {
|
videoSpeed(context, (controller, rates) => {
|
||||||
controller.setVideoSpeed(
|
controller.setVideoSpeed(
|
||||||
rates.find(it => it > controller.playbackRate)
|
rates.find(it => it > controller.videoSpeed())
|
||||||
|| rates[rates.length - 1],
|
|| rates[rates.length - 1],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -52,7 +56,7 @@ export const component: ComponentMetadata = {
|
|||||||
run: context => {
|
run: context => {
|
||||||
videoSpeed(context, (controller, rates) => {
|
videoSpeed(context, (controller, rates) => {
|
||||||
controller.setVideoSpeed(
|
controller.setVideoSpeed(
|
||||||
[...rates].reverse().find(it => it < controller.playbackRate)
|
[...rates].reverse().find(it => it < controller.videoSpeed())
|
||||||
|| rates[0],
|
|| rates[0],
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -73,7 +77,7 @@ export const component: ComponentMetadata = {
|
|||||||
displayName: '清除当前倍速记忆',
|
displayName: '清除当前倍速记忆',
|
||||||
run: context => {
|
run: context => {
|
||||||
videoSpeed(context, controller => {
|
videoSpeed(context, controller => {
|
||||||
controller.reset(true)
|
controller.resetVideoSpeed(true)
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
|
|||||||
74
registry/lib/components/video/player/remember-speed/utils.ts
Normal file
74
registry/lib/components/video/player/remember-speed/utils.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { playerAgent } from '@/components/video/player-agent'
|
||||||
|
import { ComponentSettings, getComponentSettings } from '@/core/settings'
|
||||||
|
import { ascendingSort } from '@/core/utils/sort'
|
||||||
|
|
||||||
|
export const minValue = 0.0625
|
||||||
|
export const maxValue = 16
|
||||||
|
export const stepValue = 0.5
|
||||||
|
export const errorMessageDuration = 2000
|
||||||
|
export const nativeSupportedRates = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
|
||||||
|
|
||||||
|
export const extendedAgent = playerAgent.provideCustomQuery({
|
||||||
|
video: {
|
||||||
|
speedMenuList: '.bilibili-player-video-btn-speed-menu',
|
||||||
|
speedMenuItem: '.bilibili-player-video-btn-speed-menu-list',
|
||||||
|
speedNameBtn: '.bilibili-player-video-btn-speed-name',
|
||||||
|
speedContainer: '.bilibili-player-video-btn-speed',
|
||||||
|
active: '.bilibili-player-active',
|
||||||
|
show: '.bilibili-player-speed-show',
|
||||||
|
},
|
||||||
|
bangumi: {
|
||||||
|
speedMenuList: '.squirtle-speed-select-list',
|
||||||
|
speedMenuItem: '.squirtle-select-item',
|
||||||
|
speedNameBtn: '.squirtle-speed-select-result',
|
||||||
|
speedContainer: '.squirtle-speed-wrap',
|
||||||
|
active: '.active',
|
||||||
|
// bangumi 那边没有这种 class, 随便填一个就行了
|
||||||
|
show: '.bilibili-player-speed-show',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const trimLeadingDot = (selector: string) => selector.replace(/^\./, '')
|
||||||
|
export const calcOrder = (value: number) => ((maxValue - value) * 10000).toString()
|
||||||
|
export const getAid = (aid = unsafeWindow.aid) => {
|
||||||
|
if (!aid) {
|
||||||
|
throw new Error('aid is unknown')
|
||||||
|
}
|
||||||
|
return aid
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
speed: string
|
||||||
|
extend: boolean
|
||||||
|
extendList: number[]
|
||||||
|
individualRemember: boolean
|
||||||
|
individualRememberList: Record<string, string[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { options, enabled } = getComponentSettings('rememberVideoSpeed') as ComponentSettings<Options>
|
||||||
|
|
||||||
|
export const getUniqueAscendingSortList = (values: number[]) => Array.from(new Set(values)).sort(ascendingSort())
|
||||||
|
|
||||||
|
export const getExtendedSupportedRates = () => getUniqueAscendingSortList(options.extendList)
|
||||||
|
|
||||||
|
export const getSupportedRates = () => (options.extend ? [
|
||||||
|
...nativeSupportedRates,
|
||||||
|
...getExtendedSupportedRates(),
|
||||||
|
].sort(ascendingSort()) : nativeSupportedRates)
|
||||||
|
|
||||||
|
export const formatSpeedText = (speed: number) => {
|
||||||
|
if (speed === 1) {
|
||||||
|
return '倍速'
|
||||||
|
}
|
||||||
|
return Math.trunc(speed) === speed ? `${speed}.0x` : `${speed}x`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listener<E extends Event = Event> = [string, (ev: E) => any]
|
||||||
|
const listeners: Map<HTMLElement, Listener> = new Map()
|
||||||
|
|
||||||
|
export const addListener = (element: HTMLElement, listener: Listener) => {
|
||||||
|
element.addEventListener(...listener)
|
||||||
|
listeners.set(element, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeListeners = () => listeners.forEach((listener, element) => element.removeEventListener(...listener))
|
||||||
@ -5,11 +5,11 @@ import { componentToSettings, isUserComponent } from './helpers'
|
|||||||
|
|
||||||
type Property = string | number | symbol
|
type Property = string | number | symbol
|
||||||
/** 表示一个组件的设置 */
|
/** 表示一个组件的设置 */
|
||||||
export interface ComponentSettings {
|
export interface ComponentSettings<O = { [key: string]: any }> {
|
||||||
/** 是否启用此组件 */
|
/** 是否启用此组件 */
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
/** 组件选项 */
|
/** 组件选项 */
|
||||||
options: { [key: string]: any }
|
options: O
|
||||||
}
|
}
|
||||||
/** 脚本总设置 */
|
/** 脚本总设置 */
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user