import style from 'bundle-text:./style.less'; import $on from 'bundle-text:./img/on.svg'; import $off from 'bundle-text:./img/off.svg'; import $config from 'bundle-text:./img/config.svg'; import $style from 'bundle-text:./img/style.svg'; import $mode_0_off from 'bundle-text:./img/mode_0_off.svg'; import $mode_0_on from 'bundle-text:./img/mode_0_on.svg'; import $mode_1_off from 'bundle-text:./img/mode_1_off.svg'; import $mode_1_on from 'bundle-text:./img/mode_1_on.svg'; import $mode_2_off from 'bundle-text:./img/mode_2_off.svg'; import $mode_2_on from 'bundle-text:./img/mode_2_on.svg'; import $check_on from 'bundle-text:./img/check_on.svg'; import $check_off from 'bundle-text:./img/check_off.svg'; export default class Setting { constructor(art, danmuku) { this.art = art; this.danmuku = danmuku; this.utils = art.constructor.utils; const { setStyle } = this.utils; const { $controlsCenter } = art.template; setStyle($controlsCenter, 'display', 'flex'); this.template = { $controlsCenter, $mount: $controlsCenter, $danmuku: null, $toggle: null, $configModes: null, $styleModes: null, $colors: null, $opacitySlider: null, $opacityValue: null, $marginSlider: null, $marginValue: null, $fontSizeSlider: null, $fontSizeValue: null, $speedSlider: null, $speedValue: null, $input: null, $send: null, }; this.slider = { opacity: null, margin: null, fontSize: null, speed: null, }; this.emitting = false; this.isLock = false; this.timer = null; this.createTemplate(); this.createSliders(); this.createEvents(); this.mount(this.option.mount); art.on('fullscreen', (state) => this.onFullscreen(state)); art.on('fullscreenWeb', (state) => this.onFullscreen(state)); } static get icons() { return { $on, $off, $config, $style, $mode_0_off, $mode_0_on, $mode_1_off, $mode_1_on, $mode_2_off, $mode_2_on, $check_on, $check_off, }; } get option() { return this.danmuku.option; } get outside() { return this.template.$mount !== this.template.$controlsCenter; } get TEMPLATE() { const { option } = this; return `
${$on}${$off}
${$config}
按类型屏蔽
${$mode_0_off}${$mode_0_on}
滚动
${$mode_1_off}${$mode_1_on}
顶部
${$mode_2_off}${$mode_2_on}
底部
${$check_on}${$check_off} 防止弹幕重叠
${$check_on}${$check_off} 同步视频速度
不透明度
未知
显示区域
未知
弹幕字号
未知
弹幕速度
未知
${$style}
模式
${$mode_0_on}
滚动
${$mode_1_on}
顶部
${$mode_2_on}
底部
颜色
${this.COLOR.map((color) => `
`).join('')}
发送
`; } get OPACITY() { return { min: 0, max: 100, steps: [], ...this.option.OPACITY, }; } get FONT_SIZE() { return { min: 12, max: 120, steps: [], ...this.option.FONT_SIZE, }; } get MARGIN() { return { min: 0, max: 3, steps: [ { name: '1/4', value: [10, '75%'], }, { name: '半屏', value: [10, '50%'], }, { name: '3/4', value: [10, '25%'], }, { name: '满屏', value: [10, 10], }, ], ...this.option.MARGIN, }; } get SPEED() { return { min: 0, max: 4, steps: [ { name: '极慢', value: 10, }, { name: '较慢', value: 7.5, hide: true, }, { name: '适中', value: 5, }, { name: '较快', value: 2.5, hide: true, }, { name: '极快', value: 1, }, ], ...this.option.SPEED, }; } get COLOR() { return this.option.COLOR.length ? this.option.COLOR : [ '#FE0302', '#FF7204', '#FFAA02', '#FFD302', '#FFFF00', '#A0EE00', '#00CD00', '#019899', '#4266BE', '#89D5FF', '#CC0273', '#222222', '#9B9B9B', '#FFFFFF', ]; } query(selector) { const { query } = this.utils; const { $danmuku } = this.template; return query(selector, $danmuku); } setData(key, value) { const { $player } = this.art.template; const { $mount } = this.template; $player.dataset[key] = value; if (this.outside) { $mount.dataset[key] = value; } } createTemplate() { const { createElement, tooltip } = this.utils; const $danmuku = createElement('div'); $danmuku.className = 'artplayer-plugin-danmuku'; $danmuku.innerHTML = this.TEMPLATE; this.template.$danmuku = $danmuku; this.template.$toggle = this.query('.apd-toggle'); this.template.$configModes = this.query('.apd-config-mode .apd-modes'); this.template.$styleModes = this.query('.apd-style-mode .apd-modes'); this.template.$colors = this.query('.apd-colors'); this.template.$antiOverlap = this.query('.apd-anti-overlap'); this.template.$syncVideo = this.query('.apd-sync-video'); this.template.$opacitySlider = this.query('.apd-config-opacity .apd-slider'); this.template.$opacityValue = this.query('.apd-config-opacity .apd-value'); this.template.$marginSlider = this.query('.apd-config-margin .apd-slider'); this.template.$marginValue = this.query('.apd-config-margin .apd-value'); this.template.$fontSizeSlider = this.query('.apd-config-fontSize .apd-slider'); this.template.$fontSizeValue = this.query('.apd-config-fontSize .apd-value'); this.template.$speedSlider = this.query('.apd-config-speed .apd-slider'); this.template.$speedValue = this.query('.apd-config-speed .apd-value'); this.template.$input = this.query('.apd-input'); this.template.$send = this.query('.apd-send'); const { $toggle } = this.template; this.art.on('artplayerPluginDanmuku:show', () => { tooltip($toggle, '关闭弹幕'); }); this.art.on('artplayerPluginDanmuku:hide', () => { tooltip($toggle, '打开弹幕'); }); } createEvents() { const { $toggle, $configModes, $styleModes, $colors, $antiOverlap, $syncVideo, $send, $input } = this.template; this.art.proxy($toggle, 'click', () => { this.danmuku.config({ visible: !this.option.visible, }); this.reset(); }); this.art.proxy($configModes, 'click', (event) => { const $mode = event.target.closest('.apd-mode'); if (!$mode) return; const mode = Number($mode.dataset.mode); if (this.option.modes.includes(mode)) { this.danmuku.config({ modes: this.option.modes.filter((m) => m !== mode), }); } else { this.danmuku.config({ modes: [...this.option.modes, mode], }); } this.reset(); }); this.art.proxy($antiOverlap, 'click', () => { this.danmuku.config({ antiOverlap: !this.option.antiOverlap, }); this.reset(); }); this.art.proxy($syncVideo, 'click', () => { this.danmuku.config({ synchronousPlayback: !this.option.synchronousPlayback, }); this.reset(); }); this.art.proxy($styleModes, 'click', (event) => { const $mode = event.target.closest('.apd-mode'); if (!$mode) return; const mode = Number($mode.dataset.mode); this.danmuku.config({ mode: mode, }); this.reset(); }); this.art.proxy($colors, 'click', (event) => { const $color = event.target.closest('.apd-color'); if (!$color) return; this.danmuku.config({ color: $color.dataset.color, }); this.reset(); }); this.art.proxy($send, 'click', () => this.emit()); this.art.proxy($input, 'keypress', (event) => { if (event.key === 'Enter') { event.preventDefault(); this.emit(); } }); } createSliders() { this.slider.opacity = this.createSlider({ ...this.OPACITY, container: this.template.$opacitySlider, findIndex: () => { return Math.round(this.option.opacity * 100); }, onChange: (index) => { const { $opacityValue } = this.template; $opacityValue.textContent = `${index}%`; this.danmuku.config({ opacity: index / 100, }); }, }); this.slider.margin = this.createSlider({ ...this.MARGIN, container: this.template.$marginSlider, findIndex: () => { return this.MARGIN.steps.findIndex( (item) => item.value[0] === this.option.margin[0] && item.value[1] === this.option.margin[1], ); }, onChange: (index) => { const margin = this.MARGIN.steps[index]; if (!margin) return; const { $marginValue } = this.template; $marginValue.textContent = margin.name; this.danmuku.config({ margin: margin.value, }); }, }); this.slider.fontSize = this.createSlider({ ...this.FONT_SIZE, container: this.template.$fontSizeSlider, findIndex: () => { return Math.round(this.danmuku.fontSize); }, onChange: (index) => { const { $fontSizeValue } = this.template; $fontSizeValue.textContent = `${index}px`; this.danmuku.config({ fontSize: index, }); }, }); this.slider.speed = this.createSlider({ ...this.SPEED, container: this.template.$speedSlider, findIndex: () => { return this.SPEED.steps.findIndex((item) => item.value === this.option.speed); }, onChange: (index) => { const speed = this.SPEED.steps[index]; if (!speed) return; const { $speedValue } = this.template; $speedValue.textContent = speed.name; this.danmuku.config({ speed: speed.value, }); }, }); } createSlider({ min, max, container, findIndex, onChange, steps = [] }) { const { query, clamp } = this.utils; container.innerHTML = `
${steps.map(() => `
`).join('')}
${steps.map((step) => (step.hide ? '' : `
${step.name}
`)).join('')}
`; const $dot = query('.apd-slider-dot', container); const $progress = query('.apd-slider-progress', container); let isDroging = false; function reset(index = findIndex()) { if (index < min || index > max) return; const percentage = (index - min) / (max - min); $dot.style.left = `${percentage * 100}%`; if (steps.length === 0) { $progress.style.width = $dot.style.left; } onChange(index); } function updateLeft(event) { const { left, width } = container.getBoundingClientRect(); const value = clamp(event.clientX - left, 0, width); const index = Math.round((value / width) * (max - min) + min); reset(index); } this.art.proxy(container, 'click', (event) => { updateLeft(event); }); this.art.proxy(container, 'mousedown', (event) => { isDroging = event.button === 0; }); this.art.on('document:mousemove', (event) => { if (isDroging) { updateLeft(event); } }); this.art.on('document:mouseup', (event) => { if (isDroging) { isDroging = false; updateLeft(event); } }); return { reset }; } onFullscreen(state) { if (this.outside) { const { $danmuku, $controlsCenter, $mount } = this.template; if (state) { $controlsCenter.appendChild($danmuku); } else { $mount.appendChild($danmuku); } } } async emit() { const { $input } = this.template; const text = $input.value.trim(); if (!text.length) return; if (this.isLock) return; if (this.emitting) return; const danmu = { text: text, mode: this.option.mode, color: this.option.color, time: this.art.currentTime, }; try { this.emitting = true; const state = await this.option.beforeEmit(danmu); this.emitting = false; if (state !== true) return; danmu.border = true; delete danmu.time; this.danmuku.emit(danmu); $input.value = ''; this.lock(); // eslint-disable-next-line no-unused-vars } catch (error) { this.emitting = false; } } lock() { const { addClass } = this.utils; const { $send } = this.template; this.isLock = true; let time = this.option.lockTime; $send.innerText = time; addClass($send, 'apd-lock'); const loop = () => { this.timer = setTimeout(() => { if (time === 0) { this.unlock(); } else { time -= 1; $send.innerText = time; loop(); } }, 1000); }; loop(); } unlock() { const { removeClass } = this.utils; const { $send } = this.template; clearTimeout(this.timer); this.isLock = false; $send.innerText = '发送'; removeClass($send, 'apd-lock'); } reset() { const { inverseClass, tooltip } = this.utils; const { $toggle, $colors } = this.template; this.slider.opacity.reset(); this.slider.margin.reset(); this.slider.fontSize.reset(); this.slider.speed.reset(); this.setData('danmukuVisible', this.option.visible); this.setData('danmukuMode', this.option.mode); this.setData('danmukuColor', this.option.color); this.setData('danmukuMode0', this.option.modes.includes(0)); this.setData('danmukuMode1', this.option.modes.includes(1)); this.setData('danmukuMode2', this.option.modes.includes(2)); this.setData('danmukuAntiOverlap', this.option.antiOverlap); this.setData('danmukuSyncVideo', this.option.synchronousPlayback); this.setData('danmukuTheme', this.option.theme); this.setData('danmukuEmitter', this.option.emitter); const colors = $colors.children; const $color = Array.from(colors).find((item) => item.dataset.color === this.option.color.toUpperCase()); $color && inverseClass($color, 'apd-active'); tooltip($toggle, this.option.visible ? '关闭弹幕' : '打开弹幕'); } mount(target) { const $el = typeof target === 'string' ? document.querySelector(target) : target; $el.appendChild(this.template.$danmuku); this.template.$mount = $el; this.reset(); } } if (typeof document !== 'undefined') { const id = 'artplayer-plugin-danmuku'; const $style = document.getElementById(id); if ($style) { $style.textContent = style; } else { const $style = document.createElement('style'); $style.id = id; $style.textContent = style; document.head.appendChild($style); } }