mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-10 04:36:15 -08:00
215 lines
6.3 KiB
TypeScript
215 lines
6.3 KiB
TypeScript
import type { SettingItem, SettingManager } from './types'
|
|
import { getScope, isClosing } from '../lifecycle/instance'
|
|
import { ResourceCleanupError } from '../lifecycle/scope'
|
|
import captureTemplate from '../lifecycle/template-rollback'
|
|
import { inverseClass } from '../utils/dom'
|
|
import { pauseSettingScope } from './activity'
|
|
import { captureSettingItem } from './checkpoint'
|
|
import { captureSettingFocus } from './keyboard-focus'
|
|
import { releaseSettingPanel, settingPanelScope } from './panels'
|
|
import { cancelSettingAdd } from './registration'
|
|
import { suspendSettingItem } from './resources'
|
|
|
|
const pending = new WeakMap<SettingItem, () => void>()
|
|
|
|
function finish(cleanups: (() => void)[]): void {
|
|
const errors: unknown[] = []
|
|
for (const cleanup of cleanups) {
|
|
try {
|
|
cleanup()
|
|
}
|
|
catch (error) {
|
|
if (error instanceof ResourceCleanupError)
|
|
errors.push(...error.errors)
|
|
else
|
|
errors.push(error)
|
|
}
|
|
}
|
|
if (errors.length)
|
|
throw new ResourceCleanupError(errors)
|
|
}
|
|
|
|
function suspendTree(setting: SettingManager, root: SettingItem): { items: Set<SettingItem>, resume: () => void, dispose: () => void } {
|
|
const items = new Set<SettingItem>()
|
|
const panels = new Set<HTMLDivElement>()
|
|
const collect = (item: SettingItem) => {
|
|
if (items.has(item))
|
|
return
|
|
items.add(item)
|
|
if (item.selector) {
|
|
const panel = setting.cache.get(item.selector)
|
|
if (panel)
|
|
panels.add(panel)
|
|
item.selector.forEach(collect)
|
|
}
|
|
}
|
|
collect(root)
|
|
const suspended: NonNullable<ReturnType<typeof suspendSettingItem>>[] = []
|
|
const unpause: (() => void)[] = []
|
|
try {
|
|
for (const item of items) {
|
|
const suspension = suspendSettingItem(setting.art, item)
|
|
if (suspension)
|
|
suspended.push(suspension)
|
|
}
|
|
for (const panel of panels)
|
|
unpause.push(pauseSettingScope(settingPanelScope(panel)))
|
|
}
|
|
catch (error) {
|
|
try {
|
|
finish([...suspended.reverse().map(item => item.resume), ...unpause])
|
|
}
|
|
catch (cleanupError) {
|
|
console.warn('ArtPlayer setting restore failed:', cleanupError)
|
|
}
|
|
throw error
|
|
}
|
|
let active = true
|
|
return {
|
|
items,
|
|
resume() {
|
|
if (!active)
|
|
return
|
|
active = false
|
|
finish([...suspended.reverse().map(item => item.resume), ...unpause])
|
|
},
|
|
dispose() {
|
|
if (!active)
|
|
return
|
|
active = false
|
|
finish([...suspended.reverse().map(item => item.dispose), ...Array.from(panels).reverse().map(panel => () => releaseSettingPanel(panel)), ...unpause])
|
|
},
|
|
}
|
|
}
|
|
|
|
// A newer operation restores the pending one before applying its own changes.
|
|
export function cancelSettingUpdate(item: SettingItem): void {
|
|
pending.get(item)?.()
|
|
}
|
|
|
|
export function captureSettingUpdate(item: SettingItem): () => boolean {
|
|
const operation = pending.get(item)
|
|
return () => !operation || pending.get(item) === operation
|
|
}
|
|
|
|
function assignSetting(item: SettingItem, target: SettingItem, current: () => boolean): void {
|
|
for (const key of Reflect.ownKeys(target)) {
|
|
if (!current())
|
|
return
|
|
const descriptor = Object.getOwnPropertyDescriptor(target, key)
|
|
if (!descriptor?.enumerable || !current())
|
|
continue
|
|
const value: unknown = Reflect.get(target, key)
|
|
if (!current())
|
|
return
|
|
if (!Reflect.set(item, key, value))
|
|
throw new TypeError(`Cannot assign setting item property [${String(key)}]`)
|
|
}
|
|
}
|
|
|
|
export function updateSetting(setting: SettingManager, target: SettingItem): SettingItem {
|
|
const item = setting.find(target.name)
|
|
if (isClosing(setting.art))
|
|
return item || target
|
|
if (!item)
|
|
return setting.add(target)
|
|
const restoreFocus = captureSettingFocus(setting)
|
|
cancelSettingAdd(item)
|
|
cancelSettingUpdate(item)
|
|
const restoreItem = captureSettingItem(item)
|
|
const element = item.$item
|
|
const parent = element?.parentNode
|
|
const next = element?.nextSibling
|
|
const restoreTemplate = element ? captureTemplate(element) : undefined
|
|
const previous = setting.active
|
|
const layout = ['height', 'width', 'left', 'right'].map(key => [key, setting.$parent.style.getPropertyValue(key), setting.$parent.style.getPropertyPriority(key)] as const)
|
|
const tree = suspendTree(setting, item)
|
|
let active = true
|
|
let rendered = false
|
|
const cancel = () => {
|
|
if (!active)
|
|
return
|
|
active = false
|
|
for (const affected of tree.items) {
|
|
if (pending.get(affected) === cancel)
|
|
pending.delete(affected)
|
|
}
|
|
if (isClosing(setting.art)) {
|
|
tree.dispose()
|
|
return
|
|
}
|
|
const replacement = item.$item
|
|
try {
|
|
finish([
|
|
tree.resume,
|
|
() => {
|
|
if (replacement && replacement !== element)
|
|
replacement.remove()
|
|
},
|
|
restoreItem,
|
|
() => restoreTemplate?.(),
|
|
() => {
|
|
if (parent && element)
|
|
parent.insertBefore(element, next?.parentNode === parent ? next : null)
|
|
},
|
|
() => setting.format(),
|
|
() => {
|
|
if (rendered) {
|
|
setting.active = previous
|
|
if (previous && setting.cache.has(previous))
|
|
inverseClass(setting.cache.get(previous)!, 'art-current')
|
|
for (const [key, value, priority] of layout)
|
|
setting.$parent.style.setProperty(key, value, priority)
|
|
}
|
|
},
|
|
])
|
|
restoreFocus(element)
|
|
}
|
|
catch (error) {
|
|
console.warn('ArtPlayer setting restore failed:', error)
|
|
}
|
|
}
|
|
const current = () => active && !isClosing(setting.art) && pending.get(item) === cancel
|
|
for (const affected of tree.items)
|
|
pending.set(affected, cancel)
|
|
const release = getScope(setting.art).add(() => {
|
|
active = false
|
|
for (const affected of tree.items) {
|
|
if (pending.get(affected) === cancel)
|
|
pending.delete(affected)
|
|
}
|
|
tree.dispose()
|
|
})
|
|
try {
|
|
assignSetting(item, target, current)
|
|
if (!current())
|
|
return item
|
|
setting.format()
|
|
if (!current())
|
|
return item
|
|
setting.createItem(item, true)
|
|
if (!current())
|
|
return item
|
|
rendered = true
|
|
setting.render()
|
|
if (!current())
|
|
return item
|
|
active = false
|
|
for (const affected of tree.items) {
|
|
if (pending.get(affected) === cancel)
|
|
pending.delete(affected)
|
|
}
|
|
tree.dispose()
|
|
restoreFocus(item.$item)
|
|
return item
|
|
}
|
|
catch (error) {
|
|
if (current())
|
|
cancel()
|
|
throw error
|
|
}
|
|
finally {
|
|
release()
|
|
}
|
|
}
|