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 { videoChange } from '@/core/observer'
|
||||
import {
|
||||
addListener,
|
||||
calcOrder,
|
||||
enabled,
|
||||
extendedAgent,
|
||||
formatSpeedText,
|
||||
getAid,
|
||||
getExtendedSupportedRates,
|
||||
getSupportedRates,
|
||||
nativeSupportedRates,
|
||||
options,
|
||||
removeListeners,
|
||||
trimLeadingDot,
|
||||
} from './utils'
|
||||
|
||||
export const minValue = 0.0625
|
||||
export const maxValue = 16
|
||||
export const stepValue = 0.5
|
||||
export const errorMessageDuration = 2000
|
||||
const activeClassName = trimLeadingDot(extendedAgent.custom.active.selector)
|
||||
const showClassName = trimLeadingDot(extendedAgent.custom.show.selector)
|
||||
|
||||
export const calcOrder = (value: number) => ((maxValue - value) * 10000).toString()
|
||||
const getAid = (aid = unsafeWindow.aid) => {
|
||||
if (!aid) {
|
||||
throw new Error('aid is unknown')
|
||||
// 原生倍速,应该与官方播放器内部维护的值保持一致
|
||||
let nativeSpeed: number
|
||||
// 分 P 切换时共享前一个倍速,这里指定初始倍速可以是 undefined,不需要是 1
|
||||
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 options = rememberVideoSpeed.options as {
|
||||
speed: string
|
||||
extend: boolean
|
||||
extendList: number[]
|
||||
individualRemember: boolean
|
||||
individualRememberList: Record<string, string[]>
|
||||
const getFallbackVideoSpeed = () => {
|
||||
// 如果组件被启用才使用存储的后备值
|
||||
if (enabled) {
|
||||
return parseFloat(options.speed)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
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',
|
||||
},
|
||||
})
|
||||
const trimLeadingDot = (selector: string) => selector.replace(/^\./, '')
|
||||
/**
|
||||
* 忘记对指定 aid 记忆的倍速,返回值表示指定的 aid 之前是否被记忆
|
||||
*
|
||||
* @param aid 要忘记的 aid,若不指定则从页面中自动获取
|
||||
*/
|
||||
const forgetSpeed = (aid?: string) => {
|
||||
aid = getAid(aid)
|
||||
|
||||
export class VideoSpeedController {
|
||||
static readonly classNameMap = {
|
||||
speedMenuList: trimLeadingDot(extendedAgent.custom.speedMenuList.selector),
|
||||
speedMenuItem: trimLeadingDot(extendedAgent.custom.speedMenuItem.selector),
|
||||
speedNameBtn: trimLeadingDot(extendedAgent.custom.speedNameBtn.selector),
|
||||
speedContainer: trimLeadingDot(extendedAgent.custom.speedContainer.selector),
|
||||
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())
|
||||
let aidOldIndex = -1
|
||||
for (const aids of Object.values(options.individualRememberList)) {
|
||||
aidOldIndex = aids.indexOf(aid)
|
||||
if (aidOldIndex !== -1) {
|
||||
aids.splice(aidOldIndex, 1)
|
||||
break
|
||||
}
|
||||
return VideoSpeedController.nativeSupportedRates
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后备默认速度
|
||||
*/
|
||||
static get fallbackVideoSpeed() {
|
||||
// 向下兼容,原本 settings.defaultVideoSpeed 被设计为 string 类型,用于存储全局倍速
|
||||
if (rememberVideoSpeed.enabled) {
|
||||
return parseFloat(options.speed)
|
||||
}
|
||||
return null
|
||||
return aidOldIndex !== -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定 aid 记忆指定倍速
|
||||
*
|
||||
* @param speed 要记忆的倍速
|
||||
* @param force 对于之前没有被记忆的 aid,**如果不将此参数设置为 `true`,调用完成也不会将相应的倍速记忆到设置中的**
|
||||
* @param aid 要记忆的 aid,若不指定则从页面中自动获取
|
||||
*/
|
||||
const rememberSpeed = (speed: number, force = false, aid?: string) => {
|
||||
aid = getAid(aid)
|
||||
const remembered = forgetSpeed(aid)
|
||||
// 对于没有被记忆的 aid,并且 force 参数为假就直接返回
|
||||
if (!remembered && !force) {
|
||||
return
|
||||
}
|
||||
|
||||
static formatSpeedText(speed: number) {
|
||||
if (speed === 1) {
|
||||
return '倍速'
|
||||
}
|
||||
return Math.trunc(speed) === speed ? `${speed}.0x` : `${speed}x`
|
||||
// 为新的速度值初始化相应的 aid 数组
|
||||
if (!options.individualRememberList[speed]) {
|
||||
options.individualRememberList[speed] = []
|
||||
}
|
||||
// 追加记忆值
|
||||
options.individualRememberList[speed].push(aid)
|
||||
}
|
||||
|
||||
static getRememberSpeed(aid?: string) {
|
||||
for (const [level, aids] of Object.entries(options.individualRememberList)) {
|
||||
if (aids.some(aid_ => aid_ === getAid(aid))) {
|
||||
return parseFloat(level)
|
||||
}
|
||||
}
|
||||
return null
|
||||
const getSpeedMenuItem = (speed: number) => menuListElement.querySelector(`${extendedAgent.custom.speedMenuItem.selector}[data-value="${speed}"]`) as (HTMLElement | null)
|
||||
|
||||
/**
|
||||
* 设置视频倍速值,将返回当前的倍速
|
||||
*
|
||||
* @param speed 欲设置的倍速值
|
||||
*/
|
||||
const videoSpeed = (speed?: number) => {
|
||||
if (speed) {
|
||||
getSpeedMenuItem(speed)?.click()
|
||||
return speed
|
||||
}
|
||||
return videoElement.playbackRate
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记对指定 aid 记忆的倍速,返回值表示指定的 aid 之前是否被记忆
|
||||
*
|
||||
* @param aid 要忘记的 aid,若不指定则从页面中自动获取
|
||||
*/
|
||||
static forgetSpeed(aid?: string) {
|
||||
aid = getAid(aid)
|
||||
const setVideoSpeed = videoSpeed
|
||||
|
||||
let aidOldIndex = -1
|
||||
for (const aids of Object.values(options.individualRememberList)) {
|
||||
aidOldIndex = aids.indexOf(aid)
|
||||
if (aidOldIndex !== -1) {
|
||||
aids.splice(aidOldIndex, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return aidOldIndex !== -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定 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) {
|
||||
/**
|
||||
* 重置视频倍速
|
||||
*
|
||||
* @param withForget 是否为附带清除视频记忆的重置倍速操作
|
||||
*/
|
||||
const resetVideoSpeed = (withForget = false) => {
|
||||
if (withForget) {
|
||||
const fallbackVideoSpeed = getFallbackVideoSpeed()
|
||||
// 如果 fallbackVideoSpeed 是 undefined,那么意味着没有开启记忆倍速功能
|
||||
// 考虑到与清除视频级别的倍速记忆功能的相关性,这里会忽略设定
|
||||
// 简单地说,如果没有开启记忆倍速的功能,就无法清除视频级别的倍速记忆
|
||||
if (!fallbackVideoSpeed) {
|
||||
return
|
||||
}
|
||||
// 为新的速度值初始化相应的 aid 数组
|
||||
if (!options.individualRememberList[speed]) {
|
||||
options.individualRememberList[speed] = []
|
||||
}
|
||||
// 追加记忆值
|
||||
options.individualRememberList[speed].push(aid)
|
||||
forgetSpeed()
|
||||
setVideoSpeed(fallbackVideoSpeed)
|
||||
} else {
|
||||
setVideoSpeed(1)
|
||||
}
|
||||
}
|
||||
|
||||
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!')
|
||||
}
|
||||
if (!videoElement) {
|
||||
if (!tmpVideoElement) {
|
||||
throw new Error('video element not found!')
|
||||
}
|
||||
|
||||
return new VideoSpeedController(containerElement, videoElement, previousSpeed, nativeSpeed)
|
||||
}
|
||||
|
||||
static init = lodash.once(() => {
|
||||
// 分 P 切换时共享同一个倍速,这里指定初始倍速可以是 undefined,不需要是 1
|
||||
let sharedSpeed: number
|
||||
// 分 P 切换时共享同一个原生倍速值,初始值设置为 1
|
||||
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 => {
|
||||
containerElement = tmpContainerElement
|
||||
videoElement = tmpVideoElement
|
||||
nameBtn = containerElement.querySelector(extendedAgent.custom.speedNameBtn.selector) as HTMLButtonElement
|
||||
menuListElement = containerElement.querySelector(extendedAgent.custom.speedMenuList.selector) as HTMLElement
|
||||
// 试图插入扩展的倍速菜单项
|
||||
await extendListIfAllow()
|
||||
// 为每一个倍速菜单项附加 dataset
|
||||
menuListElement.querySelectorAll(extendedAgent.custom.speedMenuItem.selector).forEach(it => {
|
||||
if (!it.hasAttribute('data-value')) {
|
||||
const speed = parseFloat(it.textContent).toString()
|
||||
it.setAttribute('data-value', speed)
|
||||
}
|
||||
})
|
||||
|
||||
VideoSpeedController.instanceMap.set(containerElement, this)
|
||||
}
|
||||
|
||||
get playbackRate() {
|
||||
return this.videoElement.playbackRate
|
||||
}
|
||||
|
||||
getSpeedMenuItem(speed?: number) {
|
||||
if (speed) {
|
||||
return this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}[data-value="${speed}"]`) as HTMLElement
|
||||
}
|
||||
return this.menuListElement.querySelector(`.${VideoSpeedController.classNameMap.speedMenuItem}.${VideoSpeedController.classNameMap.active}`) as HTMLElement
|
||||
}
|
||||
|
||||
observe() {
|
||||
allMutationsOn(this.menuListElement, mutations => {
|
||||
let [previousSpeed, currentSpeed]: [number?, number?] = [undefined, undefined]
|
||||
// 遍历所有的 mutations,获取上一个倍速值和当前倍速值(真正意义上的)
|
||||
mutations.forEach(mutation => {
|
||||
const selectedSpeedOption = mutation.target as HTMLLIElement
|
||||
|
||||
if (!selectedSpeedOption.classList.contains(VideoSpeedController.classNameMap.active)) {
|
||||
previousSpeed = parseFloat(selectedSpeedOption.dataset.value ?? '1')
|
||||
return
|
||||
// 还原共享值
|
||||
nativeSpeed = sharedNativeSpeed
|
||||
// 添加视频倍数变化监听
|
||||
addSpeedChangeEventListener(dispatchChangedEvent)
|
||||
// 视频倍数变化监听处理
|
||||
addListener(containerElement, ['changed', ({ detail: { speed, isNativeSpeed, previousSpeed } }: CustomEvent) => {
|
||||
// 记录(共享)倍速值
|
||||
sharedSpeed = speed
|
||||
if (isNativeSpeed) {
|
||||
sharedNativeSpeed = speed
|
||||
nativeSpeed = speed
|
||||
}
|
||||
// 原生支持倍速的应用后,需要清除扩展倍速选项上的样式
|
||||
if (options.extend && nativeSupportedRates.includes(speed)) {
|
||||
menuListElement.querySelector(`${extendedAgent.custom.speedMenuItem.selector}.extended${extendedAgent.custom.active.selector}`)?.classList.remove(activeClassName)
|
||||
}
|
||||
// 记忆
|
||||
// - `enabled` 表示是否启用记忆
|
||||
// - `options.individualRemember` 表示是否启用细化到视频级别的记忆
|
||||
if (enabled) {
|
||||
if (options.individualRemember) {
|
||||
rememberSpeed(
|
||||
speed,
|
||||
speed !== getFallbackVideoSpeed(),
|
||||
)
|
||||
} else {
|
||||
options.speed = speed.toString()
|
||||
}
|
||||
|
||||
currentSpeed = parseFloat(selectedSpeedOption.dataset.value ?? '1')
|
||||
|
||||
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
|
||||
}
|
||||
// 刷新 sharedPreviousSpeed
|
||||
// - 用户可以通过倍速菜单或者倍速快捷键造成类似 1.5x 2.0x 2.0x... 这样的倍速设定序列
|
||||
// 我们不希望在第二个 2.0x 的时候刷新 this._previousSpeedVal,这样会比较死板
|
||||
// 判定依据在于 previousSpeed !== currentSpeed
|
||||
if (previousSpeed && previousSpeed !== currentSpeed) {
|
||||
this.previousSpeedVal = previousSpeed
|
||||
// 判定依据在于 previousSpeed !== speed
|
||||
if (previousSpeed && previousSpeed !== speed) {
|
||||
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 { 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')
|
||||
let extendList = options.extendList as number[]
|
||||
export const getExtraSpeedMenuItemElements = async () => {
|
||||
const {
|
||||
VideoSpeedController,
|
||||
calcOrder,
|
||||
minValue,
|
||||
maxValue,
|
||||
stepValue,
|
||||
errorMessageDuration,
|
||||
} = await import('./controller')
|
||||
const getRecommendedValue = () => {
|
||||
const val = getSupportedRates().slice(-1)[0] + stepValue
|
||||
return val > maxValue ? null : val
|
||||
}
|
||||
|
||||
const getRecommendedValue = () => {
|
||||
const val = VideoSpeedController.supportedRates.slice(-1)[0] + stepValue
|
||||
return val > maxValue ? null : val
|
||||
}
|
||||
const speedMenuItemClassName = trimLeadingDot(extendedAgent.custom.speedMenuItem.selector)
|
||||
|
||||
const createExtendedSpeedMenuItemElement = (rate: number) => {
|
||||
const li = document.createElement('li')
|
||||
li.innerText = VideoSpeedController.formatSpeedText(rate)
|
||||
li.classList.add(VideoSpeedController.classNameMap.speedMenuItem, 'extended')
|
||||
li.dataset.value = rate.toString()
|
||||
li.style.order = calcOrder(rate)
|
||||
const createExtendedSpeedMenuItemElement = (rate: number) => {
|
||||
const li = document.createElement('li')
|
||||
li.innerText = formatSpeedText(rate)
|
||||
li.classList.add(speedMenuItemClassName, 'extended')
|
||||
li.dataset.value = rate.toString()
|
||||
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')
|
||||
i.classList.add('mdi', 'mdi-close-circle')
|
||||
i.addEventListener('click', () => {
|
||||
lodash.pull(extendList, rate)
|
||||
li.remove()
|
||||
})
|
||||
const updateInput = (elem: HTMLInputElement) => {
|
||||
const recommendedValue = getRecommendedValue()
|
||||
elem.setAttribute('min', recommendedValue ? (elem.value = recommendedValue.toString()) : (elem.value = '', minValue.toString()))
|
||||
}
|
||||
|
||||
li.append(i)
|
||||
|
||||
return li
|
||||
}
|
||||
|
||||
const createAddEntryElement = () => {
|
||||
const updateInput = (elem: HTMLInputElement) => {
|
||||
const value = getRecommendedValue()
|
||||
elem.setAttribute('min', value ? (elem.value = value.toString()) : (elem.value = '', minValue.toString()))
|
||||
}
|
||||
|
||||
const li = document.createElement('li')
|
||||
li.classList.add(VideoSpeedController.classNameMap.speedMenuItem)
|
||||
|
||||
const iconElement = document.createElement('i')
|
||||
iconElement.classList.add('mdi', 'mdi-playlist-plus')
|
||||
|
||||
const input = document.createElement('input')
|
||||
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)
|
||||
const createAddEntryElement = () => {
|
||||
const li = document.createElement('li')
|
||||
li.classList.add(speedMenuItemClassName)
|
||||
const iconElement = document.createElement('i')
|
||||
iconElement.classList.add('mdi', 'mdi-playlist-plus')
|
||||
const input = document.createElement('input')
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
input.style.display = 'none'
|
||||
li.addEventListener('mouseenter', () => {
|
||||
let afterElement = li.nextElementSibling as HTMLLIElement
|
||||
while (
|
||||
!afterElement.dataset.value
|
||||
|| (parseFloat(afterElement.dataset.value)
|
||||
> nativeSupportedRates.slice(-1)[0]
|
||||
&& value < parseFloat(afterElement.dataset.value))
|
||||
) {
|
||||
afterElement = afterElement.nextElementSibling as HTMLLIElement
|
||||
}
|
||||
afterElement.before(createExtendedSpeedMenuItemElement(value))
|
||||
updateInput(input)
|
||||
input.style.display = 'inline'
|
||||
iconElement.style.display = 'none'
|
||||
input.focus()
|
||||
})
|
||||
li.addEventListener('mouseleave', () => {
|
||||
iconElement.style.display = 'inline'
|
||||
input.style.display = 'none'
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
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(`
|
||||
.${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;
|
||||
}
|
||||
.${VideoSpeedController.classNameMap.speedContainer} .${VideoSpeedController.classNameMap.speedMenuItem}:first-child input {
|
||||
${extendedAgent.custom.speedContainer.selector} ${extendedAgent.custom.speedMenuItem.selector}:first-child input {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
line-height: inherit;
|
||||
@ -127,17 +126,17 @@ export const getExtraSpeedMenuItemElements = async () => {
|
||||
border: none;
|
||||
text-align: center;
|
||||
}
|
||||
.${VideoSpeedController.classNameMap.speedMenuItem} .mdi-close-circle {
|
||||
${extendedAgent.custom.speedMenuItem.selector} .mdi-close-circle {
|
||||
color: inherit;
|
||||
opacity: 0.5;
|
||||
display: none;
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
.${VideoSpeedController.classNameMap.speedMenuItem} .mdi-close-circle:hover {
|
||||
${extendedAgent.custom.speedMenuItem.selector} .mdi-close-circle:hover {
|
||||
opacity: 1;
|
||||
transition: all .3s;
|
||||
}
|
||||
@ -152,7 +151,7 @@ export const getExtraSpeedMenuItemElements = async () => {
|
||||
.add-speed-entry[type=number] {
|
||||
-moz-appearance:textfield;
|
||||
}
|
||||
.${VideoSpeedController.classNameMap.speedMenuList} {
|
||||
${extendedAgent.custom.speedMenuList.selector} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
@ -160,7 +159,7 @@ export const getExtraSpeedMenuItemElements = async () => {
|
||||
}
|
||||
`, 'extend-video-speed-style')
|
||||
|
||||
const elements = VideoSpeedController.extendedSupportedRates
|
||||
const elements = getExtendedSupportedRates()
|
||||
.map(rate => createExtendedSpeedMenuItemElement(rate))
|
||||
.reverse()
|
||||
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { importComponent } from '@/components/component'
|
||||
import { ComponentMetadata } from '@/components/types'
|
||||
import { playerUrls } from '@/core/utils/urls'
|
||||
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 = {
|
||||
name: 'rememberVideoSpeed',
|
||||
name: componentName,
|
||||
displayName: '倍速记忆',
|
||||
author: {
|
||||
name: 'JLoeve',
|
||||
@ -14,33 +20,31 @@ export const component: ComponentMetadata = {
|
||||
},
|
||||
tags: [componentsTags.video],
|
||||
urlInclude: playerUrls,
|
||||
entry: async () => {
|
||||
const { VideoSpeedController } = await import('./controller')
|
||||
VideoSpeedController.init()
|
||||
return VideoSpeedController
|
||||
},
|
||||
entry: async () => (await import('./controller')).createController(),
|
||||
plugin: {
|
||||
displayName: '倍速记忆 - 快捷键支持',
|
||||
setup: async ({ addData }) => {
|
||||
const { getComponentSettings } = await import('@/core/settings')
|
||||
|
||||
const videoSpeed = async (
|
||||
context: KeyBindingActionContext,
|
||||
controllerAction: (
|
||||
controller: InstanceType<typeof import('./controller')['VideoSpeedController']>, rates: number[]
|
||||
controller: Controller, rates: number[]
|
||||
) => void,
|
||||
) => {
|
||||
const { VideoSpeedController } = await import('./controller')
|
||||
const controller = await VideoSpeedController.getInstance()
|
||||
controllerAction(controller, VideoSpeedController.supportedRates)
|
||||
context.showTip(`${controller.playbackRate}x`, 'mdi-fast-forward')
|
||||
// 不要提前导入,插件在组件加载之前进行加载,因此如果提前加载会取不到 entry 调用后返回的对象
|
||||
const controller = importComponent(componentName) as Controller
|
||||
controllerAction(controller, controller.getSupportedRates())
|
||||
context.showTip(`${controller.videoSpeed()}x`, 'mdi-fast-forward')
|
||||
}
|
||||
|
||||
addData('keymap.actions', (actions: Record<string, KeyBindingAction>) => {
|
||||
actions.videoSpeedIncrease = {
|
||||
displayName: '提高倍速',
|
||||
run: context => {
|
||||
videoSpeed(context, (controller, rates) => {
|
||||
controller.setVideoSpeed(
|
||||
rates.find(it => it > controller.playbackRate)
|
||||
rates.find(it => it > controller.videoSpeed())
|
||||
|| rates[rates.length - 1],
|
||||
)
|
||||
})
|
||||
@ -52,7 +56,7 @@ export const component: ComponentMetadata = {
|
||||
run: context => {
|
||||
videoSpeed(context, (controller, rates) => {
|
||||
controller.setVideoSpeed(
|
||||
[...rates].reverse().find(it => it < controller.playbackRate)
|
||||
[...rates].reverse().find(it => it < controller.videoSpeed())
|
||||
|| rates[0],
|
||||
)
|
||||
})
|
||||
@ -73,7 +77,7 @@ export const component: ComponentMetadata = {
|
||||
displayName: '清除当前倍速记忆',
|
||||
run: context => {
|
||||
videoSpeed(context, controller => {
|
||||
controller.reset(true)
|
||||
controller.resetVideoSpeed(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
|
||||
/** 表示一个组件的设置 */
|
||||
export interface ComponentSettings {
|
||||
export interface ComponentSettings<O = { [key: string]: any }> {
|
||||
/** 是否启用此组件 */
|
||||
enabled: boolean
|
||||
/** 组件选项 */
|
||||
options: { [key: string]: any }
|
||||
options: O
|
||||
}
|
||||
/** 脚本总设置 */
|
||||
export interface Settings {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user