import { addClass, append, createElement, def, errorHandle, getRect, has, includeFromEvent, inverseClass, isMobile, remove, replaceElement, setStyle, } from '../utils' import Component from '../utils/component' import aspectRatio from './aspectRatio' import flip from './flip' import playbackRate from './playbackRate' import subtitleOffset from './subtitleOffset' export default class Setting extends Component { constructor(art) { super(art) const { option, controls, template: { $setting }, } = art this.name = 'setting' this.$parent = $setting this.id = 0 this.active = null this.cache = new Map() this.option = [...this.builtin, ...option.settings] if (option.setting) { this.format() this.render() art.on('blur', () => { if (this.show) { this.show = false this.render() } }) art.on('focus', (event) => { const isControl = includeFromEvent(event, controls.setting) const isSetting = includeFromEvent(event, this.$parent) if (this.show && !isControl && !isSetting) { this.show = false this.render() } }) art.on('resize', () => this.resize()) } } get builtin() { const result = [] const { option } = this.art if (option.playbackRate) { result.push(playbackRate(this.art)) } if (option.aspectRatio) { result.push(aspectRatio(this.art)) } if (option.flip) { result.push(flip(this.art)) } if (option.subtitleOffset) { result.push(subtitleOffset(this.art)) } return result } traverse(callback, option = this.option) { for (let index = 0; index < option.length; index++) { const item = option[index] callback(item) if (item.selector?.length) { this.traverse(callback, item.selector) } } } check(target) { if (!target) { return } target.$parent.tooltip = target.html this.traverse((item) => { item.default = item === target if (item.default && item.$item) { inverseClass(item.$item, 'art-current') } }, target.$option) this.render(target.$parents) } format(option = this.option, parent, parents, names = []) { for (let index = 0; index < option.length; index++) { const item = option[index] if (item?.name) { errorHandle(!names.includes(item.name), `The [${item.name}] already exists in [setting]`) names.push(item.name) } else { item.name = `setting-${this.id++}` } if (!item.$formatted) { def(item, '$parent', { get: () => parent, }) def(item, '$parents', { get: () => parents, }) def(item, '$option', { get: () => option, }) const $events = [] def(item, '$events', { get: () => $events, }) def(item, '$formatted', { get: () => true, }) } this.format(item.selector || [], item, option, names) } this.option = option } find(name = '') { let result = null this.traverse((item) => { if (item.name === name) { result = item } }) return result } resize() { const { controls, constructor: { SETTING_WIDTH, SETTING_ITEM_HEIGHT }, template: { $player, $setting }, } = this.art if (controls.setting && this.show) { const settingWidth = this.active[0]?.$parent?.width || SETTING_WIDTH const { left: controlLeft, width: controlWidth } = getRect(controls.setting) const { left: playerLeft, width: playerWidth } = getRect($player) const settingLeft = controlLeft - playerLeft + controlWidth / 2 - settingWidth / 2 const settingHeight = this.active === this.option ? this.active.length * SETTING_ITEM_HEIGHT : (this.active.length + 1) * SETTING_ITEM_HEIGHT setStyle($setting, 'height', `${settingHeight}px`) setStyle($setting, 'width', `${settingWidth}px`) if (this.art.isRotate || isMobile) return if (settingLeft + settingWidth > playerWidth) { setStyle($setting, 'left', null) setStyle($setting, 'right', null) } else { setStyle($setting, 'left', `${settingLeft}px`) setStyle($setting, 'right', 'auto') } } } inactivate(item) { for (let index = 0; index < item.$events.length; index++) { this.art.events.remove(item.$events[index]) } item.$events.length = 0 } remove(name) { const item = this.find(name) errorHandle(item, `Can't find [${name}] in the [setting]`) const index = item.$option.indexOf(item) item.$option.splice(index, 1) this.inactivate(item) if (item.$item) remove(item.$item) this.render() } update(target) { const item = this.find(target.name) if (item) { this.inactivate(item) Object.assign(item, target) this.format() this.createItem(item, true) this.render() return item } else { return this.add(target) } } add(item, option = this.option) { option.push(item) this.format() this.createItem(item) this.render() return item } createHeader(item) { if (!this.cache.has(item.$option)) return const $panel = this.cache.get(item.$option) const { proxy, icons: { arrowLeft }, constructor: { SETTING_ITEM_HEIGHT }, } = this.art const $item = createElement('div') setStyle($item, 'height', `${SETTING_ITEM_HEIGHT}px`) addClass($item, 'art-setting-item') addClass($item, 'art-setting-item-back') const $left = append($item, '
') const $icon = createElement('div') addClass($icon, 'art-setting-item-left-icon') append($icon, arrowLeft) append($left, $icon) append($left, item.$parent.html) const event = proxy($item, 'click', () => this.render(item.$parents)) item.$parent.$events.push(event) append($panel, $item) } createItem(item, isUpdate = false) { if (!this.cache.has(item.$option)) return const $panel = this.cache.get(item.$option) const oldItem = item.$item let type = 'selector' if (has(item, 'switch')) { type = 'switch' } if (has(item, 'range')) { type = 'range' } if (has(item, 'onClick')) { type = 'button' } const { icons, proxy, constructor } = this.art const $item = createElement('div') addClass($item, 'art-setting-item') setStyle($item, 'height', `${constructor.SETTING_ITEM_HEIGHT}px`) $item.dataset.name = item.name || '' $item.dataset.value = item.value || '' const $left = append($item, '') const $right = append($item, '') const $icon = createElement('div') addClass($icon, 'art-setting-item-left-icon') switch (type) { case 'button': case 'switch': case 'range': append($icon, item.icon || icons.config) break case 'selector': if (item.selector?.length) { append($icon, item.icon || icons.config) } else { append($icon, icons.check) } break default: break } append($left, $icon) def(item, '$icon', { configurable: true, get: () => $icon, }) def(item, 'icon', { configurable: true, get() { return $icon.innerHTML }, set(value) { $icon.innerHTML = '' append($icon, value) }, }) const $html = createElement('div') addClass($html, 'art-setting-item-left-text') append($html, item.html || '') append($left, $html) def(item, '$html', { configurable: true, get: () => $html, }) def(item, 'html', { configurable: true, get() { return $html.innerHTML }, set(value) { $html.innerHTML = '' append($html, value) }, }) const $tooltip = createElement('div') addClass($tooltip, 'art-setting-item-right-tooltip') append($tooltip, item.tooltip || '') append($right, $tooltip) def(item, '$tooltip', { configurable: true, get: () => $tooltip, }) def(item, 'tooltip', { configurable: true, get() { return $tooltip.innerHTML }, set(value) { $tooltip.innerHTML = '' append($tooltip, value) }, }) switch (type) { case 'switch': { const $switch = createElement('div') addClass($switch, 'art-setting-item-right-icon') const $switchOn = append($switch, icons.switchOn) const $switchOff = append($switch, icons.switchOff) setStyle(item.switch ? $switchOff : $switchOn, 'display', 'none') append($right, $switch) def(item, '$switch', { configurable: true, get: () => $switch, }) let $switchValue = item.switch def(item, 'switch', { configurable: true, get: () => $switchValue, set(value) { $switchValue = value if (value) { setStyle($switchOff, 'display', 'none') setStyle($switchOn, 'display', null) } else { setStyle($switchOff, 'display', null) setStyle($switchOn, 'display', 'none') } }, }) break } case 'range': { const $state = createElement('div') addClass($state, 'art-setting-item-right-icon') const $range = append($state, '') $range.value = item.range[0] $range.min = item.range[1] $range.max = item.range[2] $range.step = item.range[3] addClass($range, 'art-setting-range') append($right, $state) def(item, '$range', { configurable: true, get: () => $range, }) let $rangeValue = [...item.range] def(item, 'range', { configurable: true, get: () => $rangeValue, set(value) { $rangeValue = [...value] $range.value = value[0] $range.min = value[1] $range.max = value[2] $range.step = value[3] }, }) } break case 'selector': if (item.selector?.length) { const $state = createElement('div') addClass($state, 'art-setting-item-right-icon') append($state, icons.arrowRight) append($right, $state) } break default: break } switch (type) { case 'switch': { if (item.onSwitch) { const event = proxy($item, 'click', async (event) => { item.switch = await item.onSwitch.call(this.art, item, $item, event) }) item.$events.push(event) } break } case 'range': { if (item.$range) { if (item.onRange) { const event = proxy(item.$range, 'change', async (event) => { item.range[0] = item.$range.valueAsNumber item.tooltip = await item.onRange.call(this.art, item, $item, event) }) item.$events.push(event) } if (item.onChange) { const event = proxy(item.$range, 'input', async (event) => { item.range[0] = item.$range.valueAsNumber item.tooltip = await item.onChange.call(this.art, item, $item, event) }) item.$events.push(event) } } break } case 'selector': { const event = proxy($item, 'click', async (event) => { if (item.selector?.length) { this.render(item.selector) } else { this.check(item) if (item.$parent.onSelect) { item.$parent.tooltip = await item.$parent.onSelect.call(this.art, item, $item, event) } } }) item.$events.push(event) if (item.default) { addClass($item, 'art-current') } } break case 'button': if (item.onClick) { const event = proxy($item, 'click', async (event) => { item.tooltip = await item.onClick.call(this.art, item, $item, event) }) item.$events.push(event) } break default: break } def(item, '$item', { configurable: true, get: () => $item, }) if (isUpdate) { replaceElement($item, oldItem) } else { append($panel, $item) } if (item.mounted) { setTimeout(() => item.mounted.call(this.art, item.$item, item), 0) } } render(option = this.option) { this.active = option if (this.cache.has(option)) { const $panel = this.cache.get(option) inverseClass($panel, 'art-current') } else { const $panel = createElement('div') this.cache.set(option, $panel) addClass($panel, 'art-setting-panel') append(this.$parent, $panel) inverseClass($panel, 'art-current') if (option[0]?.$parent) { this.createHeader(option[0]) } for (let index = 0; index < option.length; index++) { this.createItem(option[index]) } } this.resize() } }