import Player, { $, isMobile } from '@oplayer/core' import { Icons } from '../functions/icons' import { icon, settingShown, tooltip } from '../style' import type { Setting, UIInterface } from '../types' import { activeCls, nextIcon, nextLabelText, panelCls, settingCls, settingItemCls, settingItemLeft, settingItemRight, subPanelCls, yesIcon, switcherCls, switcherContainer, backIcon, backRow, sliderCls } from './Setting.style' export const arrowSvg = (className = nextIcon) => `` // Selector Options const selectorOption = (name: string, icon: string = '') => `
${icon} ${name}
` const nexter = (name: string, icon: string = '') => `
${icon} ${name}
${arrowSvg()}
` const back = (name: string) => `
${arrowSvg(backIcon)} ${name}
` const switcher = (name: string, icon: string = '') => `
${icon} ${name}
` const normal = (name: string, icon: string = '') => `
${icon} ${name}
` export const slider = ({ name, icon = '', max = 1, min = 0, value = 0, step = 1 }: { name: string icon?: string min: number max: number value: number step: number }) => `
${icon} ${name}
` function createRow({ type, key, name, icon, default: selected, index, max, min, step, hasChildren }: Omit & { hasChildren?: boolean index?: number switcherLabe?: string }) { let $item: HTMLElement = $.create(`div.${settingItemCls}`, { 'data-key': key, role: type == 'option' ? 'menuitemradio' : 'menuitem', 'aria-haspopup': hasChildren ? 'menu' : false, 'aria-label': type || 'menuitem' }) const res = { $row: $item, $label: undefined as unknown as HTMLElement } switch (type) { case 'switcher': $item.innerHTML = switcher(name, icon) $item.setAttribute('aria-checked', selected || false) break case 'selector': $item.innerHTML = nexter(name, icon) res['$label'] = $item.querySelector('span[role="label"]')! break case 'back' as any: $item.innerHTML = back(name) break case 'slider': $item.innerHTML = slider({ name, max, min, icon, value: selected, step } as any) break case 'option': $item.innerHTML = selectorOption(name, icon) $item.setAttribute('aria-checked', selected || false) if (typeof index == 'number') { $item.setAttribute('data-index', index.toString()) } break default: if (hasChildren) { $item.innerHTML = nexter(name, icon) } else { $item.innerHTML = normal(name, icon) } break } return res } export type Panel = { $ref: HTMLElement key: string select?: Function // 全是选项才有 parent?: Panel } function createPanel( player: Player, panels: Panel[], setting: Setting[], options: { /** * 全是选项面板的用上一个面板的key */ key?: string name?: string target: HTMLElement parent?: Panel isSelectorOptionsPanel?: boolean parenOnChange?: Function } = {} as any ): Panel | void { if (!setting || setting.length == 0) return const { key: parentKey, target, parent, isSelectorOptionsPanel, name } = options let panel = {} as Panel let key: string = parentKey! || 'root' if (panels[0] && key == 'root') { panel = panels[0]! key = panels[0]!.key } else { //创建新的选项面板 panel.$ref = $.create(`div.${panels[0] && isSelectorOptionsPanel ? subPanelCls : panelCls}`, { 'data-key': key, role: 'menu' }) panel.key = key panels.push(panel) } panel.parent = parent const isRoot = panel.key == 'root' if (!isRoot) { // back row const { $row } = createRow({ name: name!, type: 'back' as any }) $row.addEventListener('click', () => { panel.$ref.classList.remove(activeCls) panel.parent?.$ref.classList.add(activeCls) }) $.render($row, panel.$ref) } for (let i = 0; i < setting.length; i++) { const { name, type, key, children, icon, default: selected, onChange, max, min, step, value } = setting[i]! const { $row, $label } = createRow( Object.assign( { name, type: isSelectorOptionsPanel ? 'option' : type, key: key, icon, default: selected, max, min, step, hasChildren: Boolean(children) }, !isRoot && isSelectorOptionsPanel && { index: i } ) ) $.render($row, panel.$ref) $.render(panel.$ref, target) if (children) { const nextIsSelectorOptionsPanel = type == 'selector' && children.every((it) => !Boolean(it.type) || it.type == 'option') const optionPanel = createPanel(player, panels, children, { key: key || name, target, parent: panel, isSelectorOptionsPanel: nextIsSelectorOptionsPanel, name, parenOnChange: onChange })! $row.addEventListener('click', () => { panel.$ref.classList.remove(activeCls) optionPanel.$ref.classList.add(activeCls) }) if (nextIsSelectorOptionsPanel) { const defaultSelected = children.find((it) => it.default) if (defaultSelected) { $label.innerText = defaultSelected.name } optionPanel.select = (i: number, shouldBeCallFn: boolean) => { if (i == -1) { optionPanel .$ref!.querySelector('[aria-checked=true]') ?.setAttribute('aria-checked', 'false') return } const $targets = optionPanel.$ref.querySelectorAll('[aria-checked]') if ($targets.item(i).getAttribute('aria-checked') != 'true') { $targets.forEach((it) => it.setAttribute('aria-checked', 'false')) $targets.item(i).setAttribute('aria-checked', 'true') const value = children[i] $label.innerText = value!.name if (shouldBeCallFn) onChange?.(value, { index: i, player }) } } optionPanel.$ref.addEventListener('click', (e) => { const target = e.target as HTMLDivElement if (target.hasAttribute('data-index')) { optionPanel.select!(+target.getAttribute('data-index')!, true) panel.$ref.classList.add(activeCls) optionPanel.$ref.classList.remove(activeCls) } }) } } else { if (type == 'switcher') { ;($row as any).select = function (shouldBeCallFn: boolean) { const selected = this.getAttribute('aria-checked') == 'true' this.setAttribute('aria-checked', `${!selected}`) if (shouldBeCallFn) onChange?.(!selected) } $row.addEventListener('click', () => ($row as any).select(true)) } else if (type == 'slider') { const $input = $row.querySelector('input')! $input.oninput = function (event: any) { event.target.setAttribute('value', event.target.value) } $input.onchange = function (event: any) { onChange?.(event.target.value) } // TODO: update methond } else { if (type == 'option' || (type == undefined && !isSelectorOptionsPanel)) { $row.addEventListener('click', () => (onChange || options.parenOnChange)?.(value)) } } } } return panel } export default function (it: UIInterface) { const { player, $root: $el, config } = it if (config.settings === false) return const position = config.theme.controller?.setting const options = config.settings || [] const isTop = config.theme.controller?.header && (position == 'top' || (isMobile && position == 'auto')) const $dom = $.create(`div.${settingCls(isTop ? 'top' : 'bottom')}`, { 'aria-label': 'Setting' }) let panels: Panel[] = [] let hasRendered = false const defaultSettingMap = { loop: { name: player.locales.get('Loop'), type: 'switcher', key: 'loop', icon: Icons.get('loop'), default: player.isLoop, onChange: (value: boolean) => player.setLoop(value) } } bootstrap(options.map((it) => (typeof it == 'string' ? defaultSettingMap[it] : it)) as Setting[]) function register(payload: Setting | Setting[]) { const _payload = Array.isArray(payload) ? payload : [payload] bootstrap( _payload .map((p) => { const repeated = panels.find((panel) => panel.key == p.key) if (repeated) { unregister(repeated.key) return } return p }) .filter(Boolean) as Setting[] ) } function unregister(key: string) { if (!hasRendered) return panels[0]?.$ref.querySelector(`[data-key=${key}]`)?.remove() panels = panels.filter((p) => (p.key === key ? (p.$ref.remove(), (p = null as any), false) : true)) } function updateLabel(key: string, text: string) { if (!hasRendered) return const $item = $dom.querySelector(`[data-key="${key}"] span[role="label"]`) if ($item) $item.innerText = text } function select(key: string, value: boolean | number, shouldBeCallFn: Boolean = true) { if (!hasRendered) return if (typeof value == 'number') { for (let i = 0; i < panels.length; i++) { const panel = panels[i]! if (panel.key == key) { panel.select!(value, shouldBeCallFn) break } } } else { $dom .querySelector(`[data-key="${key}"][aria-checked]`) ?.select(shouldBeCallFn) } } function bootstrap(settings: Setting[]) { if (settings.length < 1) return if (!hasRendered) { hasRendered = true $.render($dom, $el) renderSettingMenu() it.keyboard?.register({ c: showSetting }) } createPanel(player, panels, settings, { target: $dom }) } function outClickListener(e: Event) { if (!$dom.contains(e.target)) { player.$root.classList.remove(settingShown) panels.forEach(($p) => $p.$ref.classList.remove(activeCls)) document.removeEventListener('click', outClickListener) } } function showSetting() { player.$root.classList.add(settingShown) panels[0]!.$ref.classList.add(activeCls) setTimeout(() => { document.addEventListener('click', outClickListener) }) } player.on('destroy', () => { document.removeEventListener('click', outClickListener) }) function renderSettingMenu() { const settingButton = $.create( 'button', { class: `${icon} ${tooltip}`, 'aria-label': player.locales.get('Settings'), 'data-tooltip-pos': position == 'top' ? 'down' : '' }, `${Icons.get('setting')}` ) settingButton.addEventListener('click', (e) => { e.stopPropagation() showSetting() }) const index = [config.pictureInPicture && player.isPipEnabled, config.fullscreen].filter(Boolean).length if (isTop) { const parent = it.$controllerBar!.lastElementChild! parent.insertBefore(settingButton, parent.children[parent.children.length]!) } else { const parent = it.$controllerBottom!.lastElementChild! parent.insertBefore(settingButton, parent.children[parent.children.length - index]!) } } it.setting = { register, unregister, updateLabel, select } }