From 350471f5808c54601dfb67761d840f033ac14a61 Mon Sep 17 00:00:00 2001 From: Andrew S Date: Sun, 17 Dec 2023 20:19:46 -0600 Subject: [PATCH] Support multiple click actions --- chrome/_locales/en/messages.json | 27 ++++++- chrome/_locales/es/messages.json | 27 ++++++- chrome/_locales/ja/messages.json | 27 ++++++- chrome/player/FastStreamClient.mjs | 9 ++- .../player/options/defaults/ClickActions.mjs | 7 ++ .../options/defaults/DefaultOptions.mjs | 5 +- chrome/player/options/index.html | 31 ++++++-- chrome/player/options/options.css | 32 +++++++- chrome/player/options/options.mjs | 63 ++++++++++++--- chrome/player/ui/InterfaceController.mjs | 76 +++++++++++++++---- chrome/player/ui/KeybindManager.mjs | 17 +---- 11 files changed, 261 insertions(+), 60 deletions(-) create mode 100644 chrome/player/options/defaults/ClickActions.mjs diff --git a/chrome/_locales/en/messages.json b/chrome/_locales/en/messages.json index 87eb03e3..250a5491 100644 --- a/chrome/_locales/en/messages.json +++ b/chrome/_locales/en/messages.json @@ -711,15 +711,36 @@ "options_general_playbackrate": { "message": "Default playback rate" }, - "options_general_clickpause": { - "message": "Single click toggles play/pause" - }, "options_general_autoplayyt": { "message": "Autoplay YouTube videos" }, "options_general_quality": { "message": "Quality multiplier factor for quality selection algorithm (higher will choose higher quality)" }, + "options_general_clickaction": { + "message": "Single click action" + }, + "options_general_dblclickaction": { + "message": "Double click action" + }, + "options_general_tplclickaction": { + "message": "Triple click action" + }, + "options_general_clickaction_playpause": { + "message": "Toggle play/pause" + }, + "options_general_clickaction_fullscreen": { + "message": "Toggle fullscreen" + }, + "options_general_clickaction_pip": { + "message": "Toggle picture-in-picture" + }, + "options_general_clickaction_hidecontrols": { + "message": "Hide controls" + }, + "options_general_clickaction_hideplayer": { + "message": "Hide player" + }, "options_export_header": { "message": "Import/Export Settings" }, diff --git a/chrome/_locales/es/messages.json b/chrome/_locales/es/messages.json index 9ba9df91..938acd4c 100644 --- a/chrome/_locales/es/messages.json +++ b/chrome/_locales/es/messages.json @@ -710,15 +710,36 @@ "options_general_playbackrate": { "message": "Velocidad de reproducción predeterminada" }, - "options_general_clickpause": { - "message": "Un solo clic alterna reproducir/pausar" - }, "options_general_autoplayyt": { "message": "Reproducir automáticamente videos de YouTube incrustados" }, "options_general_quality": { "message": "Factor multiplicador de calidad para el algoritmo de selección de calidad (mayor elegirá mayor calidad)" }, + "options_general_clickaction": { + "message": "Acción de clic" + }, + "options_general_dblclickaction": { + "message": "Acción de doble clic" + }, + "options_general_tplclickaction": { + "message": "Acción de triple clic" + }, + "options_general_clickaction_playpause": { + "message": "Alternar reproducir/pausar" + }, + "options_general_clickaction_fullscreen": { + "message": "Alternar pantalla completa" + }, + "options_general_clickaction_pip": { + "message": "Alternar imagen en imagen" + }, + "options_general_clickaction_hidecontrols": { + "message": "Ocultar controles" + }, + "options_general_clickaction_hideplayer": { + "message": "Ocultar reproductor" + }, "options_export_header": { "message": "Importar/Exportar Configuraciones" }, diff --git a/chrome/_locales/ja/messages.json b/chrome/_locales/ja/messages.json index 2ec82aba..e292e86c 100644 --- a/chrome/_locales/ja/messages.json +++ b/chrome/_locales/ja/messages.json @@ -711,15 +711,36 @@ "options_general_playbackrate": { "message": "デフォルトの再生速度" }, - "options_general_clickpause": { - "message": "シングルクリックで再生/一時停止を切り替えます" - }, "options_general_autoplayyt": { "message": "YouTube 動画の自動再生" }, "options_general_quality": { "message": "品質選択アルゴリズムの品質倍率係数 (高いほど高品質を選択します)" }, + "options_general_clickaction": { + "message": "シングルクリックアクション" + }, + "options_general_dblclickaction": { + "message": "ダブルクリックアクション" + }, + "options_general_tplclickaction": { + "message": "トリプルクリックアクション" + }, + "options_general_clickaction_playpause": { + "message": "再生/一時停止を切り替えます" + }, + "options_general_clickaction_fullscreen": { + "message": "フルスクリーンを切り替えます" + }, + "options_general_clickaction_pip": { + "message": "ピクチャー イン ピクチャー (PiP) を切り替えます" + }, + "options_general_clickaction_hidecontrols": { + "message": "コントロールを隠します" + }, + "options_general_clickaction_hideplayer": { + "message": "プレーヤーを隠します" + }, "options_export_header": { "message": "設定のインポート/エクスポート" }, diff --git a/chrome/player/FastStreamClient.mjs b/chrome/player/FastStreamClient.mjs index 3e44fb6a..a8a52654 100644 --- a/chrome/player/FastStreamClient.mjs +++ b/chrome/player/FastStreamClient.mjs @@ -14,6 +14,7 @@ import {DOMElements} from './ui/DOMElements.mjs'; import {AudioConfigManager} from './ui/audio/AudioConfigManager.mjs'; import {EnvUtils} from './utils/EnvUtils.mjs'; import {Localize} from './modules/Localize.mjs'; +import {ClickActions} from './options/defaults/ClickActions.mjs'; export class FastStreamClient extends EventEmitter { @@ -31,7 +32,9 @@ export class FastStreamClient extends EventEmitter { freeFragments: true, downloadAll: false, freeUnusedChannels: true, - clickToPause: false, + singleClickAction: ClickActions.HIDE_CONTROLS, + doubleClickAction: ClickActions.PLAY_PAUSE, + tripleClickAction: ClickActions.FULLSCREEN, videoBrightness: 1, videoContrast: 1, videoSaturation: 1, @@ -116,9 +119,11 @@ export class FastStreamClient extends EventEmitter { this.options.downloadAll = options.downloadAll; this.options.freeUnusedChannels = options.freeUnusedChannels; this.options.autoEnableBestSubtitles = options.autoEnableBestSubtitles; - this.options.clickToPause = options.clickToPause; this.options.maxSpeed = options.maxSpeed; this.options.seekStepSize = options.seekStepSize; + this.options.singleClickAction = options.singleClickAction; + this.options.doubleClickAction = options.doubleClickAction; + this.options.tripleClickAction = options.tripleClickAction; this.options.videoBrightness = options.videoBrightness; this.options.videoContrast = options.videoContrast; diff --git a/chrome/player/options/defaults/ClickActions.mjs b/chrome/player/options/defaults/ClickActions.mjs new file mode 100644 index 00000000..cffc0a1d --- /dev/null +++ b/chrome/player/options/defaults/ClickActions.mjs @@ -0,0 +1,7 @@ +export const ClickActions = { + PLAY_PAUSE: 'playpause', + FULLSCREEN: 'fullscreen', + PIP: 'pip', + HIDE_CONTROLS: 'hidecontrols', + HIDE_PLAYER: 'hideplayer', +}; diff --git a/chrome/player/options/defaults/DefaultOptions.mjs b/chrome/player/options/defaults/DefaultOptions.mjs index 1090c652..a5544a2b 100644 --- a/chrome/player/options/defaults/DefaultOptions.mjs +++ b/chrome/player/options/defaults/DefaultOptions.mjs @@ -1,4 +1,5 @@ import {EnvUtils} from '../../utils/EnvUtils.mjs'; +import {ClickActions} from './ClickActions.mjs'; import {DefaultKeybinds} from './DefaultKeybinds.mjs'; export const DefaultOptions = { @@ -8,7 +9,6 @@ export const DefaultOptions = { downloadAll: true, freeUnusedChannels: true, autoEnableBestSubtitles: false, - clickToPause: false, autoplayYoutube: true, autoEnableURLs: [], keybinds: DefaultKeybinds, @@ -23,4 +23,7 @@ export const DefaultOptions = { seekStepSize: 2, playbackRate: 1, qualityMultiplier: 1.1, + singleClickAction: ClickActions.HIDE_CONTROLS, + doubleClickAction: ClickActions.PLAY_PAUSE, + tripleClickAction: ClickActions.FULLSCREEN, }; diff --git a/chrome/player/options/index.html b/chrome/player/options/index.html index ecfc2fc1..8c26e60d 100644 --- a/chrome/player/options/index.html +++ b/chrome/player/options/index.html @@ -127,13 +127,6 @@


-
- -
-
- -
-
@@ -160,9 +153,33 @@

+
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+

+
+ +

diff --git a/chrome/player/options/options.css b/chrome/player/options/options.css index 71ff992d..8e55ddcb 100644 --- a/chrome/player/options/options.css +++ b/chrome/player/options/options.css @@ -130,7 +130,7 @@ p.hint { button { background-color: rgba(255, 255, 255, 0.4); - border: 1px solid rgba(0, 0, 0, 0.4); + border: 1px solid rgba(0, 0, 0, 0.7); border-radius: 3px; cursor: pointer; padding: 2px 5px; @@ -219,7 +219,7 @@ button:hover { .option.grid2 { display: grid; gap: 5px; - grid-template-columns: 1fr 150px; + grid-template-columns: 1fr 160px; align-items: center; } @@ -232,6 +232,34 @@ button:hover { text-align: center; } +input { + border: 1px solid rgba(0, 0, 0, 0.7); + border-radius: 3px; + padding: 3px 5px; + outline: none; + background-color: white; + font-size: 12px; +} + +input:focus { + border: 1px solid blue; +} + +.option.grid2 .select { + grid-column: 2; +} + +.select > select { + width: 100%; + height: 100%; + border: 1px solid rgba(0, 0, 0, 0.7); + border-radius: 3px; + padding: 2px 5px; + outline: none; + background-color: white; + font-size: 12px; +} + input[type="checkbox"] { width: 16px; height: 16px; diff --git a/chrome/player/options/options.mjs b/chrome/player/options/options.mjs index b8b601de..8d68e54c 100644 --- a/chrome/player/options/options.mjs +++ b/chrome/player/options/options.mjs @@ -7,6 +7,7 @@ import {DefaultOptions} from './defaults/DefaultOptions.mjs'; import {Localize} from '../modules/Localize.mjs'; import {UpdateChecker} from '../utils/UpdateChecker.mjs'; // SPLICER:NO_UPDATE_CHECKER:REMOVE_LINE +import {ClickActions} from './defaults/ClickActions.mjs'; let Options = {}; const analyzeVideos = document.getElementById('analyzevideos'); @@ -20,11 +21,13 @@ const autoSub = document.getElementById('autosub'); const maxSpeed = document.getElementById('maxspeed'); const seekStepSize = document.getElementById('seekstepsize'); const playbackRate = document.getElementById('playbackrate'); -const clickToPause = document.getElementById('clicktopause'); const autoplayYoutube = document.getElementById('autoplayyt'); const qualityMultiplier = document.getElementById('qualitymultiplier'); const importButton = document.getElementById('import'); const exportButton = document.getElementById('export'); +const clickAction = document.getElementById('clickaction'); +const dblclickAction = document.getElementById('dblclickaction'); +const tplclickAction = document.getElementById('tplclickaction'); autoEnableURLSInput.setAttribute('autocapitalize', 'off'); autoEnableURLSInput.setAttribute('autocomplete', 'off'); autoEnableURLSInput.setAttribute('autocorrect', 'off'); @@ -52,13 +55,16 @@ async function loadOptions(newOptions) { playStreamURLs.checked = !!Options.playStreamURLs; playMP4URLs.checked = !!Options.playMP4URLs; autoSub.checked = !!Options.autoEnableBestSubtitles; - clickToPause.checked = !!Options.clickToPause; autoplayYoutube.checked = !!Options.autoplayYoutube; maxSpeed.value = StringUtils.getSpeedString(Options.maxSpeed); seekStepSize.value = Math.round(Options.seekStepSize * 100) / 100; playbackRate.value = Options.playbackRate; qualityMultiplier.value = Options.qualityMultiplier; + setSelectMenuValue(clickAction, Options.singleClickAction); + setSelectMenuValue(dblclickAction, Options.doubleClickAction); + setSelectMenuValue(tplclickAction, Options.tripleClickAction); + if (Options.keybinds) { keybindsList.replaceChildren(); for (const keybind in Options.keybinds) { @@ -82,15 +88,59 @@ async function loadOptions(newOptions) { autoEnableURLSInput.value = Options.autoEnableURLs.join('\n'); } +function createSelectMenu(container, options, selected, localPrefix, callback) { + container.replaceChildren(); + const select = document.createElement('select'); + for (const option of options) { + const optionElement = document.createElement('option'); + optionElement.value = option; + optionElement.textContent = Localize.getMessage(localPrefix + '_' + option); + if (option === selected) { + optionElement.selected = true; + } + select.appendChild(optionElement); + } + select.addEventListener('change', callback); + container.appendChild(select); +} + +function setSelectMenuValue(container, value) { + const select = container.querySelector('select'); + if (!select) { + return; + } + select.value = value; +} + +createSelectMenu(clickAction, Object.values(ClickActions), Options.singleClickAction, 'options_general_clickaction', (e) => { + Options.singleClickAction = e.target.value; + optionChanged(); +}); + +createSelectMenu(dblclickAction, Object.values(ClickActions), Options.doubleClickAction, 'options_general_clickaction', (e) => { + Options.doubleClickAction = e.target.value; + optionChanged(); +}); + +createSelectMenu(tplclickAction, Object.values(ClickActions), Options.tripleClickAction, 'options_general_clickaction', (e) => { + Options.tripleClickAction = e.target.value; + optionChanged(); +}); + document.querySelectorAll('.option').forEach((option) => { option.addEventListener('click', (e) => { if (e.target.tagName !== 'INPUT') { const input = option.querySelector('input'); - input.click(); + if (input) { + input.click(); + } } }); - WebUtils.setupTabIndex(option.querySelector('input')); + const input = option.querySelector('input'); + if (input) { + WebUtils.setupTabIndex(input); + } }); document.querySelectorAll('.video-option').forEach((option) => { @@ -206,11 +256,6 @@ autoplayYoutube.addEventListener('change', () => { optionChanged(); }); -clickToPause.addEventListener('change', () => { - Options.clickToPause = clickToPause.checked; - optionChanged(); -}); - maxSpeed.addEventListener('change', () => { // parse value, number unit/s Options.maxSpeed = StringUtils.getSpeedValue(maxSpeed.value); diff --git a/chrome/player/ui/InterfaceController.mjs b/chrome/player/ui/InterfaceController.mjs index fcfeb4be..a6b57c75 100644 --- a/chrome/player/ui/InterfaceController.mjs +++ b/chrome/player/ui/InterfaceController.mjs @@ -3,6 +3,7 @@ import {PlayerModes} from '../enums/PlayerModes.mjs'; import {Coloris} from '../modules/coloris.mjs'; import {Localize} from '../modules/Localize.mjs'; import {streamSaver} from '../modules/StreamSaver.mjs'; +import {ClickActions} from '../options/defaults/ClickActions.mjs'; import {SubtitleTrack} from '../SubtitleTrack.mjs'; import {EnvUtils} from '../utils/EnvUtils.mjs'; import {FastStreamArchiveUtils} from '../utils/FastStreamArchiveUtils.mjs'; @@ -20,6 +21,8 @@ export class InterfaceController { this.client = client; this.persistent = client.persistent; this.isSeeking = false; + this.hidden = false; + this.shouldPlay = false; this.isMouseOverProgressbar = false; this.lastSpeed = 0; @@ -421,14 +424,6 @@ export class InterfaceController { this.playPauseToggle(); e.stopPropagation(); }); - DOMElements.videoContainer.addEventListener('dblclick', (e) => { - if (!this.client.options.clickToPause) { - this.playPauseToggle(); - } else { - this.hideControlBarOnAction(); - } - e.stopPropagation(); - }); DOMElements.progressContainer.addEventListener('mousedown', this.onProgressbarMouseDown.bind(this)); DOMElements.progressContainer.addEventListener('mouseenter', this.onProgressbarMouseEnter.bind(this)); DOMElements.progressContainer.addEventListener('mouseleave', this.onProgressbarMouseLeave.bind(this)); @@ -590,17 +585,54 @@ export class InterfaceController { this.focusingControls = false; this.queueControlsHide(); }); - DOMElements.videoContainer.addEventListener('click', () => { + let clickCount = 0; + let clickTimeout = null; + DOMElements.videoContainer.addEventListener('click', (e) => { if (this.isBigPlayButtonVisible()) { this.playPauseToggle(); - } else if (this.client.options.clickToPause) { - this.playPauseToggle(); return; } - this.focusingControls = false; - this.mouseOverControls = false; - this.hideControlBarOnAction(); + if (clickTimeout !== null) { + clickCount++; + } else { + clickCount = 1; + } + clearTimeout(clickTimeout); + clickTimeout = setTimeout(() => { + clickTimeout = null; + + let clickAction; + if (clickCount === 1) { + clickAction = this.client.options.singleClickAction; + } else if (clickCount === 2) { + clickAction = this.client.options.doubleClickAction; + } else if (clickCount === 3) { + clickAction = this.client.options.tripleClickAction; + } else { + return; + } + + switch (clickAction) { + case ClickActions.FULLSCREEN: + this.fullscreenToggle(); + break; + case ClickActions.PIP: + this.pipToggle(); + break; + case ClickActions.PLAY_PAUSE: + this.playPauseToggle(); + break; + case ClickActions.HIDE_CONTROLS: + this.focusingControls = false; + this.mouseOverControls = false; + this.hideControlBar(); + break; + case ClickActions.HIDE_PLAYER: + this.toggleHide(); + break; + } + }, clickCount < 3 ? 300 : 0); }); DOMElements.hideButton.addEventListener('click', () => { DOMElements.hideButton.blur(); @@ -694,6 +726,22 @@ export class InterfaceController { DOMElements.playinfo.style.display = this.client.player ? 'none' : ''; } + toggleHide() { + if (this.hidden) { + DOMElements.playerContainer.classList.remove('player-hidden'); + this.hidden = false; + if (this.shouldPlay) { + this.client.player?.play(); + } + } else { + DOMElements.playerContainer.classList.add('player-hidden'); + + this.hidden = true; + this.shouldPlay = this.client.persistent.playing; + this.client.player?.pause(); + } + } + pipToggle() { if (document.pictureInPictureElement) { document.exitPictureInPicture(); diff --git a/chrome/player/ui/KeybindManager.mjs b/chrome/player/ui/KeybindManager.mjs index 74080cfc..f52ab20d 100644 --- a/chrome/player/ui/KeybindManager.mjs +++ b/chrome/player/ui/KeybindManager.mjs @@ -1,13 +1,11 @@ import {DefaultKeybinds} from '../options/defaults/DefaultKeybinds.mjs'; import {EventEmitter} from '../modules/eventemitter.mjs'; import {WebUtils} from '../utils/WebUtils.mjs'; -import {DOMElements} from './DOMElements.mjs'; export class KeybindManager extends EventEmitter { constructor(client) { super(); this.client = client; - this.hidden = false; this.keybindMap = new Map(); this.setup(); } @@ -22,21 +20,8 @@ export class KeybindManager extends EventEmitter { this.onKeyDown(e); }); - let shouldPlay = false; this.on('HidePlayer', (e) => { - if (this.hidden) { - DOMElements.playerContainer.classList.remove('player-hidden'); - this.hidden = false; - if (shouldPlay) { - this.client.player?.play(); - } - } else { - DOMElements.playerContainer.classList.add('player-hidden'); - - this.hidden = true; - shouldPlay = this.client.persistent.playing; - this.client.player?.pause(); - } + this.client.interfaceController.toggleHide(); }); this.on('GoToStart', (e) => {