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..06b7788f 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,33 +1,85 @@ /*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) { +// 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'; + elmInput.checked = configRead(key); + elmInput.addEventListener('change', (evt) => { + configWrite(key, evt.target.checked); + }); + + const elmLabel = document.createElement('label'); + elmLabel.appendChild(elmInput); + // Use non-breaking space (U+00A0) + elmLabel.appendChild(document.createTextNode('\u00A0' + getConfigDesc(key))); + + return elmLabel; +} + +function createOptionsPanel() { + const elmContainer = document.createElement('div'); + + elmContainer.classList.add('ytaf-ui-container'); + elmContainer.style['display'] = 'none'; + elmContainer.setAttribute('tabindex', 0); + + elmContainer.addEventListener( + 'focus', + () => console.info('Options panel focused!'), + true + ); + elmContainer.addEventListener( + 'blur', + () => console.info('Options panel blurred!'), + true + ); + + elmContainer.addEventListener( + 'keydown', + (evt) => { + console.info('Options panel key event:', evt.type, evt.charCode); + + if (getKeyColor(evt.charCode) === 'green') { + return; + } + if (evt.keyCode in ARROW_KEY_CODE) { navigate(ARROW_KEY_CODE[evt.keyCode]); } else if (evt.keyCode === 13) { @@ -35,98 +87,67 @@ uiContainer.addEventListener( document.querySelector(':focus').click(); } else if (evt.keyCode === 27) { // Back button - uiContainer.style.display = 'none'; - uiContainer.blur(); + showOptionsPanel(false); } + 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); - }); + }, + true + ); -uiContainer.querySelector('#__sponsorblock_sponsor').checked = configRead( - 'enableSponsorBlockSponsor' -); -uiContainer - .querySelector('#__sponsorblock_sponsor') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockSponsor', evt.target.checked); - }); + const elmHeading = document.createElement('h1'); + elmHeading.textContent = 'webOS YouTube Extended'; + elmContainer.appendChild(elmHeading); -uiContainer.querySelector('#__sponsorblock_intro').checked = configRead( - 'enableSponsorBlockIntro' -); -uiContainer - .querySelector('#__sponsorblock_intro') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockIntro', evt.target.checked); - }); + elmContainer.appendChild(createConfigCheckbox('enableAdBlock')); + elmContainer.appendChild(createConfigCheckbox('enableSponsorBlock')); -uiContainer.querySelector('#__sponsorblock_outro').checked = configRead( - 'enableSponsorBlockOutro' -); -uiContainer - .querySelector('#__sponsorblock_outro') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockOutro', evt.target.checked); - }); + const elmBlock = document.createElement('blockquote'); -uiContainer.querySelector('#__sponsorblock_interaction').checked = configRead( - 'enableSponsorBlockInteraction' -); -uiContainer - .querySelector('#__sponsorblock_interaction') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockInteraction', evt.target.checked); - }); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockSponsor')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockIntro')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockOutro')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockInteraction')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockSelfPromo')); + elmBlock.appendChild(createConfigCheckbox('enableSponsorBlockMusicOfftopic')); -uiContainer.querySelector('#__sponsorblock_selfpromo').checked = configRead( - 'enableSponsorBlockSelfPromo' -); -uiContainer - .querySelector('#__sponsorblock_selfpromo') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockSelfPromo', evt.target.checked); - }); + elmContainer.appendChild(elmBlock); -uiContainer.querySelector('#__sponsorblock_music_offtopic').checked = - configRead('enableSponsorBlockMusicOfftopic'); -uiContainer - .querySelector('#__sponsorblock_music_offtopic') - .addEventListener('change', (evt) => { - configWrite('enableSponsorBlockMusicOfftopic', evt.target.checked); - }); + 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); + +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( @@ -136,29 +157,22 @@ 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 (uiContainer.style.display === 'none') { - console.info('Showing and focusing!'); - uiContainer.style.display = 'block'; - uiContainer.focus(); - } else { - console.info('Hiding!'); - uiContainer.style.display = 'none'; - uiContainer.blur(); - } + // Toggle visibility. + showOptionsPanel(!optionsPanelVisible); } return false; } 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);