refactor(core): CORE-18 type builtins and own prompt lifetimes

This commit is contained in:
Harvey Zhao committed 2026-09-11 12:31:01 +08:00
1 parent a842058136
commit 98a1800032
55 files changed
+26511 -650

No files matched your search

-46
View File
@@ -1,46 +0,0 @@
import { getScope } from './lifecycle/instance'
import { timeout } from './lifecycle/resources'
import { isMobile, queryAll } from './utils'
import Component from './utils/component'
export default class Info extends Component {
constructor(art) {
super(art)
this.name = 'info'
if (!isMobile) {
this.init()
}
}
init() {
const {
proxy,
constructor,
template: { $infoPanel, $infoClose, $video },
} = this.art
proxy($infoClose, 'click', () => {
this.show = false
})
let cancel = () => {}
const $types = queryAll('[data-video]', $infoPanel) || []
this.art.on('destroy', () => cancel())
const scope = getScope(this.art)
function loop() {
for (let index = 0; index < $types.length; index++) {
const item = $types[index]
const value = $video[item.dataset.video]
const textContent = typeof value === 'number' ? value.toFixed(2) : value
if (item.textContent !== textContent) {
item.textContent = textContent
}
}
cancel = timeout(scope, loop, constructor.INFO_LOOP_TIME)
}
loop()
}
}
+35
View File
@@ -0,0 +1,35 @@
import type { InfoHost } from './info/poll'
import type ResourceScope from './lifecycle/scope'
import { pollInfo } from './info/poll'
import { getScope, isClosing } from './lifecycle/instance'
import { isMobile } from './utils'
import Component from './utils/component'
const states = new WeakMap<Info, { scope?: ResourceScope, revision: number }>()
export default class Info extends Component<InfoHost> {
constructor(art: InfoHost) {
super(art)
this.name = 'info'
states.set(this, { revision: 0 })
if (!isMobile)
this.init()
}
init(): void {
const state = states.get(this)!
const revision = ++state.revision
state.scope?.dispose()
if (isClosing(this.art) || state.revision !== revision)
return
const scope = getScope(this.art).child()
state.scope = scope
scope.add(() => {
if (state.scope === scope)
state.scope = undefined
})
pollInfo(this.art, scope, () => {
this.show = false
})
}
}
+71
View File
@@ -0,0 +1,71 @@
import type { SubscriptionHost } from '../component/resources'
import type { ComponentHost } from '../component/types'
import type ResourceScope from '../lifecycle/scope'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
export interface InfoHost extends ComponentHost, SubscriptionHost<{ destroy: [] }> {
template: { $player: HTMLElement, $infoPanel: HTMLElement, $infoClose: HTMLElement, $video: object }
constructor: { INFO_LOOP_TIME: number }
proxy: ComponentHost['events']['proxy']
}
export function pollInfo(art: InfoHost, scope: ResourceScope, close: () => void): void {
const active = () => !scope.closed && !isClosing(art)
const fail = (error: unknown): never => {
try {
scope.dispose()
}
catch (cleanupError) {
console.warn('Failed to release info polling resources:', cleanupError)
}
throw error
}
try {
const { proxy, constructor, template: { $infoPanel, $infoClose, $video } } = art
const cleanup = proxy($infoClose, 'click', () => {
if (active())
close()
})
scope.add(() => {
art.events.remove(cleanup)
})
const items = Array.from($infoPanel.querySelectorAll<HTMLElement>('[data-video]'))
const destroy = () => scope.dispose()
art.on('destroy', destroy)
scope.add(() => {
art.off('destroy', destroy)
})
const loop = () => {
try {
for (const item of items) {
if (!active())
return
const value: unknown = Reflect.get($video, item.dataset.video ?? 'undefined')
if (!active())
return
const raw = typeof value === 'number' ? value.toFixed(2) : value
if (!active())
return
if (item.textContent !== raw) {
// Match the DOM nullable-string conversion, including Symbol errors.
const text = raw == null ? null : `${raw}`
if (!active())
return
item.textContent = text
}
}
if (active())
timeout(scope, loop, constructor.INFO_LOOP_TIME)
}
catch (error) {
fail(error)
}
}
if (active())
loop()
}
catch (error) {
fail(error)
}
}
@@ -0,0 +1,80 @@
import type ResourceScope from '../lifecycle/scope'
import { isClosing } from '../lifecycle/instance'
import { timeout } from '../lifecycle/resources'
import { getSourceScope } from '../source/operation'
import { addClass, removeClass } from '../utils'
export interface LongPressHost {
template: { $player: HTMLElement }
constructor: { FAST_FORWARD_TIME: number, FAST_FORWARD_VALUE: number }
playing: boolean
isLock: boolean
playbackRate: number
}
interface Press {
scope: ResourceScope
active: boolean
previousRate: number
}
export function longPress(art: LongPressHost) {
let current: Press | undefined
let generation = 0
const hasActivePress = () => current?.active
const stop = () => {
generation++
current?.scope.dispose()
}
const start = (event: TouchEvent) => {
const revision = ++generation
current?.scope.dispose()
if (revision !== generation || isClosing(art) || event.touches.length !== 1 || !art.playing || art.isLock)
return
const press: Press = { scope: getSourceScope(art).child(), active: false, previousRate: 1 }
current = press
press.scope.add(() => {
if (current !== press)
return
current = undefined
if (press.active) {
try {
art.playbackRate = press.previousRate
}
finally {
// A callback may have already started and activated a newer press.
if (!hasActivePress())
removeClass(art.template.$player, 'art-fast-forward')
}
}
})
timeout(press.scope, () => {
if (current !== press || isClosing(art) || !art.playing || art.isLock) {
press.scope.dispose()
return
}
try {
press.previousRate = art.playbackRate
if (current !== press || press.scope.closed)
return
press.active = true
const rate = art.constructor.FAST_FORWARD_VALUE
if (current !== press || press.scope.closed || isClosing(art))
return
art.playbackRate = rate
if (current === press && !press.scope.closed && !isClosing(art))
addClass(art.template.$player, 'art-fast-forward')
}
catch (error) {
try {
press.scope.dispose()
}
catch (cleanupError) {
console.warn('Failed to restore fast-forward playback rate:', cleanupError)
}
throw error
}
}, art.constructor.FAST_FORWARD_TIME)
}
return { start, stop }
}
+13 -1
View File
@@ -9,6 +9,7 @@ interface LifecycleOwner {
interface State {
scope: ResourceScope
finalizers: ResourceScope
destroying: boolean
rollback?: () => void
releaseContainer?: () => void
@@ -18,7 +19,13 @@ const states = new WeakMap<object, State>()
const containers = new WeakMap<Element, object>()
export function beginLifecycle(owner: object): void {
states.set(owner, { scope: new ResourceScope(), destroying: false })
const state: State = { scope: new ResourceScope(), finalizers: new ResourceScope(), destroying: false }
states.set(owner, state)
state.scope.add(() => {
// Direct scope disposal has no destroy-event phase to wait for.
if (!state.destroying)
state.finalizers.dispose()
})
}
function stateOf(owner: object): State {
@@ -32,6 +39,10 @@ export function getScope(owner: object): ResourceScope {
return stateOf(owner).scope
}
export function getFinalizationScope(owner: object): ResourceScope {
return stateOf(owner).finalizers
}
export function isClosing(owner: object): boolean {
const state = stateOf(owner)
return state.destroying || state.scope.closed
@@ -83,6 +94,7 @@ export function destroyInstance(owner: LifecycleOwner, instances: LifecycleOwner
state.releaseContainer = undefined
}
attempt(() => owner.emit('destroy'))
attempt(() => state.finalizers.dispose())
if (failed && state.rollback)
attempt(state.rollback)
state.rollback = undefined
-10
View File
@@ -1,10 +0,0 @@
import { append } from './utils'
import Component from './utils/component'
export default class Loading extends Component {
constructor(art) {
super(art)
this.name = 'loading'
append(art.template.$loading, art.icons.loading)
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { ComponentHost } from './component/types'
import { appendElement } from './component/dom'
import { isClosing } from './lifecycle/instance'
import Component from './utils/component'
export interface LoadingHost extends ComponentHost {
template: { $player: HTMLElement, $loading: HTMLElement }
icons: { loading: string | Element }
}
export default class Loading extends Component<LoadingHost> {
constructor(art: LoadingHost) {
super(art)
this.name = 'loading'
const icon = art.icons.loading
if (!isClosing(art))
appendElement(art.template.$loading, icon)
}
}
-23
View File
@@ -1,23 +0,0 @@
import { append, setStyle, silencePromise } from './utils'
import Component from './utils/component'
export default class Mask extends Component {
constructor(art) {
super(art)
this.name = 'mask'
const { template, icons, events } = art
const $state = append(template.$state, icons.state)
const $error = append(template.$state, icons.error)
setStyle($error, 'display', 'none')
art.on('destroy', () => {
setStyle($state, 'display', 'none')
setStyle($error, 'display', null)
})
events.proxy(template.$state, 'click', () => silencePromise(art.play()))
}
}
+57
View File
@@ -0,0 +1,57 @@
import type { SubscriptionHost } from './component/resources'
import type { ComponentHost } from './component/types'
import { appendElement } from './component/dom'
import { getFinalizationScope, isClosing } from './lifecycle/instance'
import { setStyle, silencePromise } from './utils'
import Component from './utils/component'
export interface MaskHost extends ComponentHost, SubscriptionHost<{ destroy: [] }> {
template: { $player: HTMLElement, $state: HTMLElement }
icons: { state: string | Element, error: string | Element }
play: () => unknown
}
export default class Mask extends Component<MaskHost> {
constructor(art: MaskHost) {
super(art)
this.name = 'mask'
const { template, icons, events } = art
const stateIcon = icons.state
if (isClosing(art))
return
const $state = appendElement(template.$state, stateIcon)
const errorIcon = icons.error
if (isClosing(art))
return
const $error = appendElement(template.$state, errorIcon)
setStyle($error, 'display', 'none')
let terminalEventHandled = false
const destroy = () => {
setStyle($state, 'display', 'none')
setStyle($error, 'display', null)
if (isClosing(art))
terminalEventHandled = true
}
art.on('destroy', destroy)
getFinalizationScope(art).add(() => {
try {
if (!terminalEventHandled)
destroy()
}
finally {
art.off('destroy', destroy)
}
})
if (!isClosing(art)) {
events.proxy(template.$state, 'click', () => {
if (!isClosing(art))
silencePromise(art.play())
})
}
}
}
-48
View File
@@ -1,48 +0,0 @@
import { getScope } from './lifecycle/instance'
import { addClass, removeClass } from './utils'
export default class Notice {
constructor(art) {
this.art = art
this.timer = null
getScope(art).add(() => this.destroy())
art.on('destroy', () => this.destroy())
}
destroy() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
}
set show(msg) {
if (getScope(this.art).closed)
return
const {
constructor,
template: { $player, $noticeInner },
} = this.art
if (msg) {
$noticeInner.textContent = msg instanceof Error ? msg.message.trim() : msg
addClass($player, 'art-notice-show')
clearTimeout(this.timer)
this.timer = setTimeout(() => {
$noticeInner.textContent = ''
removeClass($player, 'art-notice-show')
}, constructor.NOTICE_TIME)
}
else {
removeClass($player, 'art-notice-show')
}
}
get show() {
const {
template: { $player },
} = this.art
return $player.classList.contains('art-notice-show')
}
}
+99
View File
@@ -0,0 +1,99 @@
import type { SubscriptionHost } from './component/resources'
import { getScope, isClosing } from './lifecycle/instance'
import { addClass, removeClass } from './utils'
interface NoticeHost extends SubscriptionHost<{ destroy: [] }> {
constructor: { NOTICE_TIME: number }
template: { $player: HTMLElement, $noticeInner: HTMLElement }
}
interface NoticeState {
revision: number
pending?: object
}
const states = new WeakMap<Notice, NoticeState>()
function cancelNotice(notice: Notice): void {
const state = states.get(notice)!
state.revision++
state.pending = undefined
if (notice.timer !== null) {
clearTimeout(notice.timer)
notice.timer = null
}
}
export default class Notice {
declare art: NoticeHost
declare timer: ReturnType<typeof setTimeout> | null
constructor(art: NoticeHost) {
this.art = art
this.timer = null
states.set(this, { revision: 0 })
const destroy = () => this.destroy()
art.on('destroy', destroy)
getScope(art).add(() => {
art.off('destroy', destroy)
})
getScope(art).add(() => {
this.destroy()
})
}
destroy(): void {
cancelNotice(this)
}
set show(msg: string | Error | false) {
if (isClosing(this.art))
return
const state = states.get(this)!
state.revision++
const {
constructor,
template: { $player, $noticeInner },
} = this.art
if (msg) {
cancelNotice(this)
const revision = state.revision
const active = () => !isClosing(this.art) && state.revision === revision
$noticeInner.textContent = msg instanceof Error ? msg.message.trim() : msg
if (!active())
return
addClass($player, 'art-notice-show')
if (!active())
return
const delay = constructor.NOTICE_TIME
if (!active())
return
const pending = {}
state.pending = pending
const timer = setTimeout(() => {
if (isClosing(this.art) || state.pending !== pending)
return
state.pending = undefined
const before = state.revision
$noticeInner.textContent = ''
if (!isClosing(this.art) && state.revision === before)
removeClass($player, 'art-notice-show')
}, delay)
if (active())
this.timer = timer
else
clearTimeout(timer)
}
else {
removeClass($player, 'art-notice-show')
}
}
get show(): boolean {
const {
template: { $player },
} = this.art
return $player.classList.contains('art-notice-show')
}
}
@@ -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
},
}
}
+4 -4
View File
@@ -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')
},
}
}
-68
View File
@@ -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()
}
},
}
}
+94
View File
@@ -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' }
}