refactor(core): [CORE-20] type entry and converge initialization

This commit is contained in:
Harvey Zhao committed 2026-09-11 16:07:06 +08:00
1 parent ccf77c4ed2
commit ee895a558f
100 files changed
+33854 -839

No files matched your search

@@ -1,40 +0,0 @@
import { def } from '../utils'
export default function airplayMix(art) {
const {
i18n,
notice,
proxy,
template: { $video },
} = art
let available = true
if (window.WebKitPlaybackTargetAvailabilityEvent && $video.webkitShowPlaybackTargetPicker) {
proxy($video, 'webkitplaybacktargetavailabilitychanged', (event) => {
switch (event.availability) {
case 'available':
available = true
break
case 'not-available':
available = false
break
}
})
}
else {
available = false
}
def(art, 'airplay', {
value() {
if (available) {
$video.webkitShowPlaybackTargetPicker()
art.emit('airplay')
}
else {
notice.show = i18n.get('AirPlay Not Available')
}
},
})
}
@@ -0,0 +1,70 @@
import type { NoticeSink } from '../notice'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export interface AirplayHost {
template: { $video: EventTarget & { webkitShowPlaybackTargetPicker?: () => void } }
i18n: { get: (key: string) => string }
notice: NoticeSink
proxy: (target: EventTarget, name: string, listener: (event: Event & { availability?: string }) => void) => unknown
emit: (name: 'airplay') => unknown
}
export default function airplayMix(art: AirplayHost): void {
if (isClosing(art))
return
const {
i18n,
notice,
proxy,
template: { $video },
} = art
let available = true
const supported = (window as Window & { WebKitPlaybackTargetAvailabilityEvent?: unknown }).WebKitPlaybackTargetAvailabilityEvent
if (isClosing(art))
return
const picker = supported && $video.webkitShowPlaybackTargetPicker
if (isClosing(art))
return
if (picker) {
proxy($video, 'webkitplaybacktargetavailabilitychanged', (event) => {
if (isClosing(art))
return
switch (event.availability) {
case 'available':
available = true
break
case 'not-available':
available = false
break
}
})
}
else {
available = false
}
if (isClosing(art))
return
def(art, 'airplay', {
value() {
if (isClosing(art))
return
if (available) {
const picker = $video.webkitShowPlaybackTargetPicker
if (isClosing(art))
return
Reflect.apply(picker!, $video, [])
if (!isClosing(art))
art.emit('airplay')
}
else {
const message = i18n.get('AirPlay Not Available')
if (!isClosing(art))
notice.show = message
}
},
})
}
-15
View File
@@ -1,15 +0,0 @@
import { def } from '../utils'
export default function attrMix(art) {
const {
template: { $video },
} = art
def(art, 'attr', {
value(key, value) {
if (value === undefined)
return $video[key]
$video[key] = value
},
})
}
+24
View File
@@ -0,0 +1,24 @@
import { def } from '../utils'
export interface AttributeMethods {
attr: {
(key: PropertyKey): unknown
(key: PropertyKey, value: unknown): unknown
}
}
export default function attrMix(art: { template: { $video: object } }): void {
const {
template: { $video },
} = art
def(art, 'attr', {
value(key: PropertyKey, value?: unknown): unknown {
// attr intentionally exposes arbitrary native/proxy properties, including symbols.
const target = $video as Record<PropertyKey, unknown>
if (value === undefined)
return target[key]
target[key] = value
},
})
}
@@ -1,16 +0,0 @@
import { def } from '../utils'
export default function cssVarMix(art) {
const { $player } = art.template
def(art, 'cssVar', {
value(key, value) {
if (value) {
return $player.style.setProperty(key, value)
}
else {
return getComputedStyle($player).getPropertyValue(key)
}
},
})
}
@@ -0,0 +1,24 @@
import { def } from '../utils'
export interface CssVariableMethods {
cssVar: {
(key: string): string
(key: string, value: unknown): string | void
}
}
export default function cssVarMix(art: { template: { $player: HTMLElement } }): void {
const { $player } = art.template
def(art, 'cssVar', {
value(key: string, value?: unknown): string | void {
if (value) {
// Retain Web IDL conversion and the historical truthy write condition.
return $player.style.setProperty(key, value as string)
}
else {
return getComputedStyle($player).getPropertyValue(key)
}
},
})
}
-75
View File
@@ -1,75 +0,0 @@
import airplayMix from './airplayMix'
import aspectRatioMix from './aspectRatioMix'
import attrMix from './attrMix'
import autoHeightMix from './autoHeightMix'
import autoSizeMix from './autoSizeMix'
import cssVarMix from './cssVarMix'
import currentTimeMix from './currentTimeMix'
import durationMix from './durationMix'
import eventInit from './eventInit'
import flipMix from './flipMix'
import fullscreenMix from './fullscreenMix'
import fullscreenWebMix from './fullscreenWebMix'
import loadedMix from './loadedMix'
import miniMix from './miniMix'
import optionInit from './optionInit'
import pauseMix from './pauseMix'
import pipMix from './pipMix'
import playbackRateMix from './playbackRateMix'
import playedMix from './playedMix'
import playingMix from './playingMix'
import playMix from './playMix'
import posterMix from './posterMix'
import qualityMix from './qualityMix'
import rectMix from './rectMix'
import screenshotMix from './screenshotMix'
import seekMix from './seekMix'
import stateMix from './stateMix'
import subtitleOffsetMix from './subtitleOffsetMix'
import switchMix from './switchMix'
import themeMix from './themeMix'
import thumbnailsMix from './thumbnailsMix'
import toggleMix from './toggleMix'
import typeMix from './typeMix'
import urlMix from './urlMix'
import volumeMix from './volumeMix'
export default class Player {
constructor(art) {
urlMix(art)
attrMix(art)
playMix(art)
pauseMix(art)
toggleMix(art)
seekMix(art)
volumeMix(art)
currentTimeMix(art)
durationMix(art)
switchMix(art)
playbackRateMix(art)
aspectRatioMix(art)
screenshotMix(art)
fullscreenMix(art)
fullscreenWebMix(art)
pipMix(art)
loadedMix(art)
playedMix(art)
playingMix(art)
autoSizeMix(art)
rectMix(art)
flipMix(art)
miniMix(art)
posterMix(art)
autoHeightMix(art)
cssVarMix(art)
themeMix(art)
typeMix(art)
stateMix(art)
subtitleOffsetMix(art)
airplayMix(art)
qualityMix(art)
thumbnailsMix(art)
eventInit(art)
optionInit(art)
}
}
+128
View File
@@ -0,0 +1,128 @@
import type { MediaSurface } from '../media/types'
import type { UrlHost } from '../source/types'
import { isClosing } from '../lifecycle/instance'
import airplayMix from './airplayMix'
import aspectRatioMix from './aspectRatioMix'
import attrMix from './attrMix'
import autoHeightMix from './autoHeightMix'
import autoSizeMix from './autoSizeMix'
import cssVarMix from './cssVarMix'
import currentTimeMix from './currentTimeMix'
import durationMix from './durationMix'
import eventInit from './eventInit'
import flipMix from './flipMix'
import fullscreenMix from './fullscreenMix'
import fullscreenWebMix from './fullscreenWebMix'
import loadedMix from './loadedMix'
import miniMix from './miniMix'
import optionInit from './optionInit'
import pauseMix from './pauseMix'
import pipMix from './pipMix'
import playbackRateMix from './playbackRateMix'
import playedMix from './playedMix'
import playingMix from './playingMix'
import playMix from './playMix'
import posterMix from './posterMix'
import qualityMix from './qualityMix'
import rectMix from './rectMix'
import screenshotMix from './screenshotMix'
import seekMix from './seekMix'
import stateMix from './stateMix'
import subtitleOffsetMix from './subtitleOffsetMix'
import switchMix from './switchMix'
import themeMix from './themeMix'
import thumbnailsMix from './thumbnailsMix'
import toggleMix from './toggleMix'
import typeMix from './typeMix'
import urlMix from './urlMix'
import volumeMix from './volumeMix'
// Installation requirements come from the owning modules. Some properties are
// installed here and consumed only by later callbacks, after construction.
type PlayerDependencies = Parameters<typeof airplayMix>[0]
& Parameters<typeof aspectRatioMix>[0]
& Parameters<typeof attrMix>[0]
& Parameters<typeof autoHeightMix>[0]
& Parameters<typeof autoSizeMix>[0]
& Parameters<typeof cssVarMix>[0]
& Parameters<typeof currentTimeMix>[0]
& Parameters<typeof durationMix>[0]
& Parameters<typeof eventInit>[0]
& Parameters<typeof flipMix>[0]
& Parameters<typeof fullscreenMix>[0]
& Parameters<typeof fullscreenWebMix>[0]
& Parameters<typeof loadedMix>[0]
& Parameters<typeof miniMix>[0]
& Parameters<typeof optionInit>[0]
& Parameters<typeof pauseMix>[0]
& Parameters<typeof pipMix>[0]
& Parameters<typeof playbackRateMix>[0]
& Parameters<typeof playedMix>[0]
& Parameters<typeof playingMix>[0]
& Parameters<typeof playMix>[0]
& Parameters<typeof posterMix>[0]
& Parameters<typeof qualityMix>[0]
& Parameters<typeof rectMix>[0]
& Parameters<typeof screenshotMix>[0]
& Parameters<typeof seekMix>[0]
& Parameters<typeof stateMix>[0]
& Parameters<typeof subtitleOffsetMix>[0]
& Parameters<typeof switchMix>[0]
& Parameters<typeof themeMix>[0]
& Parameters<typeof thumbnailsMix>[0]
& Parameters<typeof toggleMix>[0]
& Parameters<typeof typeMix>[0]
& Parameters<typeof volumeMix>[0]
// The order is observable through descriptors, subscriptions and option writes.
const installers: readonly ((art: PlayerDependencies) => void)[] = [
attrMix,
playMix,
pauseMix,
toggleMix,
seekMix,
volumeMix,
currentTimeMix,
durationMix,
switchMix,
playbackRateMix,
aspectRatioMix,
screenshotMix,
fullscreenMix,
fullscreenWebMix,
pipMix,
loadedMix,
playedMix,
playingMix,
autoSizeMix,
rectMix,
flipMix,
miniMix,
posterMix,
autoHeightMix,
cssVarMix,
themeMix,
typeMix,
stateMix,
subtitleOffsetMix,
airplayMix,
qualityMix,
thumbnailsMix,
eventInit,
optionInit,
]
export type PlayerHost<Host> = UrlHost<MediaSurface, Host> & PlayerDependencies
export default class Player<Host extends PlayerHost<Host>> {
constructor(art: Host) {
if (isClosing(art))
return
urlMix<MediaSurface, Host>(art)
for (const install of installers) {
if (isClosing(art))
return
install(art)
}
}
}
@@ -1,49 +0,0 @@
import { clamp, setStyle } from '../utils'
export default function optionInit(art) {
const {
option,
storage,
template: { $video, $poster },
} = art
for (const key in option.moreVideoAttr) {
art.attr(key, option.moreVideoAttr[key])
}
if (option.muted) {
art.muted = option.muted
}
if (option.volume) {
$video.volume = clamp(option.volume, 0, 1)
}
const volumeStorage = storage.get('volume')
if (typeof volumeStorage === 'number') {
$video.volume = clamp(volumeStorage, 0, 1)
}
if (option.poster) {
setStyle($poster, 'backgroundImage', `url(${option.poster})`)
}
if (option.autoplay) {
$video.autoplay = option.autoplay
}
if (option.playsInline) {
$video.playsInline = true
$video['webkit-playsinline'] = true
}
if (option.theme) {
option.cssVar['--art-theme'] = option.theme
}
for (const key in option.cssVar) {
art.cssVar(key, option.cssVar[key])
}
art.url = option.url
}
+117
View File
@@ -0,0 +1,117 @@
import type { ResolvedOption } from '../option/types'
import type { AttributeMethods } from './attrMix'
import type { CssVariableMethods } from './cssVarMix'
import { isClosing } from '../lifecycle/instance'
import { clamp, setStyle } from '../utils'
export interface InitialOptionHost extends AttributeMethods, CssVariableMethods {
option: Pick<ResolvedOption, 'moreVideoAttr' | 'muted' | 'volume' | 'poster' | 'autoplay' | 'playsInline' | 'theme' | 'cssVar' | 'url'>
storage: { get: (key: string) => unknown }
template: {
$video: Pick<HTMLMediaElement, 'volume'> & { 'autoplay'?: boolean, 'playsInline'?: boolean, 'webkit-playsinline'?: boolean }
$poster: HTMLElement
}
muted: boolean
get url(): string | null
set url(value: string)
}
export default function optionInit(art: InitialOptionHost): void {
if (isClosing(art))
return
const {
option,
storage,
template: { $video, $poster },
} = art
for (const key in option.moreVideoAttr) {
if (isClosing(art))
return
const value = (option.moreVideoAttr as Record<string, unknown>)[key]
if (isClosing(art))
return
art.attr(key, value)
}
if (isClosing(art))
return
if (option.muted) {
const value = option.muted
if (isClosing(art))
return
art.muted = value
}
if (isClosing(art))
return
if (option.volume) {
const value = clamp(option.volume, 0, 1)
if (isClosing(art))
return
$video.volume = value
}
if (isClosing(art))
return
const volumeStorage = storage.get('volume')
if (isClosing(art))
return
if (typeof volumeStorage === 'number') {
$video.volume = clamp(volumeStorage, 0, 1)
}
if (isClosing(art))
return
if (option.poster) {
const value = `url(${option.poster})`
if (isClosing(art))
return
setStyle($poster, 'backgroundImage', value)
}
if (isClosing(art))
return
if (option.autoplay) {
const value = option.autoplay
if (isClosing(art))
return
$video.autoplay = value
}
if (isClosing(art))
return
if (option.playsInline) {
if (isClosing(art))
return
$video.playsInline = true
if (isClosing(art))
return
$video['webkit-playsinline'] = true
}
if (isClosing(art))
return
if (option.theme) {
const styles = option.cssVar
const theme = option.theme
if (isClosing(art))
return
styles['--art-theme'] = theme
}
for (const key in option.cssVar) {
if (isClosing(art))
return
const value = (option.cssVar as Record<string, unknown>)[key]
if (isClosing(art))
return
art.cssVar(key, value)
}
if (isClosing(art))
return
const url = option.url
if (!isClosing(art))
art.url = url
}
@@ -1,6 +1,6 @@
import { def, setStyle } from '../utils'
export default function posterMix(art) {
export default function posterMix(art: { template: { $poster: HTMLElement } }): void {
const {
template: { $poster },
} = art
@@ -8,13 +8,13 @@ export default function posterMix(art) {
def(art, 'poster', {
get: () => {
try {
return $poster.style.backgroundImage.match(/"(.*)"/)[1]
return $poster.style.backgroundImage.match(/"(.*)"/)![1]!
}
catch {
return ''
}
},
set(url) {
set(url: string) {
setStyle($poster, 'backgroundImage', `url(${url})`)
},
})
@@ -0,0 +1,43 @@
import type { Thumbnails } from '../../types/option'
import type { QualityItem } from '../control/types'
import type { PipProperty } from '../display/types'
import type { RectState } from '../media/hosts'
import type { Position, SeekMethods, StateProperty } from '../media/playback'
import type { SwitchMethods } from '../source/types'
import type { AttributeMethods } from './attrMix'
import type { CssVariableMethods } from './cssVarMix'
// Runtime descriptors installed by Player, distinct from each installer's inputs.
// Declaring these on the entry must never emit class fields before installation.
export interface PlayerProperties extends AttributeMethods, CssVariableMethods, Position, SeekMethods, SwitchMethods, RectState, PipProperty, StateProperty {
get url(): string | null
set url(value: string)
play: () => Promise<unknown>
pause: () => unknown
toggle: () => unknown
volume: number
muted: boolean
playbackRate: number
aspectRatio: string
flip: string
fullscreen?: boolean
fullscreenWeb: boolean
mini: boolean
readonly loaded: number
readonly loadedTime: number
readonly played: number
readonly playing: boolean
autoSize: () => void
autoHeight: () => void
poster: string
theme: string
type: string
subtitleOffset: number
airplay: () => void
get quality(): undefined
set quality(value: QualityItem[])
thumbnails: Thumbnails
getDataURL: () => Promise<string>
getBlobUrl: () => Promise<string>
screenshot: (name?: string) => Promise<string>
}
+5 -4
View File
@@ -1,12 +1,13 @@
import type { ComponentHost, EntryOption } from '../component/types'
import type { QualityItem } from '../control/types'
import type { ComponentHost } from '../component/types'
import type { ControlOption, QualityItem } from '../control/types'
import type { NoticeSink } from '../notice'
import { captureSelection } from '../component/selection'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export interface QualityHost extends ComponentHost {
controls: { update: (option: EntryOption<QualityHost>) => unknown }
notice: { set show(message: string) }
controls: { update: (option: ControlOption) => unknown }
notice: NoticeSink
i18n: { get: (key: string) => string }
switchQuality: (url: string) => Promise<unknown>
}
@@ -1,4 +1,5 @@
import type { CaptureMedia } from '../capture/frame'
import type { NoticeSink } from '../notice'
import { captureBlobUrl, drawFrame } from '../capture/frame'
import { isClosing } from '../lifecycle/instance'
import { captureSource } from '../source/operation'
@@ -6,7 +7,7 @@ import { def, download, secondToTime } from '../utils'
export interface ScreenshotHost {
template: { $video: CaptureMedia }
notice: { set show(value: unknown) }
notice: NoticeSink
getDataURL: () => Promise<string>
emit: (name: 'screenshot', dataUri: string) => unknown
}
@@ -1,11 +1,12 @@
import type { CssVariableMethods } from './cssVarMix'
import { def } from '../utils'
export default function themeMix(art) {
export default function themeMix(art: CssVariableMethods): void {
def(art, 'theme', {
get() {
return art.cssVar('--art-theme')
},
set(theme) {
set(theme: string) {
art.cssVar('--art-theme', theme)
},
})
@@ -1,11 +1,11 @@
import { def } from '../utils'
export default function typeMix(art) {
export default function typeMix(art: { option: { type: string } }): void {
def(art, 'type', {
get() {
return art.option.type
},
set(type) {
set(type: string) {
art.option.type = type
},
})