From 184dc27ce9ee3351232fa6baeed532e016e113b3 Mon Sep 17 00:00:00 2001 From: throwaway96 <68320646+throwaway96@users.noreply.github.com> Date: Sun, 24 Mar 2024 20:57:20 -0400 Subject: [PATCH 1/3] rework config system/UI Centralize all information about configuration options in a single object. Replace hardcoded options UI with programmatic generation based on descriptions in config.js. Also throw exceptions on invalid config keys and stop changes to localConfig from affecting defaultConfig. Ideally the options UI would be entirely automatically generated, but a way a way to handle grouped options (like the SponsorBlock ones) is still needed. It is now easier to add a config option, and hopefully soon it will only require adding an entry to configOptions in config.js. --- src/config.js | 97 +++++++++++++++++++----- src/ui.js | 201 +++++++++++++++++++++----------------------------- 2 files changed, 162 insertions(+), 136 deletions(-) diff --git a/src/config.js b/src/config.js index a93ffe28..f271ace5 100644 --- a/src/config.js +++ b/src/config.js @@ -1,25 +1,81 @@ const CONFIG_KEY = 'ytaf-configuration'; -const defaultConfig = { - enableAdBlock: true, - enableSponsorBlock: true, - enableSponsorBlockSponsor: true, - enableSponsorBlockIntro: true, - enableSponsorBlockOutro: true, - enableSponsorBlockInteraction: true, - enableSponsorBlockSelfPromo: true, - enableSponsorBlockMusicOfftopic: true -}; - -let localConfig; - -try { - localConfig = JSON.parse(window.localStorage[CONFIG_KEY]); -} catch (err) { - console.warn('Config read failed:', err); - localConfig = defaultConfig; + +export const configOptions = new Map([ + ['enableAdBlock', { default: true, desc: 'Enable ad blocking' }], + ['enableSponsorBlock', { default: true, desc: 'Enable SponsorBlock' }], + [ + 'enableSponsorBlockSponsor', + { default: true, desc: 'Skip sponsor segments' } + ], + ['enableSponsorBlockIntro', { default: true, desc: 'Skip intro segments' }], + ['enableSponsorBlockOutro', { default: true, desc: 'Skip outro segments' }], + [ + 'enableSponsorBlockInteraction', + { + default: true, + desc: 'Skip interaction reminder segments' + } + ], + [ + 'enableSponsorBlockSelfPromo', + { + default: true, + desc: 'Skip self promotion segments' + } + ], + [ + 'enableSponsorBlockMusicOfftopic', + { + default: true, + desc: 'Skip music and off-topic segments' + } + ] +]); + +const defaultConfig = (() => { + let ret = {}; + for (const [k, v] of configOptions) { + ret[k] = v.default; + } + return ret; +})(); + +function loadStoredConfig() { + const storage = window.localStorage.getItem(CONFIG_KEY); + + if (storage === null) { + console.info('Config not set; using defaults.'); + return null; + } + + try { + return JSON.parse(storage); + } catch (err) { + console.warn('Error parsing stored config:', err); + return null; + } +} + +// Use defaultConfig as a prototype so writes to localConfig don't change it. +let localConfig = loadStoredConfig() ?? Object.create(defaultConfig); + +function configExists(key) { + return configOptions.has(key); +} + +export function getConfigDesc(key) { + if (!configExists(key)) { + throw new Error('tried to get desc for unknown config key:', key); + } + + return configOptions.get(key).desc; } export function configRead(key) { + if (!configExists(key)) { + throw new Error('tried to read unknown config key:', key); + } + if (localConfig[key] === undefined) { console.warn( 'Populating key', @@ -27,6 +83,7 @@ export function configRead(key) { 'with default value', defaultConfig[key] ); + localConfig[key] = defaultConfig[key]; } @@ -34,6 +91,10 @@ export function configRead(key) { } export function configWrite(key, value) { + if (!configExists(key)) { + throw new Error('tried to write unknown config key:', key); + } + console.info('Setting key', key, 'to', value); localConfig[key] = value; window.localStorage[CONFIG_KEY] = JSON.stringify(localConfig); diff --git a/src/ui.js b/src/ui.js index 4bce127e..afc5102c 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,132 +1,97 @@ /*global navigate*/ import './spatial-navigation-polyfill.js'; import './ui.css'; -import { configRead, configWrite } from './config.js'; +import { configRead, configWrite, getConfigDesc } from './config.js'; // We handle key events ourselves. window.__spatialNavigation__.keyMode = 'NONE'; const ARROW_KEY_CODE = { 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; -const uiContainer = document.createElement('div'); -uiContainer.classList.add('ytaf-ui-container'); -uiContainer.style['display'] = 'none'; -uiContainer.setAttribute('tabindex', 0); -uiContainer.addEventListener( - 'focus', - () => console.info('uiContainer focused!'), - true -); -uiContainer.addEventListener( - 'blur', - () => console.info('uiContainer blured!'), - true -); - -uiContainer.addEventListener( - 'keydown', - (evt) => { - console.info('uiContainer key event:', evt.type, evt.charCode); - if (evt.charCode !== 404 && evt.charCode !== 172) { - if (evt.keyCode in ARROW_KEY_CODE) { - navigate(ARROW_KEY_CODE[evt.keyCode]); - } else if (evt.keyCode === 13) { - // "OK" button - document.querySelector(':focus').click(); - } else if (evt.keyCode === 27) { - // Back button - uiContainer.style.display = 'none'; - uiContainer.blur(); - } - evt.preventDefault(); - evt.stopPropagation(); - } - }, - true -); - -uiContainer.innerHTML = ` -

webOS YouTube Extended

- - -
- - - - - - -
-
Sponsor segments skipping - https://sponsor.ajay.app
-`; - -document.querySelector('body').appendChild(uiContainer); - -uiContainer.querySelector('#__adblock').checked = configRead('enableAdBlock'); -uiContainer.querySelector('#__adblock').addEventListener('change', (evt) => { - configWrite('enableAdBlock', evt.target.checked); -}); - -uiContainer.querySelector('#__sponsorblock').checked = - configRead('enableSponsorBlock'); -uiContainer - .querySelector('#__sponsorblock') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlock', evt.target.checked); +function createConfigCheckbox(key) { + const elmInput = document.createElement('input'); + elmInput.type = 'checkbox'; + elmInput.checked = configRead(key); + elmInput.addEventListener('change', (evt) => { + configWrite(key, evt.target.checked); }); -uiContainer.querySelector('#__sponsorblock_sponsor').checked = configRead( - 'enableSponsorBlockSponsor' -); -uiContainer - .querySelector('#__sponsorblock_sponsor') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockSponsor', evt.target.checked); - }); + const elmLabel = document.createElement('label'); + elmLabel.appendChild(elmInput); + // Use non-breaking space (U+00A0) + elmLabel.appendChild(document.createTextNode('\u00A0' + getConfigDesc(key))); -uiContainer.querySelector('#__sponsorblock_intro').checked = configRead( - 'enableSponsorBlockIntro' -); -uiContainer - .querySelector('#__sponsorblock_intro') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockIntro', evt.target.checked); - }); + return elmLabel; +} -uiContainer.querySelector('#__sponsorblock_outro').checked = configRead( - 'enableSponsorBlockOutro' -); -uiContainer - .querySelector('#__sponsorblock_outro') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockOutro', evt.target.checked); - }); +function createOptionsPanel() { + const elmContainer = document.createElement('div'); -uiContainer.querySelector('#__sponsorblock_interaction').checked = configRead( - 'enableSponsorBlockInteraction' -); -uiContainer - .querySelector('#__sponsorblock_interaction') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockInteraction', evt.target.checked); - }); + elmContainer.classList.add('ytaf-ui-container'); + elmContainer.style['display'] = 'none'; + elmContainer.setAttribute('tabindex', 0); -uiContainer.querySelector('#__sponsorblock_selfpromo').checked = configRead( - 'enableSponsorBlockSelfPromo' -); -uiContainer - .querySelector('#__sponsorblock_selfpromo') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockSelfPromo', evt.target.checked); - }); + elmContainer.addEventListener( + 'focus', + () => console.info('Options panel focused!'), + true + ); + elmContainer.addEventListener( + 'blur', + () => console.info('Options panel blurred!'), + true + ); -uiContainer.querySelector('#__sponsorblock_music_offtopic').checked = - configRead('enableSponsorBlockMusicOfftopic'); -uiContainer - .querySelector('#__sponsorblock_music_offtopic') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockMusicOfftopic', evt.target.checked); - }); + elmContainer.addEventListener( + 'keydown', + (evt) => { + console.info('Options panel key event:', evt.type, evt.charCode); + if (evt.charCode !== 404 && evt.charCode !== 172) { + if (evt.keyCode in ARROW_KEY_CODE) { + navigate(ARROW_KEY_CODE[evt.keyCode]); + } else if (evt.keyCode === 13) { + // "OK" button + document.querySelector(':focus').click(); + } else if (evt.keyCode === 27) { + // Back button + elmContainer.style.display = 'none'; + elmContainer.blur(); + } + evt.preventDefault(); + evt.stopPropagation(); + } + }, + true + ); + + const elmHeading = document.createElement('h1'); + elmHeading.textContent = 'webOS YouTube Extended'; + elmContainer.appendChild(elmHeading); + + elmContainer.appendChild(createConfigCheckbox('enableAdBlock')); + elmContainer.appendChild(createConfigCheckbox('enableSponsorBlock')); + + const elmBlock = document.createElement('blockquote'); + + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockSponsor')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockIntro')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockOutro')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockInteraction')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockSelfPromo')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockMusicOfftopic')); + + elmContainer.appendChild(elmBlock); + + const elmSponsorLink = document.createElement('div'); + elmSponsorLink.innerHTML = + 'Sponsor segments skipping - https://sponsor.ajay.app'; + elmContainer.appendChild(elmSponsorLink); + + return elmContainer; +} + +const optionsPanel = createOptionsPanel(); +document.body.appendChild(optionsPanel); const eventHandler = (evt) => { console.info( @@ -141,14 +106,14 @@ const eventHandler = (evt) => { evt.preventDefault(); evt.stopPropagation(); if (evt.type === 'keydown') { - if (uiContainer.style.display === 'none') { + if (optionsPanel.style.display === 'none') { console.info('Showing and focusing!'); - uiContainer.style.display = 'block'; - uiContainer.focus(); + optionsPanel.style.display = 'block'; + optionsPanel.focus(); } else { console.info('Hiding!'); - uiContainer.style.display = 'none'; - uiContainer.blur(); + optionsPanel.style.display = 'none'; + optionsPanel.blur(); } } return false; From 0f9feb9a27bbd38ec8d9e311f6bad6a35cad3292 Mon Sep 17 00:00:00 2001 From: throwaway96 <68320646+throwaway96@users.noreply.github.com> Date: Sun, 24 Mar 2024 23:01:26 -0400 Subject: [PATCH 2/3] streamline handling of color buttons Provide a function for decoding rather than using raw character codes. --- src/ui.js | 67 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/ui.js b/src/ui.js index afc5102c..c017284b 100644 --- a/src/ui.js +++ b/src/ui.js @@ -8,6 +8,35 @@ window.__spatialNavigation__.keyMode = 'NONE'; const ARROW_KEY_CODE = { 37: 'left', 38: 'up', 39: 'right', 40: 'down' }; +// Red, Green, Yellow, Blue +// 403, 404, 405, 406 +// ---, 172, 170, 191 +const colorCodeMap = new Map([ + [403, 'red'], + + [404, 'green'], + [172, 'green'], + + [405, 'yellow'], + [170, 'yellow'], + + [406, 'blue'], + [191, 'blue'] +]); + +/** + * Returns the name of the color button associated with a code or null if not a color button. + * @param {number} charCode KeyboardEvent.charCode property from event + * @returns {string | null} Color name or null + */ +function getKeyColor(charCode) { + if (colorCodeMap.has(charCode)) { + return colorCodeMap.get(charCode); + } + + return null; +} + function createConfigCheckbox(key) { const elmInput = document.createElement('input'); elmInput.type = 'checkbox'; @@ -46,20 +75,24 @@ function createOptionsPanel() { 'keydown', (evt) => { console.info('Options panel key event:', evt.type, evt.charCode); - if (evt.charCode !== 404 && evt.charCode !== 172) { - if (evt.keyCode in ARROW_KEY_CODE) { - navigate(ARROW_KEY_CODE[evt.keyCode]); - } else if (evt.keyCode === 13) { - // "OK" button - document.querySelector(':focus').click(); - } else if (evt.keyCode === 27) { - // Back button - elmContainer.style.display = 'none'; - elmContainer.blur(); - } - evt.preventDefault(); - evt.stopPropagation(); + + if (getKeyColor(evt.charCode) === 'green') { + return; } + + if (evt.keyCode in ARROW_KEY_CODE) { + navigate(ARROW_KEY_CODE[evt.keyCode]); + } else if (evt.keyCode === 13) { + // "OK" button + document.querySelector(':focus').click(); + } else if (evt.keyCode === 27) { + // Back button + elmContainer.style.display = 'none'; + elmContainer.blur(); + } + + evt.preventDefault(); + evt.stopPropagation(); }, true ); @@ -101,10 +134,13 @@ const eventHandler = (evt) => { evt.keyCode, evt.defaultPrevented ); - if (evt.charCode == 404 || evt.charCode == 172) { + + if (getKeyColor(evt.charCode) === 'green') { console.info('Taking over!'); + evt.preventDefault(); evt.stopPropagation(); + if (evt.type === 'keydown') { if (optionsPanel.style.display === 'none') { console.info('Showing and focusing!'); @@ -121,9 +157,6 @@ const eventHandler = (evt) => { return true; }; -// Red, Green, Yellow, Blue -// 403, 404, 405, 406 -// ---, 172, 170, 191 document.addEventListener('keydown', eventHandler, true); document.addEventListener('keypress', eventHandler, true); document.addEventListener('keyup', eventHandler, true); From a271695bda89d68fe53713bae89b79856d320336 Mon Sep 17 00:00:00 2001 From: throwaway96 <68320646+throwaway96@users.noreply.github.com> Date: Sun, 24 Mar 2024 23:37:39 -0400 Subject: [PATCH 3/3] create function to show/hide the options panel The new function is available globally as ytaf_showOptionsPanel() for console use. --- src/ui.js | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/ui.js b/src/ui.js index c017284b..06b7788f 100644 --- a/src/ui.js +++ b/src/ui.js @@ -87,8 +87,7 @@ function createOptionsPanel() { document.querySelector(':focus').click(); } else if (evt.keyCode === 27) { // Back button - elmContainer.style.display = 'none'; - elmContainer.blur(); + showOptionsPanel(false); } evt.preventDefault(); @@ -126,6 +125,30 @@ function createOptionsPanel() { const optionsPanel = createOptionsPanel(); document.body.appendChild(optionsPanel); +let optionsPanelVisible = false; + +/** + * Show or hide the options panel. + * @param {boolean} [visible=true] Whether to show the options panel. + */ +function showOptionsPanel(visible) { + visible ??= true; + + if (visible && !optionsPanelVisible) { + console.info('Showing and focusing options panel!'); + optionsPanel.style.display = 'block'; + optionsPanel.focus(); + optionsPanelVisible = true; + } else if (!visible && optionsPanelVisible) { + console.info('Hiding options panel!'); + optionsPanel.style.display = 'none'; + optionsPanel.blur(); + optionsPanelVisible = false; + } +} + +window.ytaf_showOptionsPanel = showOptionsPanel; + const eventHandler = (evt) => { console.info( 'Key event:', @@ -142,15 +165,8 @@ const eventHandler = (evt) => { evt.stopPropagation(); if (evt.type === 'keydown') { - if (optionsPanel.style.display === 'none') { - console.info('Showing and focusing!'); - optionsPanel.style.display = 'block'; - optionsPanel.focus(); - } else { - console.info('Hiding!'); - optionsPanel.style.display = 'none'; - optionsPanel.blur(); - } + // Toggle visibility. + showOptionsPanel(!optionsPanelVisible); } return false; }