mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-26 00:31:20 -08:00
refactor(core): CORE-18 type builtins and own prompt lifetimes
This commit is contained in:
1 parent
a842058136
commit
98a1800032
55 files changed
+26511
-650
No files matched your search
@@ -0,0 +1,144 @@
|
||||
import type { SubscriptionHost } from '../../component/resources'
|
||||
import type { ComponentHost } from '../../component/types'
|
||||
import type ResourceScope from '../../lifecycle/scope'
|
||||
import type { PlaybackStorage } from './records'
|
||||
import { appendElement, queryElement } from '../../component/dom'
|
||||
import { entryScope, subscribeEntry } from '../../component/resources'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
import { timeout } from '../../lifecycle/resources'
|
||||
import { getSourceScope } from '../../source/operation'
|
||||
import { secondToTime, setStyle, silencePromise } from '../../utils'
|
||||
import { readTimes } from './records'
|
||||
|
||||
interface PromptEvents {
|
||||
'ready': []
|
||||
'restart': [string]
|
||||
'video:timeupdate': [Event]
|
||||
}
|
||||
|
||||
export interface ResumePromptHost extends ComponentHost, SubscriptionHost<PromptEvents> {
|
||||
template: { $player: HTMLElement, $poster: HTMLElement }
|
||||
constructor: { AUTO_PLAYBACK_MIN: number, AUTO_PLAYBACK_TIMEOUT: number }
|
||||
layers: { add: (option: { name: string, html: string }) => HTMLDivElement | undefined }
|
||||
icons: { close: string | Element }
|
||||
i18n: { get: (key: string) => string }
|
||||
storage: PlaybackStorage
|
||||
option: { id?: string, url: string }
|
||||
seek: number
|
||||
play: () => unknown
|
||||
}
|
||||
|
||||
export function installResumePrompt(art: ResumePromptHost): () => void {
|
||||
const { i18n, icons, storage, constructor, template: { $poster } } = art
|
||||
const element = art.layers.add({
|
||||
name: 'auto-playback',
|
||||
html: `
|
||||
<div class="art-auto-playback-close"></div>
|
||||
<div class="art-auto-playback-last"></div>
|
||||
<div class="art-auto-playback-jump"></div>
|
||||
`,
|
||||
})
|
||||
if (!element || isClosing(art))
|
||||
return () => {}
|
||||
const $last = queryElement('.art-auto-playback-last', element)
|
||||
const $jump = queryElement('.art-auto-playback-jump', element)
|
||||
const $close = queryElement('.art-auto-playback-close', element)
|
||||
appendElement($close, icons.close)
|
||||
const owner = entryScope(element)
|
||||
let current: ResourceScope | undefined
|
||||
let revision = 0
|
||||
const init = () => {
|
||||
const generation = ++revision
|
||||
current?.dispose()
|
||||
if (owner.closed || isClosing(art) || revision !== generation)
|
||||
return
|
||||
const scope = owner.child()
|
||||
current = scope
|
||||
const active = () => current === scope && !scope.closed && !isClosing(art)
|
||||
scope.add(() => {
|
||||
if (current === scope) {
|
||||
current = undefined
|
||||
setStyle(element, 'display', 'none')
|
||||
}
|
||||
})
|
||||
const releaseSource = getSourceScope(art).add(() => {
|
||||
scope.dispose()
|
||||
})
|
||||
scope.add(() => {
|
||||
releaseSource()
|
||||
})
|
||||
const times = readTimes(storage)
|
||||
const currentTime = times[art.option.id || art.option.url]
|
||||
if (!active())
|
||||
return
|
||||
setStyle(element, 'display', 'none')
|
||||
if (!active() || !currentTime || !(currentTime >= constructor.AUTO_PLAYBACK_MIN))
|
||||
return
|
||||
if (!active())
|
||||
return
|
||||
setStyle(element, 'display', 'flex')
|
||||
if (!active())
|
||||
return
|
||||
const lastText = `${i18n.get('Last Seen')} ${secondToTime(currentTime)}`
|
||||
if (!active())
|
||||
return
|
||||
$last.textContent = lastText
|
||||
if (!active())
|
||||
return
|
||||
const jumpText = i18n.get('Jump Play')
|
||||
if (!active())
|
||||
return
|
||||
$jump.textContent = jumpText
|
||||
if (!active())
|
||||
return
|
||||
const bind = (target: HTMLElement, callback: () => void) => {
|
||||
const cleanup = art.events.proxy(target, 'click', () => {
|
||||
if (active())
|
||||
callback()
|
||||
})
|
||||
scope.add(() => {
|
||||
art.events.remove(cleanup)
|
||||
})
|
||||
}
|
||||
bind($close, () => {
|
||||
setStyle(element, 'display', 'none')
|
||||
})
|
||||
if (!active())
|
||||
return
|
||||
bind($jump, () => {
|
||||
art.seek = currentTime
|
||||
if (!active())
|
||||
return
|
||||
silencePromise(art.play())
|
||||
if (!active())
|
||||
return
|
||||
setStyle($poster, 'display', 'none')
|
||||
if (active())
|
||||
setStyle(element, 'display', 'none')
|
||||
})
|
||||
if (!active())
|
||||
return
|
||||
let releaseUpdate = () => {}
|
||||
let fired = false
|
||||
const update = () => {
|
||||
if (fired || !active())
|
||||
return
|
||||
fired = true
|
||||
releaseUpdate()
|
||||
timeout(scope, () => {
|
||||
if (active())
|
||||
setStyle(element, 'display', 'none')
|
||||
}, constructor.AUTO_PLAYBACK_TIMEOUT)
|
||||
}
|
||||
art.on('video:timeupdate', update)
|
||||
releaseUpdate = scope.add(() => {
|
||||
art.off('video:timeupdate', update)
|
||||
})
|
||||
if (fired)
|
||||
releaseUpdate()
|
||||
}
|
||||
return () => {
|
||||
subscribeEntry<PromptEvents, 'ready'>(art, element, 'ready', init)
|
||||
subscribeEntry<PromptEvents, 'restart'>(art, element, 'restart', init)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { SubscriptionHost } from '../../component/resources'
|
||||
import { eventSubscriptions } from '../../events/subscriptions'
|
||||
import { isClosing } from '../../lifecycle/instance'
|
||||
|
||||
export type PlaybackTimes = Record<string, number>
|
||||
|
||||
export interface PlaybackStorage {
|
||||
get: (key: 'times') => PlaybackTimes | null | undefined
|
||||
set: (key: 'times', value: PlaybackTimes) => void
|
||||
del: (key: 'times') => void
|
||||
}
|
||||
|
||||
export interface PlaybackRecordHost extends SubscriptionHost<{ 'video:timeupdate': [Event] }> {
|
||||
storage: PlaybackStorage
|
||||
constructor: { AUTO_PLAYBACK_MAX: number }
|
||||
option: { id?: string, url: string }
|
||||
playing: boolean
|
||||
currentTime: number
|
||||
}
|
||||
|
||||
export function readTimes(storage: PlaybackStorage): PlaybackTimes {
|
||||
return storage.get('times') || {}
|
||||
}
|
||||
|
||||
export function installPlaybackRecords(art: PlaybackRecordHost): void {
|
||||
const { storage, constructor } = art
|
||||
eventSubscriptions<{ 'video:timeupdate': [Event] }>(art)('video:timeupdate', () => {
|
||||
if (!art.playing)
|
||||
return
|
||||
const times = readTimes(storage)
|
||||
const keys = Object.keys(times)
|
||||
const max = constructor.AUTO_PLAYBACK_MAX
|
||||
const key = art.option.id || art.option.url
|
||||
const time = art.currentTime
|
||||
if (isClosing(art))
|
||||
return
|
||||
// Preserve the historical strict threshold and one-oldest-entry pruning.
|
||||
if (keys.length > max)
|
||||
delete times[keys[0]!]
|
||||
times[key] = time
|
||||
if (!isClosing(art))
|
||||
storage.set('times', times)
|
||||
})
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { append, query, secondToTime, setStyle, silencePromise } from '../utils'
|
||||
|
||||
export default function autoPlayback(art) {
|
||||
const {
|
||||
i18n,
|
||||
icons,
|
||||
storage,
|
||||
constructor,
|
||||
proxy,
|
||||
template: { $poster },
|
||||
} = art
|
||||
|
||||
const $autoPlayback = art.layers.add({
|
||||
name: 'auto-playback',
|
||||
html: `
|
||||
<div class="art-auto-playback-close"></div>
|
||||
<div class="art-auto-playback-last"></div>
|
||||
<div class="art-auto-playback-jump"></div>
|
||||
`,
|
||||
})
|
||||
|
||||
const $last = query('.art-auto-playback-last', $autoPlayback)
|
||||
const $jump = query('.art-auto-playback-jump', $autoPlayback)
|
||||
const $close = query('.art-auto-playback-close', $autoPlayback)
|
||||
append($close, icons.close)
|
||||
|
||||
let timer = null
|
||||
|
||||
art.on('video:timeupdate', () => {
|
||||
if (art.playing) {
|
||||
const times = storage.get('times') || {}
|
||||
const keys = Object.keys(times)
|
||||
if (keys.length > constructor.AUTO_PLAYBACK_MAX) {
|
||||
delete times[keys[0]]
|
||||
}
|
||||
times[art.option.id || art.option.url] = art.currentTime
|
||||
storage.set('times', times)
|
||||
}
|
||||
})
|
||||
|
||||
function init() {
|
||||
const times = storage.get('times') || {}
|
||||
const currentTime = times[art.option.id || art.option.url]
|
||||
|
||||
clearTimeout(timer)
|
||||
setStyle($autoPlayback, 'display', 'none')
|
||||
|
||||
if (currentTime && currentTime >= constructor.AUTO_PLAYBACK_MIN) {
|
||||
setStyle($autoPlayback, 'display', 'flex')
|
||||
|
||||
$last.textContent = `${i18n.get('Last Seen')} ${secondToTime(currentTime)}`
|
||||
$jump.textContent = i18n.get('Jump Play')
|
||||
|
||||
proxy($close, 'click', () => {
|
||||
setStyle($autoPlayback, 'display', 'none')
|
||||
})
|
||||
|
||||
proxy($jump, 'click', () => {
|
||||
art.seek = currentTime
|
||||
silencePromise(art.play())
|
||||
setStyle($poster, 'display', 'none')
|
||||
setStyle($autoPlayback, 'display', 'none')
|
||||
})
|
||||
|
||||
art.once('video:timeupdate', () => {
|
||||
timer = setTimeout(() => {
|
||||
setStyle($autoPlayback, 'display', 'none')
|
||||
}, constructor.AUTO_PLAYBACK_TIMEOUT)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
art.on('ready', init)
|
||||
art.on('restart', init)
|
||||
|
||||
return {
|
||||
name: 'auto-playback',
|
||||
get times() {
|
||||
return storage.get('times') || {}
|
||||
},
|
||||
clear() {
|
||||
return storage.del('times')
|
||||
},
|
||||
delete(id) {
|
||||
const times = storage.get('times') || {}
|
||||
delete times[id]
|
||||
storage.set('times', times)
|
||||
return times
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ResumePromptHost } from './auto-playback/prompt'
|
||||
import type { PlaybackRecordHost } from './auto-playback/records'
|
||||
import { installResumePrompt } from './auto-playback/prompt'
|
||||
import { installPlaybackRecords, readTimes } from './auto-playback/records'
|
||||
|
||||
export type AutoPlaybackHost = ResumePromptHost & PlaybackRecordHost
|
||||
|
||||
export default function autoPlayback(art: AutoPlaybackHost) {
|
||||
const { storage } = art
|
||||
// Keep time recording before the prompt's first-update timer subscription.
|
||||
const installPrompt = installResumePrompt(art)
|
||||
installPlaybackRecords(art)
|
||||
installPrompt()
|
||||
return {
|
||||
name: 'auto-playback',
|
||||
get times() {
|
||||
return readTimes(storage)
|
||||
},
|
||||
clear() {
|
||||
return storage.del('times')
|
||||
},
|
||||
delete(id: string) {
|
||||
const times = readTimes(storage)
|
||||
delete times[id]
|
||||
storage.set('times', times)
|
||||
return times
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,15 @@ import miniProgressBar from './miniProgressBar'
|
||||
export default function installBuiltins<Host extends PluginHost<Host>>(registry: BuiltinRegistry<Host>, option: PluginHost<Host>['option']): void {
|
||||
// Read each condition at its original turn; earlier plugins may change options.
|
||||
if (!isClosing(registry.art) && option.miniProgressBar && !option.isLive)
|
||||
registry.add(miniProgressBar)
|
||||
registry.add(miniProgressBar as unknown as PluginFactory<Host>)
|
||||
if (!isClosing(registry.art) && option.lock && isMobile)
|
||||
registry.add(lock)
|
||||
registry.add(lock as unknown as PluginFactory<Host>)
|
||||
if (!isClosing(registry.art) && option.autoPlayback && !option.isLive)
|
||||
registry.add(autoPlayback)
|
||||
registry.add(autoPlayback as unknown as PluginFactory<Host>)
|
||||
if (!isClosing(registry.art) && option.autoOrientation && isMobile) {
|
||||
// The JS constructor supplies the complete host before builtin installation.
|
||||
registry.add(autoOrientation as unknown as PluginFactory<Host>)
|
||||
}
|
||||
if (!isClosing(registry.art) && option.fastForward && isMobile && !option.isLive)
|
||||
registry.add(fastForward)
|
||||
registry.add(fastForward as unknown as PluginFactory<Host>)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { addClass, hasClass, removeClass } from '../utils'
|
||||
|
||||
export default function fastForward(art) {
|
||||
const {
|
||||
constructor,
|
||||
proxy,
|
||||
template: { $player, $video },
|
||||
} = art
|
||||
|
||||
let timer = null
|
||||
let isPress = false
|
||||
let lastPlaybackRate = 1
|
||||
|
||||
const onStart = (event) => {
|
||||
if (event.touches.length === 1 && art.playing && !art.isLock) {
|
||||
timer = setTimeout(() => {
|
||||
isPress = true
|
||||
lastPlaybackRate = art.playbackRate
|
||||
art.playbackRate = constructor.FAST_FORWARD_VALUE
|
||||
addClass($player, 'art-fast-forward')
|
||||
}, constructor.FAST_FORWARD_TIME)
|
||||
}
|
||||
}
|
||||
|
||||
const onStop = () => {
|
||||
clearTimeout(timer)
|
||||
if (isPress) {
|
||||
isPress = false
|
||||
art.playbackRate = lastPlaybackRate
|
||||
removeClass($player, 'art-fast-forward')
|
||||
}
|
||||
}
|
||||
|
||||
proxy($video, 'touchstart', onStart)
|
||||
art.on('document:touchmove', onStop)
|
||||
art.on('document:touchend', onStop)
|
||||
|
||||
return {
|
||||
name: 'fastForward',
|
||||
get state() {
|
||||
return hasClass($player, 'art-fast-forward')
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import type { LongPressHost } from '../input/long-press'
|
||||
import type { PointerRegistry } from '../input/pointer-types'
|
||||
import { eventSubscriptions } from '../events/subscriptions'
|
||||
import { longPress } from '../input/long-press'
|
||||
import { hasClass } from '../utils'
|
||||
|
||||
interface PressEvents {
|
||||
'document:touchmove': [Event]
|
||||
'document:touchend': [Event]
|
||||
'document:touchcancel': [Event]
|
||||
'video:pause': [Event]
|
||||
'lock': [boolean]
|
||||
'destroy': []
|
||||
}
|
||||
|
||||
export interface FastForwardHost extends LongPressHost, SubscriptionHost<PressEvents> {
|
||||
template: { $player: HTMLElement, $video: EventTarget }
|
||||
proxy: PointerRegistry['proxy']
|
||||
}
|
||||
|
||||
export default function fastForward(art: FastForwardHost): { name: string, readonly state: boolean } {
|
||||
const { proxy, template: { $player, $video } } = art
|
||||
const press = longPress(art)
|
||||
const subscribe = eventSubscriptions<PressEvents>(art)
|
||||
proxy($video, 'touchstart', event => press.start(event as TouchEvent))
|
||||
proxy($video, 'touchcancel', press.stop)
|
||||
subscribe('document:touchmove', press.stop)
|
||||
subscribe('document:touchend', press.stop)
|
||||
subscribe('document:touchcancel', press.stop)
|
||||
subscribe('video:pause', press.stop)
|
||||
subscribe('destroy', press.stop)
|
||||
subscribe('lock', (locked) => {
|
||||
if (locked)
|
||||
press.stop()
|
||||
})
|
||||
return {
|
||||
name: 'fastForward',
|
||||
get state() {
|
||||
return hasClass($player, 'art-fast-forward')
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { addClass, append, hasClass, removeClass, setStyle } from '../utils'
|
||||
|
||||
export default function lock(art) {
|
||||
const {
|
||||
layers,
|
||||
icons,
|
||||
template: { $player },
|
||||
} = art
|
||||
|
||||
function getState() {
|
||||
return hasClass($player, 'art-lock')
|
||||
}
|
||||
|
||||
function setLock() {
|
||||
addClass($player, 'art-lock')
|
||||
art.isLock = true
|
||||
art.emit('lock', true)
|
||||
}
|
||||
|
||||
function setUnlock() {
|
||||
removeClass($player, 'art-lock')
|
||||
art.isLock = false
|
||||
art.emit('lock', false)
|
||||
}
|
||||
|
||||
layers.add({
|
||||
name: 'lock',
|
||||
mounted($el) {
|
||||
const $lock = append($el, icons.lock)
|
||||
const $unlock = append($el, icons.unlock)
|
||||
setStyle($lock, 'display', 'none')
|
||||
|
||||
art.on('lock', (state) => {
|
||||
if (state) {
|
||||
setStyle($lock, 'display', 'inline-flex')
|
||||
setStyle($unlock, 'display', 'none')
|
||||
}
|
||||
else {
|
||||
setStyle($lock, 'display', 'none')
|
||||
setStyle($unlock, 'display', 'inline-flex')
|
||||
}
|
||||
})
|
||||
},
|
||||
click() {
|
||||
if (getState()) {
|
||||
setUnlock()
|
||||
}
|
||||
else {
|
||||
setLock()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'lock',
|
||||
get state() {
|
||||
return getState()
|
||||
},
|
||||
set state(value) {
|
||||
if (value) {
|
||||
setLock()
|
||||
}
|
||||
else {
|
||||
setUnlock()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import { appendElement } from '../component/dom'
|
||||
import { entryScope, subscribeEntry } from '../component/resources'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { addClass, hasClass, removeClass, setStyle } from '../utils'
|
||||
|
||||
export interface LockHost extends SubscriptionHost<{ lock: [boolean] }> {
|
||||
template: { $player: HTMLElement }
|
||||
icons: { lock: string | Element, unlock: string | Element }
|
||||
isLock: boolean
|
||||
emit: (name: 'lock', state: boolean) => unknown
|
||||
layers: { add: (option: { name: string, mounted: (element: HTMLDivElement) => void, click: () => void }) => unknown }
|
||||
}
|
||||
|
||||
export default function lock(art: LockHost): { name: string, state: boolean } {
|
||||
const {
|
||||
layers,
|
||||
icons,
|
||||
template: { $player },
|
||||
} = art
|
||||
|
||||
function getState() {
|
||||
return hasClass($player, 'art-lock')
|
||||
}
|
||||
|
||||
function setLock() {
|
||||
if (isClosing(art))
|
||||
return
|
||||
addClass($player, 'art-lock')
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.isLock = true
|
||||
if (!isClosing(art))
|
||||
art.emit('lock', true)
|
||||
}
|
||||
|
||||
function setUnlock() {
|
||||
if (isClosing(art))
|
||||
return
|
||||
removeClass($player, 'art-lock')
|
||||
if (isClosing(art))
|
||||
return
|
||||
art.isLock = false
|
||||
if (!isClosing(art))
|
||||
art.emit('lock', false)
|
||||
}
|
||||
|
||||
layers.add({
|
||||
name: 'lock',
|
||||
mounted($el) {
|
||||
const scope = entryScope($el)
|
||||
if (scope.closed || isClosing(art))
|
||||
return
|
||||
const $lock = appendElement($el, icons.lock)
|
||||
if (scope.closed || isClosing(art))
|
||||
return
|
||||
const $unlock = appendElement($el, icons.unlock)
|
||||
if (scope.closed || isClosing(art))
|
||||
return
|
||||
setStyle($lock, 'display', 'none')
|
||||
|
||||
subscribeEntry<{ lock: [boolean] }, 'lock'>(art, $el, 'lock', (state) => {
|
||||
if (isClosing(art))
|
||||
return
|
||||
setStyle($lock, 'display', state ? 'inline-flex' : 'none')
|
||||
if (!scope.closed && !isClosing(art))
|
||||
setStyle($unlock, 'display', state ? 'none' : 'inline-flex')
|
||||
})
|
||||
},
|
||||
click() {
|
||||
if (getState()) {
|
||||
setUnlock()
|
||||
}
|
||||
else {
|
||||
setLock()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'lock',
|
||||
get state() {
|
||||
return getState()
|
||||
},
|
||||
set state(value: boolean) {
|
||||
if (value) {
|
||||
setLock()
|
||||
}
|
||||
else {
|
||||
setUnlock()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { addClass, removeClass } from '../utils'
|
||||
|
||||
export default function miniProgressBar(art) {
|
||||
art.on('control', (state) => {
|
||||
if (state) {
|
||||
removeClass(art.template.$player, 'art-mini-progress-bar')
|
||||
}
|
||||
else {
|
||||
addClass(art.template.$player, 'art-mini-progress-bar')
|
||||
}
|
||||
})
|
||||
|
||||
return { name: 'mini-progress-bar' }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import { eventSubscriptions } from '../events/subscriptions'
|
||||
import { addClass, removeClass } from '../utils'
|
||||
|
||||
export interface MiniProgressHost extends SubscriptionHost<{ control: [boolean] }> {
|
||||
template: { $player: HTMLElement }
|
||||
}
|
||||
|
||||
export default function miniProgressBar(art: MiniProgressHost): { name: string } {
|
||||
eventSubscriptions<{ control: [boolean] }>(art)('control', (state) => {
|
||||
if (state) {
|
||||
removeClass(art.template.$player, 'art-mini-progress-bar')
|
||||
}
|
||||
else {
|
||||
addClass(art.template.$player, 'art-mini-progress-bar')
|
||||
}
|
||||
})
|
||||
|
||||
return { name: 'mini-progress-bar' }
|
||||
}
|
||||
Reference in new issue
Block a user