diff --git a/.babelrc b/.babelrc index 1cccc7f..eb5b0c1 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "plugins": [ - "@babel/plugin-proposal-optional-chaining" + "@babel/plugin-proposal-class-properties" ], "presets": [ ["@babel/preset-env", { @@ -8,7 +8,7 @@ "targets": { "esmodules": true, }, - }] + }], ] } // { diff --git a/.gitignore b/.gitignore index 6578331..7a50513 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ build/ *.pem *.kate-swp + +src/res/img/git-ignore/ + +test/debug-configs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e293e5d..624f407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,16 @@ ## v4.x (current major) +### v4.5.1 + +* Fixed the misalignment issue on netflix ... hopefully. +* 'Site settings' tab should now work in Chrome as well ([#126](https://github.com/tamius-han/ultrawidify/issues/126)) +* Popup interface now refreshes properly ([#127](https://github.com/tamius-han/ultrawidify/issues/127)) +* Videos should now be scaled correctly when the display is narrower than video's native aspect ratio ([#118](https://github.com/tamius-han/ultrawidify/issues/118)) +* Edge users using CWS version of the extension get a very aggressive warning when trying to use the extension with Edge +* Fullscreen videos on streamable are aligned correctly ([#116](https://github.com/tamius-han/ultrawidify/issues/118)). +* **[4.5.1.1]** Streamable fix broke old.reddit + RES on embeds from v.redd.it and streamable.com. We're now using an alternative implementation. ([#128](https://github.com/tamius-han/ultrawidify/issues/128)) + ### v4.5.0 (Current) * Under the hood: migrated from vue2 to vue3, because optional chaining in templates is too OP. @@ -24,7 +34,6 @@ * (In Firefox) When extension was placed in overflow menu, the popup was cut off. That should be fixed now. [#119](https://github.com/tamius-han/ultrawidify/issues/119) * The extension will now show a notification when autodetection can't run due to DRM * Videos on facebook and reddit no longer get shifted up and to the left for me (cropping most of the video off-screen), but I haven't been deliberately trying to fix that issue. If you experience that issue, please consider contacting me (via github or email) with a link to a problematic video. -* **[4.5.0.1]** Fixed the misalignment issue on netflix ... ### v4.4.10 diff --git a/README.md b/README.md index 2d3efae..a8d70fa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ## Super TL;DR: I'm just looking for the install links, thanks -[Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/), [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi), [Edge](https://microsoftedge.microsoft.com/addons/detail/lmpgpgechmkkkehkihpiddbcbgibokbi) (Chromium-based only) +[Firefox](https://addons.mozilla.org/en/firefox/addon/ultrawidify/), [Chrome](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi). + +**Microsoft Edge is not supported at this time, as Edge features some bugs that make it impossible for extension to work.** [Read more](https://github.com/tamius-han/ultrawidify/issues/117#issuecomment-747109695). There's also [nightly "builds"](https://stuff.lionsarch.tamius.net/ultrawidify/nightly/). @@ -235,7 +237,6 @@ However, I do plan on implementing this feature. Hopefully by the end of the yea - ## Plans for the future 1. Handle porting of extension settings between versions. @@ -254,7 +255,8 @@ However, I do plan on implementing this feature. Hopefully by the end of the yea [Latest stable for Chrome — download from Chrome store](https://chrome.google.com/webstore/detail/ultrawidify/dndehlekllfkaijdlokmmicgnlanfjbi) -[Latest stable for Edge — Download from Microsoft store](https://microsoftedge.microsoft.com/addons/detail/lmpgpgechmkkkehkihpiddbcbgibokbi) +Edge version is currently not available, as Edge has some bugs that prevent this extension from working correctly. [Read more](https://github.com/tamius-han/ultrawidify/issues/117#issuecomment-747109695) + ### Installing the current, github version @@ -268,7 +270,7 @@ Requirements: npm, node. 1. Clone this repo 2. run `npm install` -3. If using **Firefox,** run: `npm run watch:dev`. If using **Chrome,** run: `npm run watch-chrome:dev`. If using Edge, run: `npm run watch-edge:dev`. +3. If using **Firefox,** run: `npm run watch:dev`. If using **Chrome,** run: `npm run watch-chrome:dev`. TODO: see if #3 already loads the extension in FF diff --git a/package-lock.json b/package-lock.json index 28c0672..3da715c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -421,12 +421,111 @@ } }, "@babel/plugin-proposal-class-properties": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.10.4.tgz", - "integrity": "sha512-vhwkEROxzcHGNu2mzUC0OFFNXdZ4M23ib8aRRcJSsW8BZK9pQMD7QB7csl97NBbgGZO7ZyHUyKDnxzOaP4IrCg==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", + "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.10.4", + "@babel/helper-create-class-features-plugin": "^7.12.1", "@babel/helper-plugin-utils": "^7.10.4" + }, + "dependencies": { + "@babel/generator": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.11.tgz", + "integrity": "sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==", + "requires": { + "@babel/types": "^7.12.11", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", + "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "requires": { + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-member-expression-to-functions": "^7.12.1", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/helper-replace-supers": "^7.12.1", + "@babel/helper-split-export-declaration": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", + "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "requires": { + "@babel/types": "^7.12.7" + } + }, + "@babel/helper-replace-supers": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.11.tgz", + "integrity": "sha512-q+w1cqmhL7R0FNzth/PLLp2N+scXEK/L2AHbXUyydxp828F4FEa5WcVoqui9vFRiHDQErj9Zof8azP32uGVTRA==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.7", + "@babel/helper-optimise-call-expression": "^7.12.10", + "@babel/traverse": "^7.12.10", + "@babel/types": "^7.12.11" + }, + "dependencies": { + "@babel/helper-optimise-call-expression": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.10.tgz", + "integrity": "sha512-4tpbU0SrSTjjt65UMWSrUOPZTsgvPgGG4S8QSTNHacKzpS51IVWGDj0yCwyeZND/i+LSN2g/O63jEXEWm49sYQ==", + "requires": { + "@babel/types": "^7.12.10" + } + } + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" + }, + "@babel/parser": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", + "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==" + }, + "@babel/traverse": { + "version": "7.12.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz", + "integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.10", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.11.0", + "@babel/parser": "^7.12.10", + "@babel/types": "^7.12.10", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + }, + "dependencies": { + "@babel/helper-split-export-declaration": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz", + "integrity": "sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==", + "requires": { + "@babel/types": "^7.12.11" + } + } + } + }, + "@babel/types": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz", + "integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==", + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } } }, "@babel/plugin-proposal-dynamic-import": { @@ -9799,9 +9898,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash._reinterpolate": { "version": "3.0.0", diff --git a/package.json b/package.json index 41cd381..7da10e4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "start": "npm run dev" }, "dependencies": { + "@babel/plugin-proposal-class-properties": "^7.12.1", "@types/core-js": "^2.5.3", "@types/es6-promise": "^3.3.0", "@vue/cli": "^4.5.9", diff --git a/src/common/js/ChromeShittinessMitigations.js b/src/common/js/ChromeShittinessMitigations.js new file mode 100644 index 0000000..a1d9ca9 --- /dev/null +++ b/src/common/js/ChromeShittinessMitigations.js @@ -0,0 +1,33 @@ +/** + * For some reason, Chrome really doesn't like when chrome.runtime + * methods are wrapped inside a ES6 proxy object. If 'port' is a + * ES6 Proxy of a Port object that `chrome.runtime.connect()` creates, + * then Chrome will do bullshits like `port.sendMessage` and + * `port.onMessage.addListener` crashing your Vue3 UI with bullshits + * excuses, e.g. + * + * | TypeError: Illegal invocation. Function must be called on + * | an object of type Port + * + * which is some grade A bullshit because Firefox can handle that just + * fine. + * + * There's two ways how I could handle this: + * * Find out how to get the original object from the proxy Vue3 + * creates, which would take time and ruin my xmass holiday, or + * * make a global object with a passive-aggressive name and ignore + * the very real possibility that there's prolly a reason Chrome + * does things in its own very special(tm) way, as if it had one + * extra chromosome over Firefox. + * + * Easy chhoice, really. + */ +export class ChromeShittinessMitigations { + static port = null; + + static setProperty(property, value) { + ChromeShittinessMitigations[property] = value; + } +} + +export default ChromeShittinessMitigations; \ No newline at end of file diff --git a/src/ext/conf/BrowserDetect.js b/src/ext/conf/BrowserDetect.js index 0c28caa..d2d58e5 100644 --- a/src/ext/conf/BrowserDetect.js +++ b/src/ext/conf/BrowserDetect.js @@ -1,3 +1,5 @@ +import browser from "vuex-webextensions/dist/browser"; + if (process.env.CHANNEL !== 'stable') { console.info('Loaded BrowserDetect'); } @@ -9,6 +11,9 @@ const BrowserDetect = { edge: process.env.BROWSER === 'edge', processEnvBrowser: process.env.BROWSER, processEnvChannel: process.env.CHANNEL, + isEdgeUA: () => /Edg\/(\.?[0-9]*)*$/.test(window.navigator.userAgent), + getBrowserObj: () => { return process.env.BROWSER === 'firefox' ? browser : chrome; }, + getURL: (url) => { console.log('getting file:', url); console.log(process.env.BROWSER === 'firefox' ? browser.runtime.getURL(url) : chrome.runtime.getURL(url)); return process.env.BROWSER === 'firefox' ? browser.runtime.getURL(url) : chrome.runtime.getURL(url); }, } if (process.env.CHANNEL !== 'stable') { diff --git a/src/ext/conf/ExtConfPatches.js b/src/ext/conf/ExtConfPatches.js index d6b8df4..242e449 100644 --- a/src/ext/conf/ExtConfPatches.js +++ b/src/ext/conf/ExtConfPatches.js @@ -407,7 +407,7 @@ const ExtensionConfPatch = [ } } }, { - forVersion: '4.5.0.1', + forVersion: '4.5.1', updateFn: (userOptions, defaultOptions) => { for (const site in userOptions.sites) { try { @@ -418,6 +418,24 @@ const ExtensionConfPatch = [ } } } + }, { + forVersion: '4.5.1.1', + updateFn: (userOptions, defaultOptions) => { + if (!userOptions.sites['streamable.com']) { + userOptions.sites['streamable.com'] = { + mode: 3, + autoar: 3, + type: 'official', + stretch: -1, + videoAlignment: -1, + keyboardShortcutsEnabled: 0, + css: ".player {text-align: left}" + }; + } + if (!userOptions.sites['streamable.com'].css) { + userOptions.sites['streamable.com'].css = '.player {text-align: left}' + }; + } } ]; diff --git a/src/ext/conf/ExtensionConf.js b/src/ext/conf/ExtensionConf.js index 8485026..d8b3412 100644 --- a/src/ext/conf/ExtensionConf.js +++ b/src/ext/conf/ExtensionConf.js @@ -1032,7 +1032,7 @@ var ExtensionConf = { }, "www.netflix.com" : { mode: ExtensionMode.Enabled, - autoar: currentBrowser.firefox ? ExtensionMode.Enabled : ExtensionMode.Disabled, + autoar: ExtensionMode.Enabled, override: false, type: 'official', stretch: Stretch.Default, @@ -1086,6 +1086,15 @@ var ExtensionConf = { } } }, + "streamable.com": { + mode: 3, + autoar: 3, + type: 'official', + stretch: -1, + videoAlignment: -1, + keyboardShortcutsEnabled: 0, + css: ".player {text-align: left}" + }, "vimeo.com": { mode: 3, autoar: 3, diff --git a/src/ext/lib/Settings.js b/src/ext/lib/Settings.js index 3c873c3..61a3b40 100644 --- a/src/ext/lib/Settings.js +++ b/src/ext/lib/Settings.js @@ -7,6 +7,7 @@ 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'; +import BrowserDetect from '../conf/BrowserDetect'; if(process.env.CHANNEL !== 'stable'){ console.info("Loading Settings"); @@ -16,9 +17,10 @@ class Settings { constructor(options) { // Options: activeSettings, updateCallback, logger - this.logger = options.logger; - this.onSettingsChanged = options.onSettingsChanged; - this.active = options.activeSettings ?? undefined; + this.logger = options?.logger; + this.onSettingsChanged = options?.onSettingsChanged; + this.afterSettingsSaved = options?.afterSettingsSaved; + this.active = options?.activeSettings ?? undefined; this.default = ExtensionConf; this.default['version'] = this.getExtensionVersion(); this.useSync = false; @@ -35,27 +37,27 @@ class Settings { if (!changes.uwSettings) { return; } - this.logger.log('info', 'settings', "[Settings::] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", 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)); + // this.logger?.log('info', 'settings',"[Settings::] new settings object:", JSON.parse(changes.uwSettings.newValue)); // } const parsedSettings = JSON.parse(changes.uwSettings.newValue); this.setActive(parsedSettings); - this.logger.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged); + this.logger?.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged); if (!parsedSettings.preventReload && this.onSettingsChanged) { try { this.onSettingsChanged(); - - - - this.logger.log('info', 'settings', '[Settings] Update callback finished.') + this.logger?.log('info', 'settings', '[Settings] Update callback finished.') } catch (e) { - this.logger.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e) + this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e) } } + if (this.afterSettingsSaved) { + this.afterSettingsSaved(); + } } static getExtensionVersion() { @@ -143,12 +145,12 @@ class Settings { applySettingsPatches(oldVersion, patches) { let index = this.findFirstNecessaryPatch(oldVersion, patches); if (index === -1) { - this.logger.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.'); + this.logger?.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.'); return; } // apply all remaining patches - this.logger.log('info', 'settings', `[Settings::applySettingsPatches] There are ${patches.length - index} settings patches to apply`); + 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; @@ -162,7 +164,7 @@ class Settings { try { updateFn(this.active, this.getDefaultSettings()); } catch (e) { - this.logger.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e); + this.logger?.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e); } } @@ -180,14 +182,14 @@ class Settings { const oldVersion = (settings && settings.version) || this.version; if (settings) { - this.logger.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", 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"); + // 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; @@ -197,7 +199,7 @@ class Settings { // if there's no settings saved, return default settings. if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) { - this.logger.log( + this.logger?.log( 'info', 'settings', '[Settings::init] settings don\'t exist. Using defaults.\n#keys:', @@ -234,7 +236,7 @@ class Settings { // check if extension has been updated. If not, return settings as they were retrieved if (this.active.version === this.version) { - this.logger.log('info', 'settings', "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is."); + this.logger?.log('info', 'settings', "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is."); return this.active; } @@ -245,7 +247,7 @@ class Settings { // if extension has been updated, update existing settings with any options added in the // new version. In addition to that, we remove old keys that are no longer used. const patched = ObjectCopy.addNew(settings, this.default); - this.logger.log('info', 'settings',"[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default); + this.logger?.log('info', 'settings',"[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default); if (patched) { this.active = patched; @@ -278,7 +280,7 @@ class Settings { }); } - this.logger.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings)); + this.logger?.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings)); try { return JSON.parse(ret.uwSettings); @@ -317,11 +319,11 @@ class Settings { this.fixSitesSettings(extensionConf.sites); - this.logger.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf) + this.logger?.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf) - if (currentBrowser.firefox || currentBrowser.edge) { + if (BrowserDetect.firefox) { return browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)}); - } else if (currentBrowser.chrome) { + } else { return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)}); } } @@ -392,7 +394,7 @@ class Settings { site = window.location.hostname; if (!site) { - this.logger.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active); + this.logger?.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active); return ExtensionMode.Disabled; } } @@ -418,7 +420,7 @@ class Settings { } } catch(e){ - this.logger.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\n\nerror:", e, "\n\nSettings object:", this) + this.logger?.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\n\nerror:", e, "\n\nSettings object:", this) return ExtensionMode.Disabled; } @@ -430,7 +432,7 @@ class Settings { site = window.location.hostname; if (!site) { - this.logger.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active); + this.logger?.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active); return false; } } @@ -457,7 +459,7 @@ class Settings { return false; } } catch(e) { - this.logger.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\nSettings object:", this); + this.logger?.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\nSettings object:", this); return false; } } @@ -479,7 +481,7 @@ class Settings { return this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Enabled; } } catch (e) { - this.logger.log('info', 'settings',"[Settings.js::keyboardDisabled] something went wrong:", e); + this.logger?.log('info', 'settings',"[Settings.js::keyboardDisabled] something went wrong:", e); return false; } } @@ -498,7 +500,7 @@ class Settings { site = window.location.hostname; if (!site) { - this.logger.log('warn', ['settings', 'init', 'debug'], `[Settings::canStartAutoAr] No site — even window.location.hostname returned nothing!: ${window.location.hostname}`); + this.logger?.log('warn', ['settings', 'init', 'debug'], `[Settings::canStartAutoAr] No site — even window.location.hostname returned nothing!: ${window.location.hostname}`); return false; } } @@ -511,7 +513,7 @@ class Settings { // const csar = this.canStartAutoAr(site); // Debug.debug = true; - this.logger.log('info', ['settings', 'init', 'debug'], "[Settings::canStartAutoAr] ----------------\nCAN WE START AUTOAR ON SITE", site, + this.logger?.log('info', ['settings', 'init', 'debug'], "[Settings::canStartAutoAr] ----------------\nCAN WE START AUTOAR ON SITE", site, "?\n\nsettings.active.sites[site]=", this.active.sites[site], "settings.active.sites[@global]=", this.active.sites['@global'], "\nAutoar mode (global)?", this.active.sites['@global'].autoar, `\nAutoar mode (${site})`, this.active.sites[site] ? this.active.sites[site].autoar : '', @@ -521,18 +523,18 @@ class Settings { // if site is not defined, we use default mode: if (! this.active.sites[site]) { - this.logger.log('info', ['settings', 'aard', 'init', 'debug'], "[Settings::canStartAutoAr] Settings not defined for this site, returning defaults.", site, this.active.sites[site], this.active.sites); + this.logger?.log('info', ['settings', 'aard', 'init', 'debug'], "[Settings::canStartAutoAr] Settings not defined for this site, returning defaults.", site, this.active.sites[site], this.active.sites); return this.active.sites['@global'].autoar === ExtensionMode.Enabled; } if (this.active.sites['@global'].autoar === ExtensionMode.Enabled) { - this.logger.log('info', ['settings', 'aard', 'init', 'debug'], `[Settings::canStartAutoAr] Aard is enabled by default. Extension can run unless disabled for this site.`, this.active.sites[site].autoar); + this.logger?.log('info', ['settings', 'aard', 'init', 'debug'], `[Settings::canStartAutoAr] Aard is enabled by default. Extension can run unless disabled for this site.`, this.active.sites[site].autoar); return this.active.sites[site].autoar !== ExtensionMode.Disabled; } else if (this.active.sites['@global'].autoar === ExtensionMode.Whitelist) { - this.logger.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — can(not) start aard because extension is in whitelist mode, and this site is (not) equal to", ExtensionMode.Enabled) + this.logger?.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — can(not) start aard because extension is in whitelist mode, and this site is (not) equal to", ExtensionMode.Enabled) return this.active.sites[site].autoar === ExtensionMode.Enabled; } else { - this.logger.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — cannot start aard because extension is globally disabled") + this.logger?.log('info', ['settings', 'init', 'debug'], "canStartAutoAr — cannot start aard because extension is globally disabled") return false; } } diff --git a/src/ext/lib/ar-detect/ArDetector.js b/src/ext/lib/ar-detect/ArDetector.js index 715c976..48b5fe8 100644 --- a/src/ext/lib/ar-detect/ArDetector.js +++ b/src/ext/lib/ar-detect/ArDetector.js @@ -9,6 +9,7 @@ import GuardLine from './GuardLine'; import VideoAlignment from '../../../common/enums/video-alignment.enum'; import AspectRatio from '../../../common/enums/aspect-ratio.enum'; import {sleep} from '../../lib/Util'; +import BrowserDetect from '../../conf/BrowserDetect'; class ArDetector { @@ -550,6 +551,7 @@ class ArDetector { // special browsers require special tests if (this.hasDRM()) { + this.fallbackMode = false; throw 'Video is protected by DRM. Autodetection cannot run here.'; } this.fallbackMode = false; @@ -559,9 +561,21 @@ class ArDetector { // nothing to see here, really, if fallback mode isn't supported by browser if (!this.drmNotificationShown) { this.drmNotificationShown = true; + + // if we detect Edge, we'll throw the aggressive popup + this.conf.player.showEdgeNotification(); this.conf.player.showNotification('AARD_DRM'); + + this.conf.resizer.setAr({type: AspectRatio.Reset}); + return; + } + + // in case of DRM errors, we need to prevent the execution to reach the aspec + // ratio setting part of this function + if (e === 'Video is protected by DRM. Autodetection cannot run here.') { return; } + if (! this.canvasReadyForDrawWindow()) { // this means canvas needs to be resized, so we'll just re-run setup with all those new parameters this.stop(); diff --git a/src/ext/lib/uwui/FontLoader.js b/src/ext/lib/uwui/FontLoader.js new file mode 100644 index 0000000..ccf87b3 --- /dev/null +++ b/src/ext/lib/uwui/FontLoader.js @@ -0,0 +1,129 @@ +import BrowserDetect from '../../conf/BrowserDetect'; + +function url(file) { + return BrowserDetect.getURL(file); +} + +export default class FontLoader { + static loadFonts() { + const fontsStyleElement = document.createElement('style'); + fontsStyleElement.type = 'text/css'; + + fontsStyleElement.textContent = ` + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-thin.woff2')} format('woff2'); /* Super Modern Browsers */ + font-weight: 200; + font-style: normal; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-thin-italic.woff2')} format('woff2'); + font-weight: 200; + font-style: italic; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-extralight.woff2')} format('woff2'); + font-weight: 300; + font-style: normal; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-extralight-italic.woff2')} format('woff2'); + font-weight: 300; + font-style: italic; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-light.woff2')} format('woff2'); + font-weight: 400; + font-style: normal; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-light-italic.woff2')} format('woff2'); + font-weight: 400; + font-style: italic; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-regular.woff2')} format('woff2'); + font-weight: 500; + font-style: normal; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-italic.woff2')} format('woff2'); + font-weight: 500; + font-style: italic; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-semibold.woff2')} format('woff2'); + font-weight: 600; + font-style: normal; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-semibold-italic.woff2')} format('woff2'); + font-weight: 600; + font-style: italic; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-bold.woff2')} format('woff2'); + font-weight: 700; + font-style: normal; + } + + @font-face { + font-family: 'Overpass'; + src: ${url('res/fonts/overpass-webfont/overpass-bold-italic.woff2')} format('woff2'); + font-weight: 700; + font-style: italic; + } + + @font-face { + font-family: 'Overpass mono'; + src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-light.woff2')} format('woff2'); + font-weight: 300; + font-style: normal; + } + + @font-face { + font-family: 'Overpass mono'; + src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-regular.woff2')} format('woff2'); + font-weight: 400; + font-style: normal; + } + + @font-face { + font-family: 'Overpass mono'; + src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-semibold.woff2')} format('woff2'); + font-weight: 500; + font-style: normal; + } + + @font-face { + font-family: 'Overpass mono'; + src: ${url('res/fonts/overpass-mono-webfont/overpass-mono-bold.woff2')} format('woff2'); + font-weight: 600; + font-style: normal; + } + `; + + document.head.appendChild(fontsStyleElement); + console.log("font loaded!") + } +} diff --git a/src/ext/lib/uwui/PlayerNotificationUI.js b/src/ext/lib/uwui/PlayerNotificationUI.js index 0b0afc5..bf57ccf 100644 --- a/src/ext/lib/uwui/PlayerNotificationUI.js +++ b/src/ext/lib/uwui/PlayerNotificationUI.js @@ -50,13 +50,11 @@ class PlayerNotificationUi extends UI { }, mutations: { 'uw-set-notification'(state, payload) { - console.log('mutation!', state, payload); state['notificationConfig'] = payload; } }, actions: { 'uw-set-notification'({commit}, payload) { - console.log('action!', commit, payload); commit('uw-set-notification', payload); } } diff --git a/src/ext/lib/video-data/PlayerData.js b/src/ext/lib/video-data/PlayerData.js index 5de288d..4fbad9e 100644 --- a/src/ext/lib/video-data/PlayerData.js +++ b/src/ext/lib/video-data/PlayerData.js @@ -45,6 +45,7 @@ class PlayerData { this.extensionMode = videoData.extensionMode; this.invalid = false; this.element = this.getPlayer(); + this.notificationService = new PlayerNotificationUi(this.element, this.settings); this.ui = new PlayerUi(this.element, this.settings); this.dimensions = undefined; @@ -483,6 +484,15 @@ class PlayerData { showNotification(notificationId) { this.notificationService?.showNotification(notificationId); } + + /** + * NOTE: this method needs to be deleted once Edge gets its shit together. + */ + showEdgeNotification() { + if (BrowserDetect.isEdgeUA() && !this.settings.active.mutedNotifications?.browserSpecific?.edge?.brokenDrm?.[window.hostname]) { + this.ui = new PlayerUi(this.element, this.settings); + } + } } if (process.env.CHANNEL !== 'stable'){ diff --git a/src/ext/lib/video-data/VideoData.js b/src/ext/lib/video-data/VideoData.js index 25eb991..9e62f8f 100644 --- a/src/ext/lib/video-data/VideoData.js +++ b/src/ext/lib/video-data/VideoData.js @@ -56,7 +56,10 @@ class VideoData { try { await this.pageInfo.injectCss(` .uw-ultrawidify-base-wide-screen { - margin: 0px 0px 0px 0px !important; width: initial !important; align-self: start !important; justify-self: start !important; + margin: 0px 0px 0px 0px !important; + width: initial !important; + align-self: start !important; + justify-self: start !important; } `); } catch (e) { @@ -277,6 +280,54 @@ class VideoData { return a < b + diff && a > b - diff } + /** + * Gets the contents of the style attribute of the video element + * in a form of an object. + */ + getVideoStyle() { + // This will _always_ give us an array. Empty string gives an array + // that contains one element. That element is an empty string. + const styleArray = (this.video.getAttribute('style') || '').split(';'); + + const styleObject = {}; + + for (const style of styleArray) { + // not a valid CSS, so we skip those + if (style.indexOf(':') === -1) { + continue; + } + + // let's play _very_ safe + let [property, value] = style.split('!important')[0].split(':'); + value = value.trim(); + styleObject[property] = value; + } + + return styleObject; + } + + /** + * Some sites try to accomodate ultrawide users by "cropping" videos + * by setting 'style' attribute of the video element to 'height: X%', + * where 'X' is something greater than 100. + * + * This function gets that percentage and converts it into a factor. + */ + getHeightCompensationFactor() { + const heightStyle = this.getVideoStyle()?.height; + + if (!heightStyle || !heightStyle.endsWith('%')) { + return 1; + } + + const heightCompensationFactor = heightStyle.split('%')[0] / 100; + if (isNaN(heightCompensationFactor)) { + return 1; + } + return heightCompensationFactor; + } + + firstTimeArdInit(){ if(this.destroyed || this.invalid) { // throw {error: 'VIDEO_DATA_DESTROYED', data: {videoData: this}}; diff --git a/src/ext/lib/video-transform/Resizer.js b/src/ext/lib/video-transform/Resizer.js index 9f58811..c475e29 100644 --- a/src/ext/lib/video-transform/Resizer.js +++ b/src/ext/lib/video-transform/Resizer.js @@ -177,43 +177,41 @@ class Resizer { 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 - if (siteSettings?.autoarPreventConditions?.videoStyleString) { - const styleString = (this.video.getAttribute('style') || '').split(';'); + // 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 + // 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)) { + // 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 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) { - this.logger.log('error', 'debug', "%c[Resizer::setAr] video style contains forbidden css property/value combo: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.") - return; - } - } else { - // no allowed values, no problem. We have forbidden property - // and this means aard can't start. - this.logger.log('info', 'debug', "%c[Resizer::setAr] video style contains forbidden css property: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.") - return; - } - } - } - } - } - } - } - - + // // 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) { + // this.logger.log('error', 'debug', "%c[Resizer::setAr] video style contains forbidden css property/value combo: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.") + // return; + // } + // } else { + // // no allowed values, no problem. We have forbidden property + // // and this means aard can't start. + // this.logger.log('info', 'debug', "%c[Resizer::setAr] video style contains forbidden css property: ", "color: #900, background: #100", prop, " — we aren't allowed to start autoar.") + // return; + // } + // } + // } + // } + // } + // } + // } if (lastAr) { this.lastAr = this.calculateRatioForLegacyOptions(lastAr); @@ -229,11 +227,11 @@ class Resizer { this.lastAr = {type: ar.type, ratio: ar.ratio} } - if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatio.Reset) { - // don't actually apply or calculate css when using basic mode if not in fullscreen - // ... unless we're resetting the aspect ratio to original - return; - } + // if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatio.Reset) { + // // don't actually apply or calculate css when using basic mode if not in fullscreen + // // ... unless we're resetting the aspect ratio to original + // return; + // } if (! this.video) { this.conf.destroy(); @@ -259,7 +257,6 @@ class Resizer { var stretchFactors = this.scaler.calculateCrop(ar); - if(! stretchFactors || stretchFactors.error){ this.logger.log('error', 'debug', `[Resizer::setAr] failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error); if (stretchFactors?.error === 'no_video'){ @@ -297,7 +294,7 @@ class Resizer { var stretchFactors = this.stretcher.calculateBasicStretch(); this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic stretch. Stretch factors are:', stretchFactors); } else { - var stretchFactors = {xFactor: 1, yFactor: 1} + var stretchFactors = {xFactor: 1, yFactor: 1}; this.logger.log('error', 'debug', '[Resizer::setAr] Okay wtf happened? If you see this, something has gone wrong', stretchFactors,"\n------[ i n f o d u m p ]------\nstretcher:", this.stretcher); } @@ -736,6 +733,8 @@ class Resizer { styleArray.push(`transform: translate(${translate.x}px, ${translate.y}px) scale(${stretchFactors.xFactor}, ${stretchFactors.yFactor}) !important;`); // important — guarantees video will be properly aligned + // Note that position:absolute cannot be put here, otherwise old.reddit /w RES breaks — videos embedded + // from certain hosts will get a height: 0px. This is bad. styleArray.push("top: 0px !important; left: 0px !important; bottom: 0px !important; right: 0px;"); // important — some websites (cough reddit redesign cough) may impose some dumb max-width and max-height diff --git a/src/ext/lib/video-transform/Scaler.js b/src/ext/lib/video-transform/Scaler.js index d846274..b625ac2 100644 --- a/src/ext/lib/video-transform/Scaler.js +++ b/src/ext/lib/video-transform/Scaler.js @@ -66,6 +66,40 @@ class Scaler { } calculateCrop(ar) { + /** + * STEP 1: NORMALIZE ASPECT RATIO + * + * Video width is normalized based on 100% of the parent. That means if the player AR + * is narrower than video ar, we need to pre-downscale the video. This scaling already + * undoes any zoom that style="height:123%" on the video element adds. + * + * There are few exceptions and additional caveatss: + * * AspectRatio.FitHeight: we don't want to pre-downscale the video at all, as things + * will be scaled to fit height as-is. + * * When player is wider than stream, we want to undo any height compensations site + * tacks on the video tag. + * + * Quick notes: + * * when I say 'video AR', I actually mean aspect ratio after we've compensated for + * any possible 'height:' stuffs in the style attribute of the video tag + * * because video width is normalized on 100% of the parent, we don't need to correct + * anything when the player is wider than the video. + */ + const streamAr = this.conf.video.videoWidth / this.conf.video.videoHeight; + const playerAr = this.conf.player.dimensions.width / this.conf.player.dimensions.height; + const heightCompensationFactor = this.conf.getHeightCompensationFactor(); + const compensatedStreamAr = streamAr * heightCompensationFactor; + + let arCorrectionFactor = 1; + + if (ar.type !== AspectRatio.FitHeight) { + if (playerAr < compensatedStreamAr) { + arCorrectionFactor = this.conf.player.dimensions.width / this.conf.video.offsetWidth; + } else if (ar.type !== AspectRatio.Reset) { + arCorrectionFactor /= heightCompensationFactor; + } + } + if(!this.conf.video){ this.logger.log('info', 'debug', "[Scaler::calculateCrop] ERROR — no video detected. Conf:", this.conf, "video:", this.conf.video, "video dimensions:", this.conf.video && this.conf.video.videoWidth, '×', this.conf.video && this.conf.video.videoHeight); @@ -81,7 +115,7 @@ class Scaler { } if (ar.type === AspectRatio.Reset){ - return {xFactor: 1, yFactor: 1} + return {xFactor: arCorrectionFactor, yFactor: arCorrectionFactor} } // handle fuckie-wuckies @@ -102,15 +136,13 @@ class Scaler { // Dejansko razmerje stranic datoteke/