diff --git a/.vscode/settings.json b/.vscode/settings.json index 05ef9e2..c2dc21f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,10 +5,12 @@ "blackbar", "blackframe", "canvas", + "comms", "equalish", "insta", "recursing", "reddit", + "rescan", "resizer", "textbox", "videodata", diff --git a/CHANGELOG.md b/CHANGELOG.md index 117e1c5..346b51d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,16 @@ QoL improvements for me: * logging: allow to enable logging at will and export said logs to a file -### v4.3.1 (current) +### v.4.4.0 (current) + +* Russian users (and users of other non-latin keyboard layouts) can now use keyboard shortcuts by default, without having to rebind them manually. (NOTE: if you've changed keyboard shortcuts manually, this change will ***NOT*** be applied to your configuration.) +* NOTE: when using non-latin layouts, 'zoom' shortcut (`z` by default) uses the position of 'Y' on QWERTY layout. +* Ability to preserve aspect ratio between different videos (applies to current page and doesn't survive proper page reloads) +* Changing aspect ratio now resets zooming and panning. +* Fixed bug where keyboard shortcuts would work while typing in certain text fields +* Fixed minor bug with autodetection + +### v4.3.1 * Minor rework of settings page (actions & shortcuts section) * Fixed bug that prevented settings page from opening diff --git a/package.json b/package.json index 29760ca..9e6f7c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ultravidify", - "version": "4.3.1", + "version": "4.4.0", "description": "Aspect ratio fixer for youtube that works around some people's disability to properly encode 21:9 (and sometimes, 16:9) videos.", "author": "Tamius Han ", "scripts": { diff --git a/src/common/components/ActionAlt.vue b/src/common/components/ActionAlt.vue index 64e8de1..47cf2e2 100644 --- a/src/common/components/ActionAlt.vue +++ b/src/common/components/ActionAlt.vue @@ -5,7 +5,7 @@
- 🗙     + 🗙 🗙     🖉     {{action.name}} diff --git a/src/common/enums/crop-mode-persistence.enum.js b/src/common/enums/crop-mode-persistence.enum.js new file mode 100644 index 0000000..121eb44 --- /dev/null +++ b/src/common/enums/crop-mode-persistence.enum.js @@ -0,0 +1,9 @@ +var CropModePersistence = Object.freeze({ + Default: -1, + Disabled: 0, + UntilPageReload: 1, + CurrentSession: 2, + Forever: 3, +}); + +export default CropModePersistence; \ No newline at end of file diff --git a/src/common/js/KeyboardShortcutParser.js b/src/common/js/KeyboardShortcutParser.js index 03236fc..f87c86c 100644 --- a/src/common/js/KeyboardShortcutParser.js +++ b/src/common/js/KeyboardShortcutParser.js @@ -1,6 +1,6 @@ class KeyboardShortcutParser { static parseShortcut(keypress) { - var shortcutCombo = ''; + let shortcutCombo = ''; if (keypress.ctrlKey) { shortcutCombo += 'Ctrl + '; diff --git a/src/common/mixins/ComputeActionsMixin.js b/src/common/mixins/ComputeActionsMixin.js index 11689ee..d6726ab 100644 --- a/src/common/mixins/ComputeActionsMixin.js +++ b/src/common/mixins/ComputeActionsMixin.js @@ -1,7 +1,13 @@ export default { computed: { scopeActions: function() { - return this.settings.active.actions.filter(x => x.scopes[this.scope] && x.scopes[this.scope].show) || []; + return this.settings.active.actions.filter(x => { + if (! x.scopes) { + console.error('This action does not have a scope.', x); + return false; + } + return x.scopes[this.scope] && x.scopes[this.scope].show + }) || []; }, extensionActions: function(){ return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-extension-mode') || []; @@ -12,6 +18,9 @@ export default { aspectRatioActions: function(){ return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar') || []; }, + cropModePersistenceActions: function() { + return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-ar-persistence') || []; + }, stretchActions: function(){ return this.scopeActions.filter(x => x.cmd.length === 1 && x.cmd[0].action === 'set-stretch') || []; }, diff --git a/src/ext/conf/ActionList.js b/src/ext/conf/ActionList.js index 6457888..c41f906 100644 --- a/src/ext/conf/ActionList.js +++ b/src/ext/conf/ActionList.js @@ -2,6 +2,7 @@ import VideoAlignment from '../../common/enums/video-alignment.enum'; import Stretch from '../../common/enums/stretch.enum'; import ExtensionMode from '../../common/enums/extension-mode.enum'; import AspectRatio from '../../common/enums/aspect-ratio.enum'; +import CropModePersistence from '../../common/enums/crop-mode-persistence.enum'; var ActionList = { 'set-ar': { @@ -30,6 +31,33 @@ var ActionList = { page: true, } }, + 'set-ar-persistence': { + name: 'Set crop mode persistence', + args: [{ + name: 'Never persist', + arg: CropModePersistence.Disabled, + },{ + name: 'While on page', + arg: CropModePersistence.UntilPageReload, + },{ + name: 'Current session', + arg: CropModePersistence.CurrentSession, + },{ + name: 'Always persist', + arg: CropModePersistence.Forever, + }, { + name: 'Default', + arg: CropModePersistence.Default, + scopes: { + site: true, + } + }], + scopes: { + global: true, + site: true, + page: false, + } + }, 'set-stretch': { name: 'Set stretch', args: [{ diff --git a/src/ext/conf/ExtConfPatches.js b/src/ext/conf/ExtConfPatches.js index 6400829..4c2ff52 100644 --- a/src/ext/conf/ExtConfPatches.js +++ b/src/ext/conf/ExtConfPatches.js @@ -117,7 +117,132 @@ const ExtensionConfPatch = [ } } } + }, { + forVersion: '4.4.0', + updateFn: (userOptions, defaultOptions) => { + // remove 'press P to toggle panning mode' thing + const togglePan = userOptions.actions.find(x => x.cmd && x.cmd.length === 1 && x.cmd[0].action === 'toggle-pan'); + if (togglePan) { + togglePan.scopes = {}; + } + + // add new actions + userOptions.actions.push({ + name: 'Don\'t persist crop', + label: 'Never persist', + cmd: [{ + action: 'set-ar-persistence', + arg: 0, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Persist crop while on page', + label: 'Until page load', + cmd: [{ + action: 'set-ar-persistence', + arg: 1, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Persist crop for current session', + label: 'Current session', + cmd: [{ + action: 'set-ar-persistence', + arg: 2, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Persist until manually reset', + label: 'Always persist', + cmd: [{ + action: 'set-ar-persistence', + arg: 3, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Default crop persistence', + label: 'Default', + cmd: [{ + action: 'set-ar-persistence', + arg: -1, + }], + scopes: { + site: { + show: true, + }, + }, + playerUi: { + show: true, + } + }); + + // patch shortcuts for non-latin layouts, but only if the user hasn't changed default keys + for (const action of userOptions.actions) { + if (!action.cmd || action.cmd.length !== 1) { + continue; + } + try { + // if this fails, then action doesn't have keyboard shortcut associated with it, so we skip it + + const actionDefaults = defaultOptions.actions.find(x => x.cmd && x.cmd.length === 1 // (redundant, default actions have exactly 1 cmd in array) + && x.cmd[0].action === action.cmd[0].action + && x.scopes.page + && x.scopes.page.shortcut + && x.scopes.page.shortcut.length === 1 + && x.scopes.page.shortcut[0].key === action.scopes.page.shortcut[0].key // this can throw exception, and it's okay + ); + if (actionDefaults === undefined) { + continue; + } + // update 'code' property for shortcut + action.scopes.page.shortcut[0]['code'] = actionDefaults.scopes.page.shortcut[0].code; + } catch (e) { + continue; + } + } + } } ]; + export default ExtensionConfPatch; \ No newline at end of file diff --git a/src/ext/conf/ExtensionConf.js b/src/ext/conf/ExtensionConf.js index fe99216..f014e23 100644 --- a/src/ext/conf/ExtensionConf.js +++ b/src/ext/conf/ExtensionConf.js @@ -5,6 +5,7 @@ import Stretch from '../../common/enums/stretch.enum'; import ExtensionMode from '../../common/enums/extension-mode.enum'; import AntiGradientMode from '../../common/enums/anti-gradient-mode.enum'; import AspectRatio from '../../common/enums/aspect-ratio.enum'; +import CropModePersistence from '../../common/enums/crop-mode-persistence.enum'; if(Debug.debug) console.log("Loading: ExtensionConf.js"); @@ -191,6 +192,7 @@ var ExtensionConf = { label: 'Automatic', // example override, takes precedence over default label shortcut: [{ key: 'a', + code: 'KeyA', ctrlKey: false, metaKey: false, altKey: false, @@ -204,7 +206,7 @@ var ExtensionConf = { show: true, path: 'crop', }, - },{ + }, { name: 'Reset to default', label: 'Reset', cmd: [{ @@ -216,6 +218,7 @@ var ExtensionConf = { show: true, shortcut: [{ key: 'r', + code: 'KeyR', ctrlKey: false, metaKey: false, altKey: false, @@ -229,7 +232,7 @@ var ExtensionConf = { show: true, path: 'crop' }, - },{ + }, { name: 'Fit to width', label: 'Fit width', cmd: [{ @@ -241,6 +244,7 @@ var ExtensionConf = { show: true, shortcut: [{ key: 'w', + code: 'KeyW', ctrlKey: false, metaKey: false, altKey: false, @@ -254,7 +258,7 @@ var ExtensionConf = { show: true, path: 'crop' } - },{ + }, { name: 'Fit to height', label: 'Fit height', cmd: [{ @@ -266,6 +270,7 @@ var ExtensionConf = { show: true, shortcut: [{ key: 'e', + code: 'KeyE', ctrlKey: false, metaKey: false, altKey: false, @@ -279,7 +284,7 @@ var ExtensionConf = { show: true, path: 'crop' } - },{ + }, { name: 'Set aspect ratio to 16:9', label: '16:9', cmd: [{ @@ -292,6 +297,7 @@ var ExtensionConf = { show: true, shortcut: [{ key: 's', + code: 'KeyS', ctrlKey: false, metaKey: false, altKey: false, @@ -305,7 +311,7 @@ var ExtensionConf = { show: true, path: 'crop' } - },{ + }, { name: 'Set aspect ratio to 21:9 (2.39:1)', label: '21:9', cmd: [{ @@ -318,6 +324,7 @@ var ExtensionConf = { show: true, shortcut: [{ key: 'd', + code: 'KeyD', ctrlKey: false, metaKey: false, altKey: false, @@ -331,7 +338,7 @@ var ExtensionConf = { show: true, path: 'crop' } - },{ + }, { name: 'Set aspect ratio to 18:9', label: '18:9', cmd: [{ @@ -344,6 +351,7 @@ var ExtensionConf = { show: true, shortcut: [{ key: 'x', + code: 'KeyX', ctrlKey: false, metaKey: false, altKey: false, @@ -357,7 +365,94 @@ var ExtensionConf = { show: true, path: 'crop', } - },{ + }, { + name: 'Don\'t persist crop', + label: 'Never persist', + cmd: [{ + action: 'set-ar-persistence', + arg: CropModePersistence.Never, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Persist crop while on page', + label: 'Until page load', + cmd: [{ + action: 'set-ar-persistence', + arg: CropModePersistence.UntilPageReload, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Persist crop for current session', + label: 'Current session', + cmd: [{ + action: 'set-ar-persistence', + arg: CropModePersistence.CurrentSession, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Persist until manually reset', + label: 'Always persist', + cmd: [{ + action: 'set-ar-persistence', + arg: CropModePersistence.Forever, + }], + scopes: { + site: { + show: true, + }, + global: { + show: true, + } + }, + playerUi: { + show: true, + } + }, { + name: 'Default crop persistence', + label: 'Default', + cmd: [{ + action: 'set-ar-persistence', + arg: CropModePersistence.Default, + }], + scopes: { + site: { + show: true, + }, + }, + playerUi: { + show: true, + } + }, { name: 'Zoom in', label: 'Zoom', cmd: [{ @@ -369,6 +464,7 @@ var ExtensionConf = { show: false, shortcut: [{ key: 'z', + code: 'KeyY', ctrlKey: false, metaKey: false, altKey: false, @@ -381,7 +477,7 @@ var ExtensionConf = { playerUi: { show: false, } - },{ + }, { name: 'Zoom out', label: 'Unzoom', cmd: [{ @@ -393,6 +489,7 @@ var ExtensionConf = { show: false, shortcut: [{ key: 'u', + code: 'KeyU', ctrlKey: false, metaKey: false, altKey: false, @@ -405,32 +502,20 @@ var ExtensionConf = { playerUi: { show: false } - },{ + }, { name: 'Toggle panning mode', label: 'Toggle pan', cmd: [{ action: 'toggle-pan', arg: 'toggle' }], - scopes: { - page: { - show: true, - shortcut: [{ - key: 'p', - ctrlKey: false, - metaKey: false, - altKey: false, - shiftKey: false, - onKeyUp: true, - onKeyDown: false, - }] - } - }, playerUi: { show: true, path: 'zoom' + }, + scopes: { } - },{ + }, { name: 'Hold to pan', cmd: [{ action: 'pan', @@ -479,7 +564,7 @@ var ExtensionConf = { show: true, path: 'stretch' } - },{ + }, { name: 'Set stretch to "basic"', label: 'Basic stretch', cmd: [{ @@ -504,7 +589,7 @@ var ExtensionConf = { show: true, path: 'stretch' } - },{ + }, { name: 'Set stretch to "hybrid"', label: 'Hybrid stretch', cmd: [{ @@ -529,7 +614,7 @@ var ExtensionConf = { show: true, path: 'stretch' } - },{ + }, { name: 'Stretch only to hide thin borders', label: 'Thin borders only', cmd: [{ @@ -554,7 +639,7 @@ var ExtensionConf = { show: true, path: 'stretch' } - },{ + }, { name: 'Set stretch to default value', label: 'Default', cmd: [{ @@ -592,7 +677,7 @@ var ExtensionConf = { show: true, path: 'align' } - },{ + }, { name: 'Align video to center', label: 'Center', cmd: [{ @@ -614,7 +699,7 @@ var ExtensionConf = { show: true, path: 'align' } - },{ + }, { name: 'Align video to the right', label: 'Right', cmd: [{ @@ -636,7 +721,7 @@ var ExtensionConf = { show: true, path: 'align' } - },{ + }, { name: 'Use default alignment', label: 'Default', cmd: [{ @@ -669,7 +754,7 @@ var ExtensionConf = { show: true, } } - },{ + }, { name: 'Enable extension on whitelisted sites only', label: 'On whitelist only', cmd: [{ @@ -682,7 +767,7 @@ var ExtensionConf = { show: true } } - },{ + }, { name: 'Extension mode: use default settings', label: 'Default', cmd: [{ @@ -695,7 +780,7 @@ var ExtensionConf = { show: true } } - },{ + }, { name: 'Disable extension', label: 'Disable', cmd: [{ @@ -711,7 +796,7 @@ var ExtensionConf = { show: true, } } - },{ + }, { name: 'Enable automatic aspect ratio detection', label: 'Enable', cmd: [{ @@ -727,7 +812,7 @@ var ExtensionConf = { show: true } } - },{ + }, { name: 'Enable automatic aspect ratio detection on whitelisted sites only', label: 'On whitelist only', cmd: [{ @@ -740,7 +825,7 @@ var ExtensionConf = { show: true, } } - },{ + }, { name: 'Use default settings for automatic aspect ratio detection', label: 'Default', cmd: [{ @@ -753,7 +838,7 @@ var ExtensionConf = { show: true, } } - },{ + }, { name: 'Disable automatic aspect ratio detection', label: 'Disable', cmd: [{ @@ -792,7 +877,7 @@ var ExtensionConf = { show: true, } } - },{ + }, { name: 'Enable keyboard shortcuts on whitelisted sites only', label: 'On whitelist only', cmd: [{ @@ -804,7 +889,7 @@ var ExtensionConf = { show: true }, } - },{ + }, { name: 'Keyboard shortcuts mode: use default settings', label: 'Default', cmd: [{ @@ -816,7 +901,7 @@ var ExtensionConf = { show: true } } - },{ + }, { name: 'Disable keyboard shortcuts', label: 'Disable', cmd: [{ diff --git a/src/ext/lib/ActionHandler.js b/src/ext/lib/ActionHandler.js index 791e72b..4685219 100644 --- a/src/ext/lib/ActionHandler.js +++ b/src/ext/lib/ActionHandler.js @@ -133,28 +133,31 @@ class ActionHandler { // don't do shit on invalid value of state } - preventAction() { + preventAction(event) { var activeElement = document.activeElement; if(this.logger.canLog('keyboard')) { this.logger.pause(); // temp disable to avoid recursing; - + const preventAction = this.preventAction(); + this.logger.resume(); // undisable + this.logger.log('info', 'keyboard', "[ActionHandler::preventAction] Testing whether we're in a textbox or something. Detailed rundown of conditions:\n" + "is full screen? (yes->allow):", PlayerData.isFullScreen(), "\nis tag one of defined inputs? (yes->prevent):", this.inputs.indexOf(activeElement.tagName.toLocaleLowerCase()) !== -1, "\nis role = textbox? (yes -> prevent):", activeElement.getAttribute("role") === "textbox", "\nis type === 'text'? (yes -> prevent):", activeElement.getAttribute("type") === "text", + "\nevent.target.isContentEditable? (yes -> prevent):", event.target.isContentEditable, "\nis keyboard local disabled? (yes -> prevent):", this.keyboardLocalDisabled, "\nis keyboard enabled in settings? (no -> prevent)", this.settings.keyboardShortcutsEnabled(window.location.hostname), - "\nwill the action be prevented? (yes -> prevent)", this.preventAction(), + "\nwill the action be prevented? (yes -> prevent)", preventAction, "\n-----------------{ extra debug info }-------------------", "\ntag name? (lowercase):", activeElement.tagName, activeElement.tagName.toLocaleLowerCase(), "\nrole:", activeElement.getAttribute('role'), "\ntype:", activeElement.getAttribute('type'), - "insta-fail inputs:", this.inputs + "\ninsta-fail inputs:", this.inputs, + "\nevent:", event, + "\nevent.target:", event.target ); - - this.logger.resume(); // undisable } // lately youtube has allowed you to read and write comments while watching video in @@ -175,25 +178,49 @@ class ActionHandler { if (activeElement.getAttribute("role") === "textbox") { return true; } + if (event.target.isContentEditable) { + return true; + } if (activeElement.getAttribute("type") === "text") { return true; } return false; } - isActionMatch(shortcut, event) { + isLatin(key) { + return 'abcdefghijklmnopqrstuvwxyz,.-+1234567890'.indexOf(key.toLocaleLowerCase()) !== -1; + } + + isActionMatchStandard(shortcut, event) { return shortcut.key === event.key && shortcut.ctrlKey === event.ctrlKey && shortcut.metaKey === event.metaKey && shortcut.altKey === event.altKey && shortcut.shiftKey === event.shiftKey } + isActionMatchKeyCode(shortcut, event) { + return shortcut.code === event.code && + shortcut.ctrlKey === event.ctrlKey && + shortcut.metaKey === event.metaKey && + shortcut.altKey === event.altKey && + shortcut.shiftKey === event.shiftKey + } + + isActionMatch(shortcut, event, isLatin = true) { + // ASCII and symbols fall back to key code matching, because we don't know for sure that + // regular matching by key is going to work + return isLatin ? + this.isActionMatchStandard(shortcut, event) : + this.isActionMatchStandard(shortcut, event) || this.isActionMatchKeyCode(shortcut, event); + } execAction(actions, event, videoData) { this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] Trying to find and execute action for event. Actions/event: ", "color: #ff0", actions, event); + const isLatin = event.key ? this.isLatin(event.key) : true; + for (var action of actions) { - if (this.isActionMatch(action.shortcut, event)) { + if (this.isActionMatch(action.shortcut, event, isLatin)) { this.logger.log('info', 'keyboard', "%c[ActionHandler::execAction] found an action associated with keypress/event: ", "color: #ff0", action); for (var cmd of action.cmd) { @@ -228,8 +255,15 @@ class ActionHandler { this.settings.active.sites[site].arStatus = cmd.arg; } else if (cmd.action === 'set-keyboard') { this.settings.active.sites[site].keyboardShortcutsEnabled = cmd.arg; + } else if (cmd.action === 'set-ar-persistence') { + this.settings.active.sites[site]['cropModePersistence'] = cmd.arg; + this.pageInfo.setArPersistence(cmd.arg); + this.settings.saveWithoutReload(); + } + + if (cmd.action !== 'set-ar-persistence') { + this.settings.save(); } - this.settings.save(); } } @@ -244,7 +278,7 @@ class ActionHandler { handleKeyup(event) { this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeyup] we pressed a key: ", "color: #ff0", event.key , " | keyup: ", event.keyup, "event:", event); - if (this.preventAction()) { + if (this.preventAction(event)) { this.logger.log('info', 'keyboard', "[ActionHandler::handleKeyup] we are in a text box or something. Doing nothing."); return; } @@ -255,7 +289,7 @@ class ActionHandler { handleKeydown(event) { this.logger.log('info', 'keyboard', "%c[ActionHandler::handleKeydown] we pressed a key: ", "color: #ff0", event.key , " | keydown: ", event.keydown, "event:", event) - if (this.preventAction()) { + if (this.preventAction(event)) { this.logger.log('info', 'keyboard', "[ActionHandler::handleKeydown] we are in a text box or something. Doing nothing."); return; } diff --git a/src/ext/lib/Settings.js b/src/ext/lib/Settings.js index 0caed9e..173f380 100644 --- a/src/ext/lib/Settings.js +++ b/src/ext/lib/Settings.js @@ -6,6 +6,7 @@ import ObjectCopy from '../lib/ObjectCopy'; import Stretch from '../../common/enums/stretch.enum'; import VideoAlignment from '../../common/enums/video-alignment.enum'; import ExtensionConfPatch from '../conf/ExtConfPatches'; +import CropModePersistence from '../../common/enums/crop-mode-persistence.enum'; @@ -26,42 +27,29 @@ class Settings { const ths = this; - if(currentBrowser.firefox) { - browser.storage.onChanged.addListener( (changes, area) => { - this.logger.log('info', 'settings', "[Settings::] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area); - if (changes['uwSettings'] && changes['uwSettings'].newValue) { - this.logger.log('info', 'settings',"[Settings::] new settings object:", JSON.parse(changes.uwSettings.newValue)); - } - if(changes['uwSettings'] && changes['uwSettings'].newValue) { - ths.setActive(JSON.parse(changes.uwSettings.newValue)); - } - - if(this.updateCallback) { - try { - updateCallback(ths); - } catch (e) { - this.logger.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED.") - } - } - }); + if (currentBrowser.firefox) { + browser.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)}); } else if (currentBrowser.chrome) { - chrome.storage.onChanged.addListener( (changes, area) => { - this.logger.log('info', 'settings', "[Settings::] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area); - if (changes['uwSettings'] && changes['uwSettings'].newValue) { - this.logger.log('info', 'settings',"[Settings::] new settings object:", JSON.parse(changes.uwSettings.newValue)); - } - if(changes['uwSettings'] && changes['uwSettings'].newValue) { - ths.setActive(JSON.parse(changes.uwSettings.newValue)); - } + chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)}); + } + } - if(this.updateCallback) { - try { - updateCallback(ths); - } catch (e) { - this.logger.log('error', 'settings',"[Settings] CALLING UPDATE CALLBACK FAILED.") - } - } - }); + storageChangeListener(changes, area) { + this.logger.log('info', 'settings', "[Settings::] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area); + if (changes['uwSettings'] && changes['uwSettings'].newValue) { + this.logger.log('info', 'settings',"[Settings::] new settings object:", JSON.parse(changes.uwSettings.newValue)); + } + const parsedSettings = JSON.parse(changes.uwSettings.newValue); + if(changes['uwSettings'] && changes['uwSettings'].newValue) { + this.setActive(parsedSettings); + } + + if(!parsedSettings.preventReload && this.updateCallback) { + try { + updateCallback(ths); + } catch (e) { + this.logger.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED.") + } } } @@ -156,8 +144,23 @@ class Settings { // apply all remaining patches this.logger.log('info', 'settings', `[Settings::applySettingsPatches] There are ${patches.length - index} settings patches to apply`); while (index < patches.length) { + const updateFn = patches[index].updateFn; delete patches[index].forVersion; - ObjectCopy.overwrite(this.active, patches[index]); + delete patches[index].updateFn; + + if (Object.keys(patches[index]).length > 0) { + ObjectCopy.overwrite(this.active, patches[index]); + } + if (updateFn) { + + try { + updateFn(this.active, this.getDefaultSettings()); + } catch (e) { + console.log("!!!!", e) + this.logger.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e); + } + } + index++; } } @@ -171,22 +174,22 @@ class Settings { // | needed. In this case, we assume we're on the current version const oldVersion = (settings && settings.version) || this.version; - if(Debug.debug) { + if (settings) { this.logger.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings, "\nlast saved with:", settings.version, "\ncurrent version:", this.version ); - - // if (Debug.flushStoredSettings) { - // this.logger.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd"); - // Debug.flushStoredSettings = false; // don't do it again this session - // this.active = this.getDefaultSettings(); - // this.active.version = this.version; - // this.set(this.active); - // return this.active; - // } } + // if (Debug.flushStoredSettings) { + // this.logger.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd"); + // Debug.flushStoredSettings = false; // don't do it again this session + // this.active = this.getDefaultSettings(); + // this.active.version = this.version; + // this.set(this.active); + // return this.active; + // } + // if there's no settings saved, return default settings. if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) { this.logger.log( @@ -279,8 +282,10 @@ class Settings { } } - async set(extensionConf) { - extensionConf.version = this.version; + async set(extensionConf, options) { + if (!options || !options.forcePreserveVersion) { + extensionConf.version = this.version; + } this.logger.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf) @@ -299,11 +304,17 @@ class Settings { this.active[prop] = value; } - async save() { + async save(options) { if (Debug.debug && Debug.storage) { console.log("[Settings::save] Saving active settings:", this.active); } + this.active.preventReload = undefined; + await this.set(this.active, options); + } + + async saveWithoutReload() { + this.active.preventReload = true; await this.set(this.active); } @@ -522,6 +533,15 @@ class Settings { return this.active.sites['@global'].stretch; } + getDefaultCropPersistenceMode(site) { + if (site && this.active.sites[site] && this.active.sites[site].cropModePersistence !== Stretch.Default) { + return this.active.sites[site].cropModePersistence; + } + + // persistence mode thing is missing from settings by default + return this.active.sites['@global'].cropModePersistence || CropModePersistence.Disabled; + } + getDefaultVideoAlignment(site) { if (site && this.active.sites[site] && this.active.sites[site].videoAlignment !== VideoAlignment.Default) { return this.active.sites[site].videoAlignment; diff --git a/src/ext/lib/ar-detect/ArDetector.js b/src/ext/lib/ar-detect/ArDetector.js index d68b6c0..9976501 100644 --- a/src/ext/lib/ar-detect/ArDetector.js +++ b/src/ext/lib/ar-detect/ArDetector.js @@ -492,6 +492,13 @@ class ArDetector { this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: trueAr}, {type: AspectRatio.Automatic, ratio: trueAr}); } + clearImageData(id) { + if (ArrayBuffer.transfer) { + ArrayBuffer.transfer(id, 0); + } + id = undefined; + } + frameCheck(){ if(! this.video){ this.logger.log('error', 'debug', `%c[ArDetect::frameCheck] <@${this.arid}> Video went missing. Destroying current instance of videoData.`); @@ -581,6 +588,7 @@ class ArDetector { this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Letterbox not detected in fast test. Letterbox is either gone or we manually corrected aspect ratio. Nothing will be done.`, "color: #fa3"); + this.clearImageData(imageData); return; } @@ -598,6 +606,7 @@ class ArDetector { // if both succeed, then aspect ratio hasn't changed. if (!guardLineOut.imageFail && !guardLineOut.blackbarFail) { this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] guardLine tests were successful. (no imagefail and no blackbarfail)\n`, "color: #afa", guardLineOut); + this.clearImageData(imageData); return; } @@ -617,6 +626,7 @@ class ArDetector { this.guardLine.reset(); this.noLetterboxCanvasReset = true; + this.clearImageData(imageData); return; } @@ -643,6 +653,7 @@ class ArDetector { triggerTimeout = this.getTimeout(baseTimeout, startTime); this.scheduleFrameCheck(triggerTimeout); + this.clearImageData(imageData); return; } } @@ -664,6 +675,8 @@ class ArDetector { // rob ni bil zaznan, zato ne naredimo ničesar. // no edge was detected. Let's leave things as they were this.logger.log('info', 'arDetect_verbose', `%c[ArDetect::frameCheck] Edge wasn't detected with findBars`, "color: #fa3", edgePost, "EdgeStatus.AR_KNOWN:", EdgeStatus.AR_KNOWN); + + this.clearImageData(imageData); return; } @@ -696,14 +709,16 @@ class ArDetector { } catch (e) { // edges weren't gucci, so we'll just reset // the aspect ratio to defaults - try { - this.guardline.reset(); - } catch (e) { - // guardline wasn't gucci either, but we'll just ignore - // that and reset the aspect ratio anyway - } - this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()}); + this.logger.log('error', 'arDetect', `%c[ArDetect::frameCheck] There was a problem setting blackbar. Doing nothing. Error:`, e); + + this.guardline.reset(); + // WE DO NOT RESET ASPECT RATIO HERE IN CASE OF PROBLEMS, CAUSES UNWARRANTED RESETS: + // (eg. here: https://www.youtube.com/watch?v=nw5Z93Yt-UQ&t=410) + // + // this.conf.resizer.setAr({type: AspectRatio.Automatic, ratio: this.getDefaultAr()}); } + + this.clearImageData(imageData); } resetBlackLevel(){ diff --git a/src/ext/lib/ar-detect/GuardLine.js b/src/ext/lib/ar-detect/GuardLine.js index ca03448..de95cf6 100644 --- a/src/ext/lib/ar-detect/GuardLine.js +++ b/src/ext/lib/ar-detect/GuardLine.js @@ -34,7 +34,7 @@ class GuardLine { // to odstrani vse neveljavne nastavitve in vse možnosti, ki niso smiselne // this removes any configs with invalid values or values that dont make sense if (bbTop < 0 || bbBottom >= this.conf.canvas.height ){ - throw "INVALID_SETTINGS_IN_GUARDLINE" + throw {error: "INVALID_SETTINGS_IN_GUARDLINE", bbTop, bbBottom} } this.blackbar = { diff --git a/src/ext/lib/comms/CommsClient.js b/src/ext/lib/comms/CommsClient.js index 4b3d2e4..cd8c7d9 100644 --- a/src/ext/lib/comms/CommsClient.js +++ b/src/ext/lib/comms/CommsClient.js @@ -92,6 +92,8 @@ class CommsClient { this.pageInfo.setManualTick(message.arg); } else if (message.cmd === 'autoar-tick') { this.pageInfo.tick(); + } else if (message.cmd === 'set-ar-persistence') { + this.pageInfo.setArPersistence(message.arg); } } diff --git a/src/ext/lib/video-data/PageInfo.js b/src/ext/lib/video-data/PageInfo.js index de2720c..6e055a9 100644 --- a/src/ext/lib/video-data/PageInfo.js +++ b/src/ext/lib/video-data/PageInfo.js @@ -2,6 +2,7 @@ import Debug from '../../conf/Debug'; import VideoData from './VideoData'; import RescanReason from './enums/RescanReason'; import AspectRatio from '../../../common/enums/aspect-ratio.enum'; +import CropModePersistence from '../../../common/enums/crop-mode-persistence.enum'; if(Debug.debug) console.log("Loading: PageInfo.js"); @@ -20,23 +21,38 @@ class PageInfo { this.lastUrl = window.location.href; this.extensionMode = extensionMode; this.readOnly = readOnly; - + this.defaultCrop = undefined; + this.currentCrop = undefined; if (comms){ this.comms = comms; } - // request inject css immediately try { + // request inject css immediately const playerStyleString = this.settings.active.sites[window.location.host].css.replace('\\n', ''); this.comms.sendMessage({ cmd: 'inject-css', cssString: playerStyleString }); } catch (e) { - // do nothing. It's ok if there's no special settings for the player element + // do nothing. It's ok if there's no special settings for the player element or crop persistence } + // try getting default crop immediately. + const cropModePersistence = this.settings.getDefaultCropPersistenceMode(window.location.host); + + // try { + // if (cropModePersistence === CropModePersistence.Forever) { + // this.defaultCrop = this.settings.active.sites[window.location.host].defaultCrop; + // } else if (cropModePersistence === CropModePersistence.CurrentSession) { + // this.defaultCrop = JSON.parse(sessionStorage.getItem('uw-crop-mode-session-persistence')); + // } + // } catch (e) { + // // do nothing. It's ok if there's no special settings for the player element or crop persistence + // } + this.currentCrop = this.defaultCrop; + this.rescan(RescanReason.PERIODIC); this.scheduleUrlCheck(); @@ -197,11 +213,17 @@ class PageInfo { try { v = new VideoData(video, this.settings, this); - if (!v.invalid) { - v.initArDetection(); + + if (!this.defaultCrop) { + if (!v.invalid) { + v.initArDetection(); + } else { + this.logger.log('error', 'debug', 'Video is invalid. Aard not started.', video); + } } else { - this.logger.log('error', 'debug', 'Video is invalid. Aard not started.', video); + this.logger.log('info', 'debug', 'Default crop is specified for this site. Not starting aard.'); } + this.videos.push(v); } catch (e) { this.logger.log('error', 'debug', "rescan error: failed to initialize videoData. Skipping this video.",e); @@ -216,7 +238,7 @@ class PageInfo { // če smo ostali brez videev, potem odregistriraj stran. // če nismo ostali brez videev, potem registriraj stran. // - // if we're left withotu videos on the current page, we unregister the page. + // if we're left without videos on the current page, we unregister the page. // if we have videos, we call register. if (this.comms) { if (this.videos.length != oldVideoCount) { // only if number of videos changed, tho @@ -551,6 +573,53 @@ class PageInfo { setKeyboardShortcutsEnabled(state) { this.actionHandler.setKeybordLocal(state); } + + setArPersistence(persistenceMode) { + // name of this function is mildly misleading — we don't really _set_ ar persistence. (Ar persistence + // mode is set and saved via popup or keyboard shortcuts, if user defined them) We just save the current + // aspect ratio whenever aspect ratio persistence mode changes. + if (persistenceMode === CropModePersistence.CurrentSession) { + sessionStorage.setItem('uw-crop-mode-session-persistence', JSON.stringify(this.currentCrop)); + } else if (persistenceMode === CropModePersistence.Forever) { + if (this.settings.active.sites[window.location.host]) { + // | key may be missing, so we do this + this.settings.active.sites[window.location.host]['defaultAr'] = this.currentCrop; + } else { + this.settings.active.sites[window.location.host] = this.settings.getDefaultOption(); + this.settings.active.sites[window.location.host]['defaultAr'] = this.currentCrop; + } + + this.settings.saveWithoutReload(); + } + } + + updateCurrentCrop(ar) { + this.currentCrop = ar; + // This means crop persistance is disabled. If crop persistance is enabled, then settings for current + // site MUST exist (crop persistence mode is disabled by default) + + const cropModePersistence = this.settings.getDefaultCropPersistenceMode(window.location.host); + + if (cropModePersistence === CropModePersistence.Disabled) { + return; + } + + this.defaultCrop = ar; + + if (cropModePersistence === CropModePersistence.CurrentSession) { + sessionStorage.setItem('uw-crop-mode-session-persistence', JSON.stringify(ar)); + } else if (cropModePersistence === CropModePersistence.Forever) { + if (this.settings.active.sites[window.location.host]) { + // | key may be missing, so we do this + this.settings.active.sites[window.location.host]['defaultAr'] = ar; + } else { + this.settings.active.sites[window.location.host] = this.settings.getDefaultOption(); + this.settings.active.sites[window.location.host]['defaultAr'] = ar; + } + + this.settings.saveWithoutReload(); + } + } } export default PageInfo; diff --git a/src/ext/lib/video-data/VideoData.js b/src/ext/lib/video-data/VideoData.js index d98b9b5..d343f6d 100644 --- a/src/ext/lib/video-data/VideoData.js +++ b/src/ext/lib/video-data/VideoData.js @@ -44,6 +44,7 @@ class VideoData { }; this.resizer = new Resizer(this); + this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting // player dimensions need to be in: // this.player.dimensions @@ -59,6 +60,11 @@ class VideoData { // start fallback video/player size detection this.fallbackChangeDetection(); + + // force reload last aspect ratio (if default crop ratio exists) + if (this.pageInfo.defaultCrop) { + this.resizer.setAr(this.pageInfo.defaultCrop); + } } async fallbackChangeDetection() { @@ -80,21 +86,33 @@ class VideoData { } return; } + let confirmAspectRatioRestore = false; + for (let mutation of mutationList) { - if (mutation.type === 'attributes' - && mutation.attributeName === 'class' - && !context.video.classList.contains(this.userCssClassName) ) { - // force the page to include our class in classlist, if the classlist has been removed - // while classList.add() doesn't duplicate classes (does nothing if class is already added), - // we still only need to make sure we're only adding our class to classlist if it has been - // removed. classList.add() will _still_ trigger mutation (even if classlist wouldn't change). - // This is a problem because INFINITE RECURSION TIME, and we _really_ don't want that. - - context.video.classList.add(this.userCssClassName); - break; + if (mutation.type === 'attributes') { + if (mutation.attributeName === 'class') { + if(!context.video.classList.contains(this.userCssClassName) ) { + // force the page to include our class in classlist, if the classlist has been removed + // while classList.add() doesn't duplicate classes (does nothing if class is already added), + // we still only need to make sure we're only adding our class to classlist if it has been + // removed. classList.add() will _still_ trigger mutation (even if classlist wouldn't change). + // This is a problem because INFINITE RECURSION TIME, and we _really_ don't want that. + context.video.classList.add(this.userCssClassName); + } + // always trigger refresh on class changes, since change of classname might trigger change + // of the player size as well. + confirmAspectRatioRestore = true; + } + if (mutation.attributeName === 'style') { + confirmAspectRatioRestore = true; + } } } + if (!confirmAspectRatioRestore) { + return; + } + // adding player observer taught us that if element size gets triggered by a class, then // the 'style' attributes don't necessarily trigger. This means we also need to trigger // restoreAr here, in case video size was changed this way @@ -131,8 +149,9 @@ class VideoData { const pw = +(pcs.width.split('px')[0]); // TODO: check & account for panning and alignment - if (this.isWithin(vh, (ph - (translateY / 2)), 2) - && this.isWithin(vw, (pw - (translateX / 2)), 2)) { + if (transformMatrix[0] !== 'none' + && this.isWithin(vh, (ph - (translateY * 2)), 2) + && this.isWithin(vw, (pw - (translateX * 2)), 2)) { } else { this.player.forceRefreshPlayerElement(); this.restoreAr(); @@ -162,7 +181,7 @@ class VideoData { // throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}}; return; } - if(this.arDetector){ + if (this.arDetector){ this.arDetector.init(); } else{ @@ -177,8 +196,8 @@ class VideoData { // throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}}; return; } - if(!this.arDetector) { - this.arDetector.init(); + if (!this.arDetector) { + this.initArDetection(); } this.arDetector.start(); } diff --git a/src/ext/lib/video-transform/Resizer.js b/src/ext/lib/video-transform/Resizer.js index 4794d55..7cecc69 100644 --- a/src/ext/lib/video-transform/Resizer.js +++ b/src/ext/lib/video-transform/Resizer.js @@ -7,6 +7,7 @@ import ExtensionMode from '../../../common/enums/extension-mode.enum'; import Stretch from '../../../common/enums/stretch.enum'; import VideoAlignment from '../../../common/enums/video-alignment.enum'; import AspectRatio from '../../../common/enums/aspect-ratio.enum'; +import CropModePersistance from '../../../common/enums/crop-mode-persistence.enum'; if(Debug.debug) { console.log("Loading: Resizer.js"); @@ -119,7 +120,7 @@ class Resizer { } - setAr(ar, lastAr){ + setAr(ar, lastAr) { if (this.destroyed) { return; } @@ -130,26 +131,50 @@ class Resizer { return; } + const siteSettings = this.settings.active.sites[window.location.host]; + + // reset zoom, but only on aspect ratio switch. We also know that aspect ratio gets converted to + // AspectRatio.Fixed when zooming, so let's keep that in mind + if (ar.type !== AspectRatio.Fixed) { + this.zoom.reset(); + this.resetPan(); + } else if (ar.ratio !== this.lastAr.ratio) { + // we must check against this.lastAR.ratio because some calls provide same value for ar and lastAr + this.zoom.reset(); + this.resetPan(); + } + + // most everything that could go wrong went wrong by this stage, and returns can happen afterwards + // this means here's the optimal place to set or forget aspect ratio. Saving of current crop ratio + // is handled in pageInfo.updateCurrentCrop(), which also makes sure to persist aspect ratio if ar + // is set to persist between videos / through current session / until manual reset. + if (ar.type === AspectRatio.Automatic || + ar.type === AspectRatio.Reset || + ar.type === AspectRatio.Initial ) { + // reset/undo default + this.conf.pageInfo.updateCurrentCrop(undefined); + } else { + this.conf.pageInfo.updateCurrentCrop(ar); + } + if (ar.type === AspectRatio.Automatic || ar.type === AspectRatio.Reset && this.lastAr.type === AspectRatio.Initial) { // some sites do things that interfere with our site (and aspect ratio setting in general) // first, we check whether video contains anything we don't like - - const siteSettings = this.settings.active.sites[window.location.host]; if (siteSettings && siteSettings.autoarPreventConditions) { if (siteSettings.autoarPreventConditions.videoStyleString) { const styleString = (this.video.getAttribute('style') || '').split(';'); - + if (siteSettings.autoarPreventConditions.videoStyleString.containsProperty) { const bannedProperties = siteSettings.autoarPreventConditions.videoStyleString.containsProperty; for (const prop in bannedProperties) { for (const s of styleString) { if (s.trim().startsWith(prop)) { - + // check if css property has a list of allowed values: if (bannedProperties[prop].allowedValues) { const styleValue = s.split(':')[1].trim(); - + // check if property value is on the list of allowed values // if it's not, we aren't allowed to start aard if (bannedProperties[prop].allowedValues.indexOf(styleValue) === -1) { @@ -170,6 +195,8 @@ class Resizer { } } + + if (lastAr) { this.lastAr = this.calculateRatioForLegacyOptions(lastAr); ar = this.calculateRatioForLegacyOptions(ar); @@ -247,6 +274,10 @@ class Resizer { } + toFixedAr() { + this.lastAr.type = AspectRatio.Fixed; + } + resetLastAr() { this.lastAr = {type: AspectRatio.Initial}; } @@ -272,6 +303,9 @@ class Resizer { // dont allow weird floats this.videoAlignment = VideoAlignment.Center; + // because non-fixed aspect ratios reset panning: + this.toFixedAr(); + const player = this.conf.player.element; const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth; @@ -283,6 +317,11 @@ class Resizer { } } + resetPan() { + this.pan = {}; + this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.host); + } + setPan(relativeMousePosX, relativeMousePosY){ // relativeMousePos[X|Y] - on scale from 0 to 1, how close is the mouse to player edges. // use these values: top, left: 0, bottom, right: 1 diff --git a/src/ext/lib/video-transform/Scaler.js b/src/ext/lib/video-transform/Scaler.js index 4cb9ea6..577eccf 100644 --- a/src/ext/lib/video-transform/Scaler.js +++ b/src/ext/lib/video-transform/Scaler.js @@ -120,14 +120,31 @@ class Scaler { actualHeight: 0, // height of the video (excluding letterbox) when