This commit is contained in:
JLoeve 2021-11-30 01:15:15 +08:00
parent 72dc861245
commit cdcb7b4b73
5 changed files with 464 additions and 481 deletions

View File

@ -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)
}
}
})

View File

@ -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()

View File

@ -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
},

View 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))

View File

@ -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 {