refactor(core): CORE-16 type display modes and own fullscreen lifetimes

This commit is contained in:
Harvey Zhao committed 2026-09-11 09:42:30 +08:00
1 parent 1b83081e8f
commit 88aef830df
81 files changed
+33897 -1503

No files matched your search

@@ -1,47 +0,0 @@
import { def, setStyle } from '../utils'
export default function aspectRatioMix(art) {
const {
i18n,
notice,
template: { $video, $player },
} = art
def(art, 'aspectRatio', {
get() {
return $player.dataset.aspectRatio || 'default'
},
set(ratio) {
if (!ratio)
ratio = 'default'
if (ratio === 'default') {
setStyle($video, 'width', null)
setStyle($video, 'height', null)
setStyle($video, 'margin', null)
delete $player.dataset.aspectRatio
}
else {
const ratioArray = ratio.split(':').map(Number)
const { clientWidth, clientHeight } = $player
const playerRatio = clientWidth / clientHeight
const setupRatio = ratioArray[0] / ratioArray[1]
if (playerRatio > setupRatio) {
setStyle($video, 'width', `${setupRatio * clientHeight}px`)
setStyle($video, 'height', '100%')
setStyle($video, 'margin', '0 auto')
}
else {
setStyle($video, 'width', '100%')
setStyle($video, 'height', `${clientWidth / setupRatio}px`)
setStyle($video, 'margin', 'auto 0')
}
$player.dataset.aspectRatio = ratio
}
notice.show = `${i18n.get('Aspect Ratio')}: ${ratio === 'default' ? i18n.get('Default') : ratio}`
art.emit('aspectRatio', ratio)
},
})
}
@@ -0,0 +1,44 @@
import type { AspectRatioHost } from '../display/sizing-types'
import { containSize } from '../display/sizing'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export default function aspectRatioMix(art: AspectRatioHost): void {
const { i18n, notice, template: { $video, $player } } = art
def(art, 'aspectRatio', {
get: () => $player.dataset.aspectRatio || 'default',
set(ratio: string) {
if (isClosing(art))
return
if (!ratio)
ratio = 'default'
if (ratio === 'default') {
$video.style.width = ''
$video.style.height = ''
$video.style.margin = ''
delete $player.dataset.aspectRatio
}
else {
// Keep the legacy split/Number coercion and dataset even for invalid ratios.
const parts = ratio.split(':').map(Number)
const size = containSize({ width: $player.clientWidth, height: $player.clientHeight }, parts[0]! / parts[1]!)
if (isClosing(art))
return
if (size?.axis === 'width') {
$video.style.width = `${size.width}px`
$video.style.height = '100%'
$video.style.margin = '0 auto'
}
else if (size) {
$video.style.width = '100%'
$video.style.height = `${size.height}px`
$video.style.margin = 'auto 0'
}
$player.dataset.aspectRatio = ratio
}
notice.show = `${i18n.get('Aspect Ratio')}: ${ratio === 'default' ? i18n.get('Default') : ratio}`
if (!isClosing(art))
art.emit('aspectRatio', ratio)
},
})
}
@@ -1,17 +0,0 @@
import { def, setStyle } from '../utils'
export default function autoHeightMix(art) {
const {
template: { $container, $video },
} = art
def(art, 'autoHeight', {
value() {
const { clientWidth } = $container
const { videoHeight, videoWidth } = $video
const height = videoHeight * (clientWidth / videoWidth)
setStyle($container, 'height', `${height}px`)
art.emit('autoHeight', height)
},
})
}
@@ -0,0 +1,19 @@
import type { AutoHeightHost } from '../display/sizing-types'
import { proportionalHeight } from '../display/sizing'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export default function autoHeightMix(art: AutoHeightHost): void {
const { $container, $video } = art.template
def(art, 'autoHeight', {
value() {
if (isClosing(art))
return
const height = proportionalHeight($container.clientWidth, { width: $video.videoWidth, height: $video.videoHeight })
if (height === undefined || isClosing(art))
return
$container.style.height = `${height}px`
art.emit('autoHeight', height)
},
})
}
@@ -1,28 +0,0 @@
import { def, getRect, setStyle } from '../utils'
export default function autoSizeMix(art) {
const { $container, $player, $video } = art.template
def(art, 'autoSize', {
value() {
const { videoWidth, videoHeight } = $video
const { width, height } = getRect($container)
const videoRatio = videoWidth / videoHeight
const containerRatio = width / height
if (containerRatio > videoRatio) {
const percentage = ((height * videoRatio) / width) * 100
setStyle($player, 'width', `${percentage}%`)
setStyle($player, 'height', '100%')
}
else {
const percentage = (width / videoRatio / height) * 100
setStyle($player, 'width', '100%')
setStyle($player, 'height', `${percentage}%`)
}
art.emit('autoSize', {
width: art.width,
height: art.height,
})
},
})
}
@@ -0,0 +1,30 @@
import type { AutoSizeHost } from '../display/sizing-types'
import { containSize, positiveSize } from '../display/sizing'
import { isClosing } from '../lifecycle/instance'
import { def, getRect } from '../utils'
export default function autoSizeMix(art: AutoSizeHost): void {
const { $container, $player, $video } = art.template
def(art, 'autoSize', {
value() {
if (isClosing(art))
return
const media = { width: $video.videoWidth, height: $video.videoHeight }
if (!positiveSize(media))
return
const container = getRect($container)
const size = containSize(container, media.width / media.height)
if (!size || isClosing(art))
return
if (size.axis === 'width') {
$player.style.width = `${(size.width / container.width) * 100}%`
$player.style.height = '100%'
}
else {
$player.style.width = '100%'
$player.style.height = `${(size.height / container.height) * 100}%`
}
art.emit('autoSize', { width: art.width, height: art.height })
},
})
}
-28
View File
@@ -1,28 +0,0 @@
import { capitalize, def } from '../utils'
export default function flipMix(art) {
const {
template: { $player },
i18n,
notice,
} = art
def(art, 'flip', {
get() {
return $player.dataset.flip || 'normal'
},
set(flip) {
if (!flip)
flip = 'normal'
if (flip === 'normal') {
delete $player.dataset.flip
}
else {
$player.dataset.flip = flip
}
notice.show = `${i18n.get('Video Flip')}: ${i18n.get(capitalize(flip))}`
art.emit('flip', flip)
},
})
}
+23
View File
@@ -0,0 +1,23 @@
import type { FlipHost } from '../display/sizing-types'
import { isClosing } from '../lifecycle/instance'
import { capitalize, def } from '../utils'
export default function flipMix(art: FlipHost): void {
const { template: { $player }, i18n, notice } = art
def(art, 'flip', {
get: () => $player.dataset.flip || 'normal',
set(flip: string) {
if (isClosing(art))
return
if (!flip)
flip = 'normal'
if (flip === 'normal')
delete $player.dataset.flip
else
$player.dataset.flip = flip
notice.show = `${i18n.get('Video Flip')}: ${i18n.get(capitalize(flip))}`
if (!isClosing(art))
art.emit('flip', flip)
},
})
}
@@ -1,88 +0,0 @@
import screenfull from '../libs/screenfull'
import { addClass, def, get, removeClass } from '../utils'
export default function fullscreenMix(art) {
const {
i18n,
notice,
template: { $video, $player },
} = art
const nativeScreenfull = (art) => {
screenfull.on('change', () => {
art.emit('fullscreen', screenfull.isFullscreen)
if (screenfull.isFullscreen) {
art.state = 'fullscreen'
addClass($player, 'art-fullscreen')
}
else {
removeClass($player, 'art-fullscreen')
}
art.emit('resize')
})
screenfull.on('error', (event) => {
art.emit('fullscreenError', event)
})
def(art, 'fullscreen', {
get() {
return screenfull.isFullscreen
},
async set(value) {
if (value) {
await screenfull.request($player)
}
else {
await screenfull.exit()
}
},
})
}
const webkitScreenfull = (art) => {
art.on('document:webkitfullscreenchange', () => {
art.emit('fullscreen', art.fullscreen)
art.emit('resize')
})
def(art, 'fullscreen', {
get() {
return document.fullscreenElement === $video
},
set(value) {
if (value) {
art.state = 'fullscreen'
$video.webkitEnterFullscreen()
}
else {
$video.webkitExitFullscreen()
}
},
})
}
art.once('video:loadedmetadata', () => {
if (screenfull.isEnabled) {
nativeScreenfull(art)
}
else if ($video.webkitSupportsFullscreen) {
webkitScreenfull(art)
}
else {
def(art, 'fullscreen', {
get() {
return false
},
set() {
notice.show = i18n.get('Fullscreen Not Supported')
},
})
}
// Asynchronous setting
def(art, 'fullscreen', get(art, 'fullscreen'))
})
}
@@ -0,0 +1,35 @@
import type { FullscreenHost } from '../display/types'
import { fullscreenAdapter } from '../display/fullscreen-adapter'
import { nativeFullscreen } from '../display/native-fullscreen'
import { videoFullscreen } from '../display/video-fullscreen'
import { getScope, isClosing } from '../lifecycle/instance'
import { def, get } from '../utils'
export default function fullscreenMix(art: FullscreenHost): void {
const { i18n, notice, template: { $video, $player } } = art
const metadata = () => {
if (isClosing(art))
return
const adapter = fullscreenAdapter($player)
if (adapter) {
def(art, 'fullscreen', nativeFullscreen(art, adapter))
}
else if ($video.webkitSupportsFullscreen && typeof $video.webkitEnterFullscreen === 'function' && typeof $video.webkitExitFullscreen === 'function') {
def(art, 'fullscreen', videoFullscreen(art))
}
else {
def(art, 'fullscreen', {
get: () => false,
set() {
if (!isClosing(art))
notice.show = i18n.get('Fullscreen Not Supported')
},
})
}
def(art, 'fullscreen', get(art, 'fullscreen')!)
}
art.once('video:loadedmetadata', metadata)
getScope(art).add(() => {
art.off('video:loadedmetadata', metadata)
})
}
@@ -1,41 +0,0 @@
import { addClass, append, def, hasClass, removeClass, setStyle } from '../utils'
export default function fullscreenWebMix(art) {
const {
constructor,
template: { $container, $player },
} = art
let cssText = ''
def(art, 'fullscreenWeb', {
get() {
return hasClass($player, 'art-fullscreen-web')
},
set(value) {
if (value) {
cssText = $player.style.cssText
if (constructor.FULLSCREEN_WEB_IN_BODY) {
append(document.body, $player)
}
art.state = 'fullscreenWeb'
setStyle($player, 'width', '100%')
setStyle($player, 'height', '100%')
addClass($player, 'art-fullscreen-web')
art.emit('fullscreenWeb', true)
}
else {
if (constructor.FULLSCREEN_WEB_IN_BODY) {
append($container, $player)
}
if (cssText) {
$player.style.cssText = cssText
cssText = ''
}
removeClass($player, 'art-fullscreen-web')
art.emit('fullscreenWeb', false)
}
art.emit('resize')
},
})
}
@@ -0,0 +1,13 @@
import type { WebFullscreenHost, WebFullscreenProperty } from '../display/types'
import { webFullscreen } from '../display/web-fullscreen'
import { def } from '../utils'
export default function fullscreenWebMix(art: WebFullscreenHost): asserts art is WebFullscreenHost & WebFullscreenProperty {
const setFullscreen = webFullscreen(art)
def(art, 'fullscreenWeb', {
get(): boolean {
return art.template.$player.classList.contains('art-fullscreen-web')
},
set: setFullscreen,
})
}
-134
View File
@@ -1,134 +0,0 @@
import { addClass, append, createElement, def, getRect, hasClass, isInViewport, removeClass, setStyle, silencePromise } from '../utils'
export default function miniMix(art) {
const {
icons,
proxy,
storage,
template: { $player, $video },
} = art
let isDragging = false
let lastPageX = 0
let lastPageY = 0
function hideMini() {
const { $mini } = art.template
if ($mini) {
removeClass($player, 'art-mini')
setStyle($mini, 'display', 'none')
$player.prepend($video)
art.emit('mini', false)
}
}
function initState($play, $pause) {
if (art.playing) {
setStyle($play, 'display', `none`)
setStyle($pause, 'display', `flex`)
}
else {
setStyle($play, 'display', `flex`)
setStyle($pause, 'display', `none`)
}
}
function createMini() {
const { $mini } = art.template
if ($mini) {
append($mini, $video)
return setStyle($mini, 'display', 'flex')
}
else {
const $mini = createElement('div')
addClass($mini, 'art-mini-popup')
append(document.body, $mini)
art.template.$mini = $mini
append($mini, $video)
const $close = append($mini, `<div class="art-mini-close"></div>`)
append($close, icons.close)
proxy($close, 'click', hideMini)
const $state = append($mini, `<div class="art-mini-state"></div>`)
const $play = append($state, icons.play)
const $pause = append($state, icons.pause)
proxy($play, 'click', () => silencePromise(art.play()))
proxy($pause, 'click', () => art.pause())
initState($play, $pause)
art.on('video:playing', () => initState($play, $pause))
art.on('video:pause', () => initState($play, $pause))
art.on('video:timeupdate', () => initState($play, $pause))
proxy($mini, 'mousedown', (event) => {
isDragging = event.button === 0
lastPageX = event.pageX
lastPageY = event.pageY
})
art.on('document:mousemove', (event) => {
if (isDragging) {
addClass($mini, 'art-mini-dragging')
const x = event.pageX - lastPageX
const y = event.pageY - lastPageY
setStyle($mini, 'transform', `translate(${x}px, ${y}px)`)
}
})
art.on('document:mouseup', () => {
if (isDragging) {
isDragging = false
removeClass($mini, 'art-mini-dragging')
const rect = getRect($mini)
storage.set('left', rect.left)
storage.set('top', rect.top)
setStyle($mini, 'left', `${rect.left}px`)
setStyle($mini, 'top', `${rect.top}px`)
setStyle($mini, 'transform', null)
}
})
return $mini
}
}
function initMini() {
const { $mini } = art.template
const rect = getRect($mini)
const top = window.innerHeight - rect.height - 50
const left = window.innerWidth - rect.width - 50
storage.set('top', top)
storage.set('left', left)
setStyle($mini, 'top', `${top}px`)
setStyle($mini, 'left', `${left}px`)
}
def(art, 'mini', {
get() {
return hasClass($player, 'art-mini')
},
set(value) {
if (value) {
art.state = 'mini'
addClass($player, 'art-mini')
const $mini = createMini()
const top = storage.get('top')
const left = storage.get('left')
if (typeof top === 'number' && typeof left === 'number') {
setStyle($mini, 'top', `${top}px`)
setStyle($mini, 'left', `${left}px`)
if (!isInViewport($mini)) {
initMini()
}
}
else {
initMini()
}
art.emit('mini', true)
}
else {
hideMini()
}
},
})
}
+7
View File
@@ -0,0 +1,7 @@
import type { MiniHost, MiniProperty } from '../display/types'
import { mini } from '../display/mini'
import { def } from '../utils'
export default function miniMix(art: MiniHost): asserts art is MiniHost & MiniProperty {
def(art, 'mini', mini(art))
}
-87
View File
@@ -1,87 +0,0 @@
import { def } from '../utils'
function nativePip(art) {
const {
template: { $video },
proxy,
notice,
} = art
$video.disablePictureInPicture = false
def(art, 'pip', {
get() {
return document.pictureInPictureElement
},
set(value) {
if (value) {
art.state = 'pip'
$video.requestPictureInPicture().catch((err) => {
notice.show = err
throw err
})
}
else {
document.exitPictureInPicture().catch((err) => {
notice.show = err
throw err
})
}
},
})
proxy($video, 'enterpictureinpicture', () => {
art.emit('pip', true)
})
proxy($video, 'leavepictureinpicture', () => {
art.emit('pip', false)
})
}
function webkitPip(art) {
const { $video } = art.template
$video.webkitSetPresentationMode('inline')
def(art, 'pip', {
get() {
return $video.webkitPresentationMode === 'picture-in-picture'
},
set(value) {
if (value) {
art.state = 'pip'
$video.webkitSetPresentationMode('picture-in-picture')
art.emit('pip', true)
}
else {
$video.webkitSetPresentationMode('inline')
art.emit('pip', false)
}
},
})
}
export default function pipMix(art) {
const {
i18n,
notice,
template: { $video },
} = art
if (document.pictureInPictureEnabled) {
nativePip(art)
}
else if ($video.webkitSupportsPresentationMode) {
webkitPip(art)
}
else {
def(art, 'pip', {
get() {
return false
},
set() {
notice.show = i18n.get('PIP Not Supported')
},
})
}
}
+25
View File
@@ -0,0 +1,25 @@
import type { PipHost, PipProperty } from '../display/types'
import { nativePip } from '../display/native-pip'
import { webkitPip } from '../display/webkit-pip'
import { isClosing } from '../lifecycle/instance'
import { def } from '../utils'
export default function pipMix(art: PipHost): asserts art is PipHost & PipProperty {
const { i18n, notice, template: { $video } } = art
const document = $video.ownerDocument
if (document.pictureInPictureEnabled && typeof $video.requestPictureInPicture === 'function' && typeof document.exitPictureInPicture === 'function') {
def(art, 'pip', nativePip(art))
}
else if (typeof $video.webkitSupportsPresentationMode === 'function' && typeof $video.webkitSetPresentationMode === 'function') {
def(art, 'pip', webkitPip(art))
}
else {
def(art, 'pip', {
get: () => false,
set() {
if (!isClosing(art))
notice.show = i18n.get('PIP Not Supported')
},
})
}
}