diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a0ff76..870a3929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Added + - [#142](https://github.com/webosbrew/youtube-webos/pull/141): Blocked some additional ads (@throwaway96) +- [#144](https://github.com/webosbrew/youtube-webos/pull/144): Added support for config change listeners (@throwaway96) ### Fixed - [#103](https://github.com/webosbrew/youtube-webos/pull/103): Fixed SponsorBlock on videos with chapters (@alyyousuf7) - [#131](https://github.com/webosbrew/youtube-webos/pull/131): Fixed minor README issue (@ANewDawn) - [#141](https://github.com/webosbrew/youtube-webos/pull/141): Fixed black background behind video menu (@throwaway96; thanks to @reisxd) +- [#143](https://github.com/webosbrew/youtube-webos/pull/143): Fixed duplicate click bug (@throwaway96) ### Changed @@ -21,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - [#133](https://github.com/webosbrew/youtube-webos/pull/133): Changed various dev stuff (@throwaway96) - [#134](https://github.com/webosbrew/youtube-webos/pull/134): Refactored config/UI code (@throwaway96) - [#138](https://github.com/webosbrew/youtube-webos/pull/138): Changed webpack to production mode by default (@throwaway96) +- [#145](https://github.com/webosbrew/youtube-webos/pull/145): Made observing attributes optional in waitForChildAdd() (@throwaway96) ## [0.3.2] - 2024/03/07 diff --git a/src/config.js b/src/config.js index 49f9c485..3a6e976f 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,6 @@ const CONFIG_KEY = 'ytaf-configuration'; -export const configOptions = new Map([ +const configOptions = new Map([ ['enableAdBlock', { default: true, desc: 'Enable ad blocking' }], ['enableSponsorBlock', { default: true, desc: 'Enable SponsorBlock' }], [ @@ -40,6 +40,15 @@ const defaultConfig = (() => { return ret; })(); +/** @type {Record} as const */ +const configFrags = (() => { + let ret = {}; + for (const k of configOptions.keys()) { + ret[k] = new DocumentFragment(); + } + return ret; +})(); + function loadStoredConfig() { const storage = window.localStorage.getItem(CONFIG_KEY); @@ -63,7 +72,7 @@ function configExists(key) { return configOptions.has(key); } -export function getConfigDesc(key) { +export function configGetDesc(key) { if (!configExists(key)) { throw new Error('tried to get desc for unknown config key: ' + key); } @@ -95,7 +104,38 @@ export function configWrite(key, value) { throw new Error('tried to write unknown config key: ' + key); } - console.info('Setting key', key, 'to', value); + const oldValue = + localConfig[key] !== undefined ? localConfig[key] : defaultConfig[key]; + + console.info('Changing key', key, 'from', oldValue, 'to', value); localConfig[key] = value; window.localStorage[CONFIG_KEY] = JSON.stringify(localConfig); + + configFrags[key].dispatchEvent( + new CustomEvent('ytafConfigChange', { + detail: { key, newValue: value, oldValue } + }) + ); +} + +/** + * Add a listener for changes in the value of a specified config option + * @param {string} key Config option to monitor + * @param {(evt: Event) => void} callback Function to be called on change + */ +export function configAddChangeListener(key, callback) { + const frag = configFrags[key]; + + frag.addEventListener('ytafConfigChange', callback); +} + +/** + * Remove a listener for changes in the value of a specified config option + * @param {string} key Config option to monitor + * @param {(evt: Event) => void} callback Function to be called on change + */ +export function configRemoveChangeListener(key, callback) { + const frag = configFrags[key]; + + frag.removeEventListener('ytafConfigChange', callback); } diff --git a/src/ui.js b/src/ui.js index 16dcf4cc..3aa1aac6 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,7 +1,7 @@ /*global navigate*/ import './spatial-navigation-polyfill.js'; import './ui.css'; -import { configRead, configWrite, getConfigDesc } from './config.js'; +import { configRead, configWrite, configGetDesc } from './config.js'; // We handle key events ourselves. window.__spatialNavigation__.keyMode = 'NONE'; @@ -41,14 +41,22 @@ function createConfigCheckbox(key) { const elmInput = document.createElement('input'); elmInput.type = 'checkbox'; elmInput.checked = configRead(key); - elmInput.addEventListener('change', (evt) => { + + /** @type {(evt: Event) => void} */ + const changeHandler = (evt) => { configWrite(key, evt.target.checked); + }; + + elmInput.addEventListener('change', changeHandler); + + configAddChangeListener(key, (evt) => { + elmInput.checked = evt.detail.newValue; }); const elmLabel = document.createElement('label'); elmLabel.appendChild(elmInput); // Use non-breaking space (U+00A0) - elmLabel.appendChild(document.createTextNode('\u00A0' + getConfigDesc(key))); + elmLabel.appendChild(document.createTextNode('\u00A0' + configGetDesc(key))); return elmLabel; } @@ -84,7 +92,13 @@ function createOptionsPanel() { navigate(ARROW_KEY_CODE[evt.keyCode]); } else if (evt.keyCode === 13) { // "OK" button - document.querySelector(':focus').click(); + + // The YouTube app generates these "OK" events from clicks (including + // with the Magic Remote), and we don't want to send a duplicate click + // event for those. It seems isTrusted is only true for "real" events. + if (evt.isTrusted === true) { + document.activeElement.click(); + } } else if (evt.keyCode === 27) { // Back button showOptionsPanel(false); diff --git a/src/userScript.js b/src/userScript.js index 0ee2da5f..cd550219 100644 --- a/src/userScript.js +++ b/src/userScript.js @@ -21,7 +21,8 @@ import './ui.js'; /** @type {HTMLVideoElement} */ const video = await waitForChildAdd( document.body, - (node) => node instanceof HTMLVideoElement + (node) => node instanceof HTMLVideoElement, + false ); const playerCtrlObs = new MutationObserver(() => { diff --git a/src/utils.js b/src/utils.js index a5707c76..55519f00 100644 --- a/src/utils.js +++ b/src/utils.js @@ -87,14 +87,24 @@ export function handleLaunch(params) { } /** - * Wait for a child element to be added that holds true for a predicate - * @template T - * @param {Element} parent - * @param {(node: Node) => node is T} predicate - * @param {AbortSignal=} abortSignal - * @return {Promise} + * Wait for a child element to be added for which a predicate is true. + * + * When `observeAttributes` is false, the predicate is checked only when a node + * is first added. If you want the predicate to run every time an attribute is + * modified, set `observeAttributes` to true. + * @template {Node} T + * @param {Element} parent Root of tree to watch + * @param {(node: Node) => node is T} predicate Function that checks whether its argument is the desired element + * @param {boolean} observeAttributes Also run predicate on attribute changes + * @param {AbortSignal=} abortSignal Signal that can be used to stop waiting + * @return {Promise} Matched element */ -export async function waitForChildAdd(parent, predicate, abortSignal) { +export async function waitForChildAdd( + parent, + predicate, + observeAttributes, + abortSignal +) { return new Promise((resolve, reject) => { const obs = new MutationObserver((mutations) => { for (const mut of mutations) { @@ -128,6 +138,10 @@ export async function waitForChildAdd(parent, predicate, abortSignal) { }); } - obs.observe(parent, { subtree: true, attributes: true, childList: true }); + obs.observe(parent, { + subtree: true, + attributes: observeAttributes, + childList: true + }); }); }