Merge branch 'master' into feature/player-ui
This commit is contained in:
commit
190737b915
15
CHANGELOG.md
15
CHANGELOG.md
@ -8,16 +8,27 @@
|
|||||||
* Settings page looks ugly af right now. Maybe fix it some time later
|
* Settings page looks ugly af right now. Maybe fix it some time later
|
||||||
* other bug fixes
|
* other bug fixes
|
||||||
|
|
||||||
|
## v7.0 (planned major)
|
||||||
|
* WebGL autodetection
|
||||||
|
|
||||||
## v6.0 (planned major)
|
## v6.0 (planned major)
|
||||||
|
|
||||||
* WebGL autodetection
|
|
||||||
* in-player GUI
|
* in-player GUI
|
||||||
* Fix UI logger
|
* Fix UI logger
|
||||||
|
|
||||||
## v5.x (next major)
|
## v5.x (next major)
|
||||||
|
|
||||||
* Migrate main scripts to typescript (vue is currently not included)
|
### v5.0.0
|
||||||
|
|
||||||
|
There's been some big-ish changes under the hood:
|
||||||
|
|
||||||
|
* Migrate main scripts to typescript (vue is currently not included).
|
||||||
|
* webextension-polyfill is now used everywhere (if only because typescript throws a hissy fit with `browser` and `chrome` otherwise) ([#114](https://github.com/tamius-han/ultrawidify/issues/114))
|
||||||
|
* Fix some bugs that I didn't even know I had, but typescript kinda shone some light on them
|
||||||
|
* Manual zoom (Z/U unless sites override the two) should now work again (without automatic AR constantly overriding it). Same goes for panning. ([#135](https://github.com/tamius-han/ultrawidify/issues/135) & [#138](https://github.com/tamius-han/ultrawidify/issues/138))
|
||||||
|
* Fix issue when video would be scaled incorrectly if video element uses `height:auto`.
|
||||||
|
* **[5.0.0.1]** Fixed the issue where settings were reset on page load.
|
||||||
|
* **[5.0.0.1]** Fixed the issue where settings page wouldn't load.
|
||||||
## v4.x (current major)
|
## v4.x (current major)
|
||||||
|
|
||||||
### v4.5.3
|
### v4.5.3
|
||||||
|
1074
package-lock.json
generated
1074
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,9 @@
|
|||||||
"@types/es6-promise": "^3.3.0",
|
"@types/es6-promise": "^3.3.0",
|
||||||
"@types/firefox": "0.0.30",
|
"@types/firefox": "0.0.30",
|
||||||
"@types/node": "^14.14.25",
|
"@types/node": "^14.14.25",
|
||||||
|
"@types/resize-observer-browser": "^0.1.5",
|
||||||
"@vue/cli": "^4.5.9",
|
"@vue/cli": "^4.5.9",
|
||||||
|
"@vue/cli-plugin-typescript": "^4.5.11",
|
||||||
"bootstrap": "^4.5.3",
|
"bootstrap": "^4.5.3",
|
||||||
"bootstrap-icons": "^1.1.0",
|
"bootstrap-icons": "^1.1.0",
|
||||||
"bootstrap-icons-vue": "^0.3.0",
|
"bootstrap-icons-vue": "^0.3.0",
|
||||||
@ -35,6 +37,7 @@
|
|||||||
"fs-extra": "^7.0.1",
|
"fs-extra": "^7.0.1",
|
||||||
"json-cyclic": "0.0.3",
|
"json-cyclic": "0.0.3",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
"vue": "^3.0.0-beta.1",
|
"vue": "^3.0.0-beta.1",
|
||||||
"vuex": "^4.0.0-alpha.1",
|
"vuex": "^4.0.0-alpha.1",
|
||||||
"vuex-webextensions": "^1.3.0",
|
"vuex-webextensions": "^1.3.0",
|
||||||
|
@ -116,8 +116,8 @@ import KeyboardShortcutParser from '../js/KeyboardShortcutParser';
|
|||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
Stretch: Stretch,
|
StretchType: StretchType,
|
||||||
AspectRatio: AspectRatio,
|
AspectRatioType: AspectRatioType,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -52,7 +52,7 @@ import KeyboardShortcutParser from '../js/KeyboardShortcutParser'
|
|||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
Stretch: Stretch
|
StretchType: StretchType
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export async function sleep(timeout) {
|
|
||||||
return new Promise( (resolve, reject) => setTimeout(() => resolve(), timeout));
|
|
||||||
}
|
|
3
src/common/js/utils.ts
Normal file
3
src/common/js/utils.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export async function sleep(timeout) {
|
||||||
|
return new Promise<void>( (resolve, reject) => setTimeout(() => resolve(), timeout));
|
||||||
|
}
|
@ -16,7 +16,7 @@ export default class UWServer {
|
|||||||
ports: any[] = [];
|
ports: any[] = [];
|
||||||
hasVideos: boolean;
|
hasVideos: boolean;
|
||||||
currentSite: string = '';
|
currentSite: string = '';
|
||||||
videoTabs: any;
|
videoTabs: any = {};
|
||||||
currentTabId: number = 0;
|
currentTabId: number = 0;
|
||||||
|
|
||||||
selectedSubitem: any = {
|
selectedSubitem: any = {
|
||||||
@ -37,14 +37,14 @@ export default class UWServer {
|
|||||||
|
|
||||||
const loggingOptions = {
|
const loggingOptions = {
|
||||||
isBackgroundScript: true,
|
isBackgroundScript: true,
|
||||||
allowLogging: true,
|
allowLogging: false,
|
||||||
useConfFromStorage: true,
|
useConfFromStorage: true,
|
||||||
logAll: true,
|
logAll: true,
|
||||||
fileOptions: {
|
fileOptions: {
|
||||||
enabled: true,
|
enabled: false,
|
||||||
},
|
},
|
||||||
consoleOptions: {
|
consoleOptions: {
|
||||||
enabled: true
|
enabled: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.logger = new Logger();
|
this.logger = new Logger();
|
||||||
|
@ -10,13 +10,13 @@ if (process.env.CHANNEL !== 'stable'){
|
|||||||
|
|
||||||
export const baseLoggingOptions: LoggerConfig = {
|
export const baseLoggingOptions: LoggerConfig = {
|
||||||
isContentScript: true,
|
isContentScript: true,
|
||||||
allowLogging: true,
|
allowLogging: false,
|
||||||
useConfFromStorage: true,
|
useConfFromStorage: true,
|
||||||
fileOptions: {
|
fileOptions: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
consoleOptions: {
|
consoleOptions: {
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"init": true,
|
"init": true,
|
||||||
"settings": true,
|
"settings": true,
|
||||||
|
@ -231,16 +231,6 @@ class Settings {
|
|||||||
// if there's settings, set saved object as active settings
|
// if there's settings, set saved object as active settings
|
||||||
this.active = settings;
|
this.active = settings;
|
||||||
|
|
||||||
// if last saved settings was for version prior to 4.x, we reset settings to default
|
|
||||||
// it's not like people will notice cos that version didn't preserve settings at all
|
|
||||||
if (this.active.version && !settings.version.startsWith('4')) {
|
|
||||||
this.active = this.getDefaultSettings();
|
|
||||||
this.active.version = this.version;
|
|
||||||
await this.save();
|
|
||||||
return this.active;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// if version number is undefined, we make it defined
|
// if version number is undefined, we make it defined
|
||||||
// this should only happen on first extension initialization
|
// this should only happen on first extension initialization
|
||||||
if (!this.active.version) {
|
if (!this.active.version) {
|
||||||
|
@ -19,124 +19,130 @@ class CommsServer {
|
|||||||
}[] = [];
|
}[] = [];
|
||||||
popupPort: any;
|
popupPort: any;
|
||||||
|
|
||||||
commands: {[x: string]: ((a: any, b: any) => void | Promise<void>)[]}
|
/**
|
||||||
|
* commands — functions that handle incoming messages
|
||||||
|
* functions can have the following arguments, which are,
|
||||||
|
* in this order:
|
||||||
|
* message — the message we received
|
||||||
|
* port|sender — on persistent channels, second argument is port on which the server
|
||||||
|
* listens. If the message was sent in non-persistent way, this is the
|
||||||
|
* sender script/frame/whatever of the message
|
||||||
|
* sendResponse — callback function on messages received via non-persistent channel
|
||||||
|
*/
|
||||||
|
commands: {[x: string]: ((a: any, b: any) => void | Promise<void>)[]} = {
|
||||||
|
'announce-zoom': [
|
||||||
|
(message) => {
|
||||||
|
try {
|
||||||
|
// forward message to the popup
|
||||||
|
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
|
||||||
|
} catch (e) {
|
||||||
|
// if popup is closed, this will/may fail. This is okay, so we just ignore this error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'get-current-zoom': [
|
||||||
|
(message) => this.sendToActive(message),
|
||||||
|
],
|
||||||
|
'get-current-site': [
|
||||||
|
async (message, port) => {
|
||||||
|
port.postMessage({
|
||||||
|
cmd: 'set-current-site',
|
||||||
|
site: await this.server.getVideoTab(),
|
||||||
|
tabHostname: await this.getCurrentTabHostname()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'popup-set-selected-tab': [
|
||||||
|
(message) => this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem),
|
||||||
|
],
|
||||||
|
'has-video': [
|
||||||
|
(message, port) => this.server.registerVideo(port.sender),
|
||||||
|
],
|
||||||
|
'noVideo': [
|
||||||
|
(message, port) => this.server.unregisterVideo(port.sender),
|
||||||
|
],
|
||||||
|
'inject-css': [
|
||||||
|
(message, sender) => this.server.injectCss(message.cssString, sender),
|
||||||
|
],
|
||||||
|
'eject-css': [
|
||||||
|
(message, sender) => this.server.removeCss(message.cssString, sender),
|
||||||
|
],
|
||||||
|
'replace-css': [
|
||||||
|
(message, sender) => this.server.replaceCss(message.oldCssString, message.newCssString, sender),
|
||||||
|
],
|
||||||
|
// 'get-config': [
|
||||||
|
// (message, port) => {
|
||||||
|
// this.logger.log('info', 'comms', "CommsServer: received get-config. Active settings?", this.settings.active, "\n(settings:", this.settings, ")");
|
||||||
|
// port.postMessage(
|
||||||
|
// {cmd: "set-config", conf: this.settings.active, site: this.server.currentSite}
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
'get-config': [
|
||||||
|
(message, sender) => {
|
||||||
|
var ret = {extensionConf: JSON.stringify(this.settings.active)};
|
||||||
|
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Returning this:", "background-color: #11D; color: #aad", ret);
|
||||||
|
Promise.resolve(ret);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'autoar-enable': [
|
||||||
|
() => {
|
||||||
|
this.settings.active.sites['@global'].autoar = ExtensionMode.Enabled;
|
||||||
|
this.settings.save();
|
||||||
|
this.logger.log('info', 'comms', "[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'autoar-disable': [
|
||||||
|
(message) => {
|
||||||
|
this.settings.active.sites['@global'].autoar = ExtensionMode.Disabled;
|
||||||
|
if (message.reason){
|
||||||
|
this.settings.active.arDetect.disabledReason = message.reason;
|
||||||
|
} else {
|
||||||
|
this.settings.active.arDetect.disabledReason = 'User disabled';
|
||||||
|
}
|
||||||
|
this.settings.save();
|
||||||
|
this.logger.log('info', 'comms', "[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'autoar-set-interval': [
|
||||||
|
(message) => {
|
||||||
|
this.logger.log('info', 'comms', `[uw-bg] trying to set new interval for autoAr. New interval is, ${message.timeout} ms`);
|
||||||
|
|
||||||
|
// set fairly liberal limit
|
||||||
|
var timeout = message.timeout < 4 ? 4 : message.timeout;
|
||||||
|
this.settings.active.arDetect.timers.playing = timeout;
|
||||||
|
this.settings.save();
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'logging-stop-and-save': [ // TODO: possibly never used/superseded — check
|
||||||
|
(message, sender) => {
|
||||||
|
this.logger.log('info', 'comms', "Received command to stop logging and export the received input");
|
||||||
|
this.logger.addToGlobalHistory(`${message.host}::${sender?.tab?.id ?? '×'}-${sender.frameId ?? '×'}`, JSON.parse(message.history));
|
||||||
|
this.logger.finish();
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'logging-save': [
|
||||||
|
(message, sender) => {
|
||||||
|
this.logger.log('info', 'comms', `Received command to save log for site ${message.host} (tabId ${sender.tab.id}, frameId ${sender.frameId}`);
|
||||||
|
this.logger.addToGlobalHistory(`${message?.host}::${sender?.tab?.id ?? '×'}-${sender?.frameId ?? '×'}`, JSON.parse(message.history));
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//#region getters
|
||||||
|
get activeTab() {
|
||||||
|
return browser.tabs.query({currentWindow: true, active: true});
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
constructor(server) {
|
constructor(server) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.logger = server.logger;
|
this.logger = server.logger;
|
||||||
this.settings = server.settings;
|
this.settings = server.settings;
|
||||||
this.popupPort = null;
|
|
||||||
|
|
||||||
browser.runtime.onConnect.addListener(p => this.onConnect(p));
|
browser.runtime.onConnect.addListener(p => this.onConnect(p));
|
||||||
browser.runtime.onMessage.addListener((m, sender) => this.processReceivedMessage_nonpersistent(m, sender));
|
browser.runtime.onMessage.addListener((m, sender) => this.processReceivedMessage_nonpersistent(m, sender));
|
||||||
|
|
||||||
// commands — functions that handle incoming messages
|
|
||||||
// functions can have the following arguments, which are,
|
|
||||||
// in this order:
|
|
||||||
// message — the message we received
|
|
||||||
// port|sender — on persistent channels, second argument is port on which the server
|
|
||||||
// listens. If the message was sent in non-persistent way, this is the
|
|
||||||
// sender script/frame/whatever of the message
|
|
||||||
// sendResponse — callback function on messages received via non-persistent channel
|
|
||||||
this.commands = {
|
|
||||||
'announce-zoom': [
|
|
||||||
(message) => {
|
|
||||||
try {
|
|
||||||
// forward message to the popup
|
|
||||||
this.popupPort.postMessage({cmd: 'set-current-zoom', zoom: message.zoom});
|
|
||||||
} catch (e) {
|
|
||||||
// if popup is closed, this will/may fail. This is okay, so we just ignore this error
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'get-current-zoom': [
|
|
||||||
(message) => this.sendToActive(message),
|
|
||||||
],
|
|
||||||
'get-current-site': [
|
|
||||||
async (message, port) => {
|
|
||||||
port.postMessage({
|
|
||||||
cmd: 'set-current-site',
|
|
||||||
site: await this.server.getVideoTab(),
|
|
||||||
tabHostname: await this.getCurrentTabHostname()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'popup-set-selected-tab': [
|
|
||||||
(message) => this.server.setSelectedTab(message.selectedMenu, message.selectedSubitem),
|
|
||||||
],
|
|
||||||
'has-video': [
|
|
||||||
(message, port) => this.server.registerVideo(port.sender),
|
|
||||||
],
|
|
||||||
'noVideo': [
|
|
||||||
(message, port) => this.server.unregisterVideo(port.sender),
|
|
||||||
],
|
|
||||||
'inject-css': [
|
|
||||||
(message, sender) => this.server.injectCss(message.cssString, sender),
|
|
||||||
],
|
|
||||||
'eject-css': [
|
|
||||||
(message, sender) => this.server.removeCss(message.cssString, sender),
|
|
||||||
],
|
|
||||||
'replace-css': [
|
|
||||||
(message, sender) => this.server.replaceCss(message.oldCssString, message.newCssString, sender),
|
|
||||||
],
|
|
||||||
// 'get-config': [
|
|
||||||
// (message, port) => {
|
|
||||||
// this.logger.log('info', 'comms', "CommsServer: received get-config. Active settings?", this.settings.active, "\n(settings:", this.settings, ")");
|
|
||||||
// port.postMessage(
|
|
||||||
// {cmd: "set-config", conf: this.settings.active, site: this.server.currentSite}
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
'get-config': [
|
|
||||||
(message, sender) => {
|
|
||||||
var ret = {extensionConf: JSON.stringify(this.settings.active)};
|
|
||||||
this.logger.log('info', 'comms', "%c[CommsServer.js::processMessage_nonpersistent] Returning this:", "background-color: #11D; color: #aad", ret);
|
|
||||||
Promise.resolve(ret);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'autoar-enable': [
|
|
||||||
() => {
|
|
||||||
this.settings.active.sites['@global'].autoar = ExtensionMode.Enabled;
|
|
||||||
this.settings.save();
|
|
||||||
this.logger.log('info', 'comms', "[uw-bg] autoar set to enabled (blacklist). evidenz:", this.settings.active);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'autoar-disable': [
|
|
||||||
(message) => {
|
|
||||||
this.settings.active.sites['@global'].autoar = ExtensionMode.Disabled;
|
|
||||||
if (message.reason){
|
|
||||||
this.settings.active.arDetect.disabledReason = message.reason;
|
|
||||||
} else {
|
|
||||||
this.settings.active.arDetect.disabledReason = 'User disabled';
|
|
||||||
}
|
|
||||||
this.settings.save();
|
|
||||||
this.logger.log('info', 'comms', "[uw-bg] autoar set to disabled. evidenz:", this.settings.active);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'autoar-set-interval': [
|
|
||||||
(message) => {
|
|
||||||
this.logger.log('info', 'comms', `[uw-bg] trying to set new interval for autoAr. New interval is, ${message.timeout} ms`);
|
|
||||||
|
|
||||||
// set fairly liberal limit
|
|
||||||
var timeout = message.timeout < 4 ? 4 : message.timeout;
|
|
||||||
this.settings.active.arDetect.timers.playing = timeout;
|
|
||||||
this.settings.save();
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'logging-stop-and-save': [ // TODO: possibly never used/superseded — check
|
|
||||||
(message, sender) => {
|
|
||||||
this.logger.log('info', 'comms', "Received command to stop logging and export the received input");
|
|
||||||
this.logger.addToGlobalHistory(`${message.host}::${sender?.tab?.id ?? '×'}-${sender.frameId ?? '×'}`, JSON.parse(message.history));
|
|
||||||
this.logger.finish();
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'logging-save': [
|
|
||||||
(message, sender) => {
|
|
||||||
this.logger.log('info', 'comms', `Received command to save log for site ${message.host} (tabId ${sender.tab.id}, frameId ${sender.frameId}`);
|
|
||||||
this.logger.addToGlobalHistory(`${message?.host}::${sender?.tab?.id ?? '×'}-${sender?.frameId ?? '×'}`, JSON.parse(message.history));
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(command, callback) {
|
subscribe(command, callback) {
|
||||||
@ -181,9 +187,6 @@ class CommsServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeTab() {
|
|
||||||
return browser.tabs.query({currentWindow: true, active: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a message to addon content scripts.
|
* Sends a message to addon content scripts.
|
||||||
@ -306,7 +309,7 @@ class CommsServer {
|
|||||||
await this.execCmd(message, portOrSender);
|
await this.execCmd(message, portOrSender);
|
||||||
|
|
||||||
if (message.forwardToSameFramePort) {
|
if (message.forwardToSameFramePort) {
|
||||||
this.sendToFrameContentScripts(message, portOrSender.tab.id, portOrSender.frameId, message.port)
|
this.sendToFrameContentScripts(message, portOrSender.tab.id, portOrSender.frameId, message.port);
|
||||||
}
|
}
|
||||||
if (message.forwardToContentScript) {
|
if (message.forwardToContentScript) {
|
||||||
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
|
this.logger.log('info', 'comms', "[CommsServer.js::processReceivedMessage] Message has 'forward to content script' flag set. Forwarding message as is. Message:", message);
|
||||||
|
548
src/ext/lib/video-data/PlayerData.ts
Normal file
548
src/ext/lib/video-data/PlayerData.ts
Normal file
@ -0,0 +1,548 @@
|
|||||||
|
import Debug from '../../conf/Debug';
|
||||||
|
import ExtensionMode from '../../../common/enums/ExtensionMode.enum'
|
||||||
|
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
||||||
|
import PlayerNotificationUi from '../uwui/PlayerNotificationUI';
|
||||||
|
import PlayerUi from '../uwui/PlayerUI';
|
||||||
|
import BrowserDetect from '../../conf/BrowserDetect';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import { sleep } from '../../../common/js/utils';
|
||||||
|
import VideoData from './VideoData';
|
||||||
|
import Settings from '../Settings';
|
||||||
|
import Logger from '../Logger';
|
||||||
|
|
||||||
|
if (process.env.CHANNEL !== 'stable'){
|
||||||
|
console.info("Loading: PlayerData.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sprejme <video> tag (element) in seznam imen, ki se lahko pojavijo v razredih oz. id staršev.
|
||||||
|
// vrne dimenzije predvajalnika (širina, višina)
|
||||||
|
//
|
||||||
|
// Na youtube v theater mode je razširitev rahlo pokvarjena. Video tag ostane večji od predvajalnika, ko se zapusti
|
||||||
|
// celozaslonski način. Ta funkcija skuša to težavo rešiti tako, da poišče element predvajalnika, ki je zavit okoli videa.
|
||||||
|
//
|
||||||
|
// Funkcija izkorišča lastnost, da bi načeloma moral biti vsak zunanji element večji od notranjega. Najmanjši element od
|
||||||
|
// <video> značke pa do korena drevesa bi tako moral biti predvajalnik.
|
||||||
|
//
|
||||||
|
// Če je podan seznam imen, potem funkcija vrne dimenzije prvega elementa, ki v id oz. razredu vsebuje katerokoli ime iz seznama
|
||||||
|
//
|
||||||
|
// | EN |
|
||||||
|
//
|
||||||
|
// accepts <video> tag (element) and list of names that can appear in id or class
|
||||||
|
// returns player dimensions (width, height)
|
||||||
|
//
|
||||||
|
// Theater mode is mildly broken on youtube. <video> tag remains bigger than the player after leaving the fullscreen mode, and
|
||||||
|
// there's nothing we can do about that. This function aims to solve the problem by finding the player element that's wrapped around
|
||||||
|
// the <video> tag.
|
||||||
|
//
|
||||||
|
// In general, an outer tag should be bigger than the inner tag. Therefore the smallest element between <video> tag and the document
|
||||||
|
// root should be the player.
|
||||||
|
//
|
||||||
|
// If list of names is provided, the function returns dimensions of the first element that contains any name from the list in either
|
||||||
|
// id or class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class PlayerData {
|
||||||
|
|
||||||
|
//#region helper objects
|
||||||
|
logger: Logger;
|
||||||
|
videoData: VideoData;
|
||||||
|
settings: Settings;
|
||||||
|
notificationService: PlayerNotificationUi;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region HTML objects
|
||||||
|
video: any;
|
||||||
|
element: any;
|
||||||
|
overlayNode: any;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region flags
|
||||||
|
invalid: boolean = false;
|
||||||
|
private periodicallyRefreshPlayerElement: boolean = false;
|
||||||
|
halted: boolean = true;
|
||||||
|
|
||||||
|
//#region misc stuff
|
||||||
|
extensionMode: any;
|
||||||
|
dimensions: {width?: number, height?: number, fullscreen?: boolean};
|
||||||
|
private playerIdElement: any;
|
||||||
|
private observer: ResizeObserver;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
constructor(videoData) {
|
||||||
|
try {
|
||||||
|
this.logger = videoData.logger;
|
||||||
|
this.videoData = videoData;
|
||||||
|
this.video = videoData.video;
|
||||||
|
this.settings = videoData.settings;
|
||||||
|
this.extensionMode = videoData.extensionMode;
|
||||||
|
this.invalid = false;
|
||||||
|
this.element = this.getPlayer();
|
||||||
|
|
||||||
|
this.notificationService = new PlayerNotificationUi(this.element, this.settings);
|
||||||
|
|
||||||
|
this.dimensions = undefined;
|
||||||
|
this.overlayNode = undefined;
|
||||||
|
|
||||||
|
this.periodicallyRefreshPlayerElement = false;
|
||||||
|
try {
|
||||||
|
this.periodicallyRefreshPlayerElement = this.settings.active.sites[window.location.hostname].DOM.player.periodicallyRefreshPlayerElement;
|
||||||
|
} catch (e) {
|
||||||
|
// no biggie — that means we don't have any special settings for this site.
|
||||||
|
}
|
||||||
|
|
||||||
|
// this happens when we don't find a matching player element
|
||||||
|
if (!this.element) {
|
||||||
|
this.invalid = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.extensionMode === ExtensionMode.Enabled) {
|
||||||
|
this.checkPlayerSizeChange();
|
||||||
|
}
|
||||||
|
this.startChangeDetection();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[Ultrawidify::PlayerData::ctor] There was an error setting up player data. You should be never seeing this message. Error:', e);
|
||||||
|
this.invalid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static isFullScreen(){
|
||||||
|
return ( window.innerHeight == window.screen.height && window.innerWidth == window.screen.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onPlayerDimensionsChanged(mutationList?, observer?) {
|
||||||
|
if (this?.checkPlayerSizeChange()) {
|
||||||
|
this.videoData.resizer.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
start(){
|
||||||
|
this.startChangeDetection();
|
||||||
|
}
|
||||||
|
|
||||||
|
stop(){
|
||||||
|
this.halted = true;
|
||||||
|
this.stopChangeDetection();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.stopChangeDetection();
|
||||||
|
this.destroyOverlay();
|
||||||
|
this.notificationService?.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
startChangeDetection(){
|
||||||
|
if (this.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (BrowserDetect.firefox) {
|
||||||
|
this.observer = new ResizeObserver(
|
||||||
|
_.debounce( // don't do this too much:
|
||||||
|
this.onPlayerDimensionsChanged,
|
||||||
|
250, // do it once per this many ms
|
||||||
|
{
|
||||||
|
leading: true, // do it when we call this fallback first
|
||||||
|
trailing: true // do it after the timeout if we call this callback few more times
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
||||||
|
// when it's not wrapped into an anonymous function
|
||||||
|
this.observer = new ResizeObserver(
|
||||||
|
_.debounce( // don't do this too much:
|
||||||
|
(m,o) => this.onPlayerDimensionsChanged(m,o),
|
||||||
|
250, // do it once per this many ms
|
||||||
|
{
|
||||||
|
leading: true, // do it when we call this fallback first
|
||||||
|
trailing: true // do it after the timeout if we call this callback few more times
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observerConf = {
|
||||||
|
attributes: true,
|
||||||
|
// attributeFilter: ['style', 'class'],
|
||||||
|
attributeOldValue: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.observer.observe(this.element);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("failed to set observer",e )
|
||||||
|
}
|
||||||
|
// legacy mode still exists, but acts as a fallback for observers and is triggered less
|
||||||
|
// frequently in order to avoid too many pointless checks
|
||||||
|
this.legacyChangeDetection();
|
||||||
|
}
|
||||||
|
|
||||||
|
async legacyChangeDetection() {
|
||||||
|
while (!this.halted) {
|
||||||
|
await sleep(1000);
|
||||||
|
try {
|
||||||
|
this.doPeriodicPlayerElementChangeCheck();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[PlayerData::legacycd] this message is pretty high on the list of messages you shouldnt see', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doPeriodicPlayerElementChangeCheck() {
|
||||||
|
if (this.periodicallyRefreshPlayerElement) {
|
||||||
|
this.forceDetectPlayerElementChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopChangeDetection(){
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
makeOverlay() {
|
||||||
|
if (!this.overlayNode) {
|
||||||
|
this.destroyOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlay = document.createElement('div');
|
||||||
|
overlay.style.width = '100%';
|
||||||
|
overlay.style.height = '100%';
|
||||||
|
overlay.style.position = 'absolute';
|
||||||
|
overlay.style.top = '0';
|
||||||
|
overlay.style.left = '0';
|
||||||
|
overlay.style.zIndex = '1000000000';
|
||||||
|
overlay.style.pointerEvents = 'none';
|
||||||
|
|
||||||
|
this.overlayNode = overlay;
|
||||||
|
this.element.appendChild(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyOverlay() {
|
||||||
|
if(this.playerIdElement) {
|
||||||
|
this.playerIdElement.remove();
|
||||||
|
this.playerIdElement = undefined;
|
||||||
|
}
|
||||||
|
if (this.overlayNode) {
|
||||||
|
this.overlayNode.remove();
|
||||||
|
this.overlayNode = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markPlayer(name, color) {
|
||||||
|
if (!this.overlayNode) {
|
||||||
|
this.makeOverlay();
|
||||||
|
}
|
||||||
|
if (this.playerIdElement) {
|
||||||
|
this.playerIdElement.remove();
|
||||||
|
}
|
||||||
|
this.playerIdElement = document.createElement('div');
|
||||||
|
this.playerIdElement.innerHTML = `<div style="background-color: ${color}; color: #fff; position: absolute; top: 0; left: 0">${name}</div>`;
|
||||||
|
|
||||||
|
this.overlayNode.appendChild(this.playerIdElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarkPlayer() {
|
||||||
|
this.logger.log('info', 'debug', "[PlayerData::unmarkPlayer] unmarking player!", {playerIdElement: this.playerIdElement});
|
||||||
|
if (this.playerIdElement) {
|
||||||
|
this.playerIdElement.innerHTML = '';
|
||||||
|
this.playerIdElement.remove();
|
||||||
|
}
|
||||||
|
this.playerIdElement = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
collectionHas(collection, element) {
|
||||||
|
for (let i = 0, len = collection.length; i < len; i++) {
|
||||||
|
if (collection[i] == element) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePlayerDimensions(element) {
|
||||||
|
const isFullScreen = PlayerData.isFullScreen();
|
||||||
|
|
||||||
|
if (element.offsetWidth !== this.dimensions?.width
|
||||||
|
|| element.offsetHeight !== this.dimensions?.height
|
||||||
|
|| isFullScreen !== this.dimensions?.fullscreen) {
|
||||||
|
|
||||||
|
// update dimensions only if they've changed, _before_ we do a restore (not after)
|
||||||
|
this.dimensions = {
|
||||||
|
width: element.offsetWidth,
|
||||||
|
height: element.offsetHeight,
|
||||||
|
fullscreen: isFullScreen
|
||||||
|
};
|
||||||
|
|
||||||
|
// actually re-calculate zoom when player size changes, but only if videoData.resizer
|
||||||
|
// is defined. Since resizer needs a PlayerData object to exist, videoData.resizer will
|
||||||
|
// be undefined the first time this function will run.
|
||||||
|
this.videoData.resizer?.restore();
|
||||||
|
|
||||||
|
// NOTE: it's possible that notificationService hasn't been initialized yet at this point.
|
||||||
|
// no biggie if it wasn't, we just won't replace the notification UI
|
||||||
|
this.notificationService?.replace(this.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayer() {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
let element = this.video.parentNode;
|
||||||
|
const videoWidth = this.video.offsetWidth;
|
||||||
|
const videoHeight = this.video.offsetHeight;
|
||||||
|
const elementQ = [];
|
||||||
|
let scorePenalty = 0;
|
||||||
|
let score;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(! element ){
|
||||||
|
this.logger.log('info', 'debug', "[PlayerDetect::_pd_getPlayer] element is not valid, doing nothing.", element)
|
||||||
|
if(this.element) {
|
||||||
|
const ths = this;
|
||||||
|
}
|
||||||
|
this.element = undefined;
|
||||||
|
this.dimensions = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// log the entire hierarchy from <video> to root
|
||||||
|
if (this.logger.canLog('playerDetect')) {
|
||||||
|
const logObj = [];
|
||||||
|
logObj.push(`window size: ${window.innerWidth} x ${window.innerHeight}`);
|
||||||
|
let e = element;
|
||||||
|
while (e) {
|
||||||
|
logObj.push({offsetSize: {width: e.offsetWidth, height: e.offsetHeight}, clientSize: {width: e.clientWidth, height: e.clientHeight}, element: e});
|
||||||
|
e = e.parentNode;
|
||||||
|
}
|
||||||
|
this.logger.log('info', 'playerDetect', "\n\n[PlayerDetect::getPlayer()] element hierarchy (video->root)", logObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.active.sites[host]?.DOM?.player?.manual) {
|
||||||
|
if (this.settings.active.sites[host]?.DOM?.player?.useRelativeAncestor
|
||||||
|
&& this.settings.active.sites[host]?.DOM?.player?.videoAncestor) {
|
||||||
|
|
||||||
|
let parentsLeft = this.settings.active.sites[host].DOM.player.videoAncestor - 1;
|
||||||
|
while (parentsLeft --> 0) {
|
||||||
|
element = element.parentNode;
|
||||||
|
}
|
||||||
|
if (element) {
|
||||||
|
this.updatePlayerDimensions(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
} else if (this.settings.active.sites[host]?.DOM?.player?.querySelectors) {
|
||||||
|
const allSelectors = document.querySelectorAll(this.settings.active.sites[host].DOM.player.querySelectors);
|
||||||
|
// actually we'll also score this branch in a similar way we score the regular, auto branch
|
||||||
|
while (element) {
|
||||||
|
|
||||||
|
// Let's see how this works
|
||||||
|
if (this.collectionHas(allSelectors, element)) {
|
||||||
|
score = 100; // every matching element gets a baseline 100 points
|
||||||
|
|
||||||
|
// elements that match the size get a hefty bonus
|
||||||
|
if ( (element.offsetWidth >= videoWidth && this.equalish(element.offsetHeight, videoHeight, 2))
|
||||||
|
|| (element.offsetHeight >= videoHeight && this.equalish(element.offsetWidth, videoHeight, 2))) {
|
||||||
|
score += 75;
|
||||||
|
}
|
||||||
|
|
||||||
|
// elements farther away from the video get a penalty
|
||||||
|
score -= (scorePenalty++) * 20;
|
||||||
|
|
||||||
|
// push the element on the queue/stack:
|
||||||
|
elementQ.push({
|
||||||
|
score: score,
|
||||||
|
element: element,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
element = element.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// log player candidates
|
||||||
|
this.logger.log('info', 'playerDetect', 'player detect via query selector: element queue and final element:', {queue: elementQ, bestCandidate: elementQ.length ? elementQ.sort( (a,b) => b.score - a.score)[0].element : 'n/a'});
|
||||||
|
|
||||||
|
if (elementQ.length) {
|
||||||
|
// return element with biggest score
|
||||||
|
// if video player has not been found, proceed to automatic detection
|
||||||
|
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
|
||||||
|
this.updatePlayerDimensions(playerElement);
|
||||||
|
return playerElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to find element the old fashioned way
|
||||||
|
|
||||||
|
while (element){
|
||||||
|
// odstranimo čudne elemente, ti bi pokvarili zadeve
|
||||||
|
// remove weird elements, those would break our stuff
|
||||||
|
if ( element.offsetWidth == 0 || element.offsetHeight == 0){
|
||||||
|
element = element.parentNode;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// element je player, če je ena stranica enako velika kot video, druga pa večja ali enaka.
|
||||||
|
// za enakost dovolimo mala odstopanja
|
||||||
|
// element is player, if one of the sides is as long as the video and the other bigger (or same)
|
||||||
|
// we allow for tiny variations when checking for equality
|
||||||
|
if ( (element.offsetWidth >= videoWidth && this.equalish(element.offsetHeight, videoHeight, 2))
|
||||||
|
|| (element.offsetHeight >= videoHeight && this.equalish(element.offsetWidth, videoHeight, 2))) {
|
||||||
|
|
||||||
|
// todo — in case the match is only equalish and not exact, take difference into account when
|
||||||
|
// calculating score
|
||||||
|
|
||||||
|
score = 100;
|
||||||
|
|
||||||
|
// This entire section is disabled because of some bullshit on vk and some shady CIS streaming sites.
|
||||||
|
// Possibly removal of this criteria is not necessary, because there was also a bug with force player
|
||||||
|
//
|
||||||
|
|
||||||
|
// if (element.id.indexOf('player') !== -1) { // prefer elements with 'player' in id
|
||||||
|
// score += 75;
|
||||||
|
// }
|
||||||
|
// this has only been observed on steam
|
||||||
|
// if (element.id.indexOf('movie') !== -1) {
|
||||||
|
// score += 75;
|
||||||
|
// }
|
||||||
|
// if (element.classList.toString().indexOf('player') !== -1) { // prefer elements with 'player' in classlist, but a bit less than id
|
||||||
|
// score += 50;
|
||||||
|
// }
|
||||||
|
score -= scorePenalty++; // prefer elements closer to <video>
|
||||||
|
|
||||||
|
elementQ.push({
|
||||||
|
element: element,
|
||||||
|
score: score,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
element = element.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// log player candidates
|
||||||
|
this.logger.log('info', 'playerDetect', 'player detect, auto/fallback: element queue and final element:', {queue: elementQ, bestCandidate: elementQ.length ? elementQ.sort( (a,b) => b.score - a.score)[0].element : 'n/a'});
|
||||||
|
|
||||||
|
if (elementQ.length) {
|
||||||
|
// return element with biggest score
|
||||||
|
const playerElement = elementQ.sort( (a,b) => b.score - a.score)[0].element;
|
||||||
|
|
||||||
|
this.updatePlayerDimensions(playerElement);
|
||||||
|
return playerElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no candidates were found, something is obviously very, _very_ wrong.
|
||||||
|
// we return nothing. Player will be marked as invalid and setup will stop.
|
||||||
|
// VideoData should check for that before starting anything.
|
||||||
|
this.logger.log('warn', 'debug', '[PlayerData::getPlayer] no matching player was found for video', this.video, 'Extension cannot work on this site.');
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.log('crit', 'debug', '[PlayerData::getPlayer] something went wrong while detecting player:', e, 'Shutting down extension for this page');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
equalish(a,b, tolerance) {
|
||||||
|
return a > b - tolerance && a < b + tolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
forceDetectPlayerElementChange() {
|
||||||
|
// Player dimension changes get calculated every time updatePlayerDimensions is called (which happens
|
||||||
|
// every time getPlayer() detects an element). If updatePlayerDimension detects dimensions were changed,
|
||||||
|
// it will always re-apply current crop, rendering this function little more than a fancy alias for
|
||||||
|
// getPlayer().
|
||||||
|
this.getPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
forceRefreshPlayerElement() {
|
||||||
|
this.getPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPlayerSizeChange() {
|
||||||
|
// this 'if' is just here for debugging — real code starts later. It's safe to collapse and
|
||||||
|
// ignore the contents of this if (unless we need to change how logging works)
|
||||||
|
if (this.logger.canLog('debug')){
|
||||||
|
if (this.dimensions?.fullscreen){
|
||||||
|
if(! PlayerData.isFullScreen()){
|
||||||
|
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: exited fullscreen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(! this.element) {
|
||||||
|
this.logger.log('info', 'playerDetect', "[PlayerDetect] player element isn't defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.element &&
|
||||||
|
( +this.dimensions?.width != +this.element?.offsetWidth ||
|
||||||
|
+this.dimensions?.height != +this.element?.offsetHeight )
|
||||||
|
) {
|
||||||
|
this.logger.log('info', 'debug', "[PlayerDetect] player size changed. reason: dimension change. Old dimensions?", this.dimensions?.width, this.dimensions?.height, "new dimensions:", this.element?.offsetWidth, this.element?.offsetHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if size doesn't match, update & return true
|
||||||
|
if (this.dimensions?.width != this.element.offsetWidth
|
||||||
|
|| this.dimensions?.height != this.element.offsetHeight ){
|
||||||
|
|
||||||
|
const isFullScreen = PlayerData.isFullScreen();
|
||||||
|
|
||||||
|
if (isFullScreen) {
|
||||||
|
this.dimensions = {
|
||||||
|
width: window.innerWidth,
|
||||||
|
height: window.innerHeight,
|
||||||
|
fullscreen: true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.dimensions = {
|
||||||
|
width: this.element.offsetWidth,
|
||||||
|
height: this.element.offsetHeight,
|
||||||
|
fullscreen: isFullScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFullscreenChange() {
|
||||||
|
const isFs = PlayerData.isFullScreen();
|
||||||
|
|
||||||
|
if (this.dimensions) {
|
||||||
|
if (this.dimensions.fullscreen != isFs) {
|
||||||
|
this.dimensions = {
|
||||||
|
fullscreen: isFs,
|
||||||
|
width: isFs ? screen.width : this.video.offsetWidth,
|
||||||
|
height: isFs ? screen.height : this.video.offsetHeight
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('info', 'debug', "[PlayerData::checkFullscreenChange] this.dimensions is not defined. Assuming fs change happened and setting default values.")
|
||||||
|
|
||||||
|
this.dimensions = {
|
||||||
|
fullscreen: isFs,
|
||||||
|
width: isFs ? screen.width : this.video.offsetWidth,
|
||||||
|
height: isFs ? screen.height : this.video.offsetHeight
|
||||||
|
};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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'){
|
||||||
|
console.info("PlayerData loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PlayerData;
|
@ -25,7 +25,13 @@ class VideoData {
|
|||||||
//#region misc stuff
|
//#region misc stuff
|
||||||
vdid: string;
|
vdid: string;
|
||||||
video: any;
|
video: any;
|
||||||
observer: MutationObserver;
|
observer: ResizeObserver;
|
||||||
|
mutationObserver: MutationObserver;
|
||||||
|
mutationObserverConf: MutationObserverInit = {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class', 'style'],
|
||||||
|
attributeOldValue: true,
|
||||||
|
};
|
||||||
extensionMode: any;
|
extensionMode: any;
|
||||||
userCssClassName: string;
|
userCssClassName: string;
|
||||||
validationId: number;
|
validationId: number;
|
||||||
@ -107,20 +113,27 @@ class VideoData {
|
|||||||
|
|
||||||
async injectBaseCss() {
|
async injectBaseCss() {
|
||||||
try {
|
try {
|
||||||
await this.pageInfo.injectCss(`
|
if (!this.mutationObserver) {
|
||||||
.uw-ultrawidify-base-wide-screen {
|
this.setupMutationObserver();
|
||||||
margin: 0px 0px 0px 0px !important;
|
|
||||||
width: initial !important;
|
|
||||||
align-self: start !important;
|
|
||||||
justify-self: start !important;
|
|
||||||
}
|
}
|
||||||
`);
|
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;
|
||||||
|
max-height: initial !important;
|
||||||
|
max-width: initial !important;
|
||||||
|
}
|
||||||
|
`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to inject base css!', e);
|
console.error('Failed to inject base css!', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unsetBaseClass() {
|
unsetBaseClass() {
|
||||||
|
this.mutationObserver.disconnect();
|
||||||
|
this.mutationObserver = undefined;
|
||||||
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
|
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,14 +178,6 @@ class VideoData {
|
|||||||
async setupStageTwo() {
|
async setupStageTwo() {
|
||||||
// POZOR: VRSTNI RED JE POMEMBEN (arDetect mora bit zadnji)
|
// POZOR: VRSTNI RED JE POMEMBEN (arDetect mora bit zadnji)
|
||||||
// NOTE: ORDERING OF OBJ INITIALIZATIONS IS IMPORTANT (arDetect needs to go last)
|
// NOTE: ORDERING OF OBJ INITIALIZATIONS IS IMPORTANT (arDetect needs to go last)
|
||||||
|
|
||||||
// NOTE: We only init observers once player is confirmed valid
|
|
||||||
const observerConf = {
|
|
||||||
attributes: true,
|
|
||||||
// attributeFilter: ['style', 'class'],
|
|
||||||
attributeOldValue: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.player = new PlayerData(this);
|
this.player = new PlayerData(this);
|
||||||
if (this.player.invalid) {
|
if (this.player.invalid) {
|
||||||
this.invalid = true;
|
this.invalid = true;
|
||||||
@ -184,7 +189,7 @@ class VideoData {
|
|||||||
// INIT OBSERVERS
|
// INIT OBSERVERS
|
||||||
try {
|
try {
|
||||||
if (BrowserDetect.firefox) {
|
if (BrowserDetect.firefox) {
|
||||||
this.observer = new MutationObserver(
|
this.observer = new ResizeObserver(
|
||||||
_.debounce(
|
_.debounce(
|
||||||
this.onVideoDimensionsChanged,
|
this.onVideoDimensionsChanged,
|
||||||
250,
|
250,
|
||||||
@ -197,7 +202,7 @@ class VideoData {
|
|||||||
} else {
|
} else {
|
||||||
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
||||||
// when it's not wrapped into an anonymous function
|
// when it's not wrapped into an anonymous function
|
||||||
this.observer = new MutationObserver(
|
this.observer = new ResizeObserver(
|
||||||
_.debounce(
|
_.debounce(
|
||||||
(m, o) => {
|
(m, o) => {
|
||||||
this.onVideoDimensionsChanged(m, o)
|
this.onVideoDimensionsChanged(m, o)
|
||||||
@ -213,7 +218,7 @@ class VideoData {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[VideoData] Observer setup failed:', e);
|
console.error('[VideoData] Observer setup failed:', e);
|
||||||
}
|
}
|
||||||
this.observer.observe(this.video, observerConf);
|
this.observer.observe(this.video);
|
||||||
|
|
||||||
// INIT AARD
|
// INIT AARD
|
||||||
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
|
this.arDetector = new ArDetector(this); // this starts Ar detection. needs optional parameter that prevets ardetdctor from starting
|
||||||
@ -257,6 +262,41 @@ class VideoData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupMutationObserver() {
|
||||||
|
try {
|
||||||
|
if (BrowserDetect.firefox) {
|
||||||
|
this.mutationObserver = new MutationObserver(
|
||||||
|
_.debounce(
|
||||||
|
this.onVideoMutation,
|
||||||
|
250,
|
||||||
|
{
|
||||||
|
leading: true,
|
||||||
|
trailing: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
|
||||||
|
// when it's not wrapped into an anonymous function
|
||||||
|
this.mutationObserver = new MutationObserver(
|
||||||
|
_.debounce(
|
||||||
|
(m, o) => {
|
||||||
|
this.onVideoMutation(m, o)
|
||||||
|
},
|
||||||
|
250,
|
||||||
|
{
|
||||||
|
leading: true,
|
||||||
|
trailing: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[VideoData] Observer setup failed:', e);
|
||||||
|
}
|
||||||
|
this.mutationObserver.observe(this.video, this.mutationObserverConf);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cleans up handlers and stuff when the show is over
|
* cleans up handlers and stuff when the show is over
|
||||||
*/
|
*/
|
||||||
@ -339,6 +379,34 @@ class VideoData {
|
|||||||
this.validateVideoOffsets();
|
this.validateVideoOffsets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVideoMutation(mutationList?: MutationRecord[], observer?) {
|
||||||
|
// verify that mutation didn't remove our class. Some pages like to do that.
|
||||||
|
let confirmAspectRatioRestore = false;
|
||||||
|
|
||||||
|
for(const mutation of mutationList) {
|
||||||
|
if (mutation.type === 'attributes') {
|
||||||
|
if( mutation.attributeName === 'class'
|
||||||
|
&& mutation.oldValue.indexOf('uw-ultrawidify-base-wide-screen') !== -1
|
||||||
|
&& !this.video.classList.contains('uw-ultrawidify-base-wide-screen')
|
||||||
|
) {
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
confirmAspectRatioRestore = true;
|
||||||
|
this.video.classList.add(this.userCssClassName);
|
||||||
|
this.video.classList.add('uw-ultrawidify-base-wide-screen');
|
||||||
|
} else if (mutation.attributeName === 'style') {
|
||||||
|
confirmAspectRatioRestore = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processDimensionsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
onVideoDimensionsChanged(mutationList, observer) {
|
onVideoDimensionsChanged(mutationList, observer) {
|
||||||
if (!mutationList || this.video === undefined) { // something's wrong
|
if (!mutationList || this.video === undefined) { // something's wrong
|
||||||
if (observer && this.video) {
|
if (observer && this.video) {
|
||||||
@ -346,34 +414,15 @@ class VideoData {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let confirmAspectRatioRestore = false;
|
|
||||||
|
|
||||||
for (let mutation of mutationList) {
|
this.processDimensionsChanged();
|
||||||
if (mutation.type === 'attributes') {
|
}
|
||||||
if (mutation.attributeName === 'class') {
|
|
||||||
if(!this.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.
|
|
||||||
this.video.classList.add(this.userCssClassName);
|
|
||||||
this.video.classList.add('uw-ultrawidify-base-wide-screen');
|
|
||||||
}
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces Ultrawidify to resotre aspect ratio. You should never call this method directly,
|
||||||
|
* instead you should be calling processDimensionChanged() wrapper function.
|
||||||
|
*/
|
||||||
|
private _processDimensionsChanged() {
|
||||||
// adding player observer taught us that if element size gets triggered by a class, then
|
// 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
|
// the 'style' attributes don't necessarily trigger. This means we also need to trigger
|
||||||
// restoreAr here, in case video size was changed this way
|
// restoreAr here, in case video size was changed this way
|
||||||
@ -387,6 +436,21 @@ class VideoData {
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores aspect ratio and validates video offsets after the restore. Execution uses
|
||||||
|
* debounce to limit how often the function executes.
|
||||||
|
*/
|
||||||
|
private processDimensionsChanged() {
|
||||||
|
_.debounce(
|
||||||
|
this._processDimensionsChanged,
|
||||||
|
250,
|
||||||
|
{
|
||||||
|
leading: true,
|
||||||
|
trailing: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
validateVideoOffsets() {
|
validateVideoOffsets() {
|
||||||
// validate if current video still exists. If not, we destroy current object
|
// validate if current video still exists. If not, we destroy current object
|
||||||
try {
|
try {
|
||||||
|
752
src/ext/lib/video-transform/Resizer.ts
Normal file
752
src/ext/lib/video-transform/Resizer.ts
Normal file
@ -0,0 +1,752 @@
|
|||||||
|
import Debug from '../../conf/Debug';
|
||||||
|
import Scaler, { CropStrategy, VideoDimensions } from './Scaler';
|
||||||
|
import Stretcher from './Stretcher';
|
||||||
|
import Zoom from './Zoom';
|
||||||
|
import PlayerData from '../video-data/PlayerData';
|
||||||
|
import ExtensionMode from '../../../common/enums/ExtensionMode.enum';
|
||||||
|
import StretchType from '../../../common/enums/StretchType.enum';
|
||||||
|
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
|
||||||
|
import AspectRatioType from '../../../common/enums/AspectRatioType.enum';
|
||||||
|
import CropModePersistance from '../../../common/enums/CropModePersistence.enum';
|
||||||
|
import { sleep } from '../Util';
|
||||||
|
import Logger from '../Logger';
|
||||||
|
import Settings from '../Settings';
|
||||||
|
import VideoData from '../video-data/VideoData';
|
||||||
|
|
||||||
|
if(Debug.debug) {
|
||||||
|
console.log("Loading: Resizer.js");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Resizer {
|
||||||
|
//#region flags
|
||||||
|
canPan: boolean = false;
|
||||||
|
destroyed: boolean = false;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region helper objects
|
||||||
|
logger: Logger;
|
||||||
|
settings: Settings;
|
||||||
|
scaler: Scaler;
|
||||||
|
stretcher: Stretcher;
|
||||||
|
zoom: Zoom;
|
||||||
|
conf: VideoData;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region HTML elements
|
||||||
|
video: any;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region data
|
||||||
|
correctedVideoDimensions: any;
|
||||||
|
currentCss: any;
|
||||||
|
currentStyleString: string;
|
||||||
|
currentPlayerStyleString: any;
|
||||||
|
currentCssValidFor: any;
|
||||||
|
currentVideoSettings: any;
|
||||||
|
lastAr: {type: any, ratio?: number} = {type: AspectRatioType.Initial};
|
||||||
|
resizerId: any;
|
||||||
|
videoAlignment: any;
|
||||||
|
userCss: string;
|
||||||
|
userCssClassName: any;
|
||||||
|
pan: any = null;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
constructor(videoData) {
|
||||||
|
this.resizerId = (Math.random()*100).toFixed(0);
|
||||||
|
this.conf = videoData;
|
||||||
|
this.logger = videoData.logger;
|
||||||
|
this.video = videoData.video;
|
||||||
|
this.settings = videoData.settings;
|
||||||
|
|
||||||
|
this.scaler = new Scaler(this.conf);
|
||||||
|
this.stretcher = new Stretcher(this.conf);
|
||||||
|
this.zoom = new Zoom(this.conf);
|
||||||
|
|
||||||
|
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname); // this is initial video alignment
|
||||||
|
this.destroyed = false;
|
||||||
|
|
||||||
|
|
||||||
|
if (this.settings.active.pan) {
|
||||||
|
this.canPan = this.settings.active.miscSettings.mousePan.enabled;
|
||||||
|
} else {
|
||||||
|
this.canPan = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.userCssClassName = videoData.userCssClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectCss(css) {
|
||||||
|
this.conf.pageInfo.injectCss(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
ejectCss(css) {
|
||||||
|
this.conf.pageInfo.ejectCss(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceCss(oldCss, newCss) {
|
||||||
|
this.conf.pageInfo.replaceCss(oldCss, newCss);
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareCss(css) {
|
||||||
|
return `.${this.userCssClassName} {${css}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(){
|
||||||
|
this.logger.log('info', ['debug', 'init'], `[Resizer::destroy] <rid:${this.resizerId}> received destroy command.`);
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateRatioForLegacyOptions(ar){
|
||||||
|
// also present as modeToAr in Scaler.js
|
||||||
|
if (ar.type !== AspectRatioType.FitWidth && ar.type !== AspectRatioType.FitHeight && ar.ratio) {
|
||||||
|
return ar;
|
||||||
|
}
|
||||||
|
// Skrbi za "stare" možnosti, kot na primer "na širino zaslona", "na višino zaslona" in "ponastavi".
|
||||||
|
// Približevanje opuščeno.
|
||||||
|
// handles "legacy" options, such as 'fit to widht', 'fit to height' and AspectRatioType.Reset. No zoom tho
|
||||||
|
let ratioOut;
|
||||||
|
|
||||||
|
if (!this.conf.video) {
|
||||||
|
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] No video??",this.conf.video, "killing videoData");
|
||||||
|
this.conf.destroy();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (! this.conf.player.dimensions) {
|
||||||
|
ratioOut = screen.width / screen.height;
|
||||||
|
} else {
|
||||||
|
this.logger.log('info', 'debug', `[Resizer::calculateRatioForLegacyOptions] <rid:${this.resizerId}> Player dimensions:`, this.conf.player.dimensions.width ,'x', this.conf.player.dimensions.height,'aspect ratio:', this.conf.player.dimensions.width / this.conf.player.dimensions.height)
|
||||||
|
ratioOut = this.conf.player.dimensions.width / this.conf.player.dimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// POMEMBNO: lastAr je potrebno nastaviti šele po tem, ko kličemo _res_setAr(). _res_setAr() predvideva,
|
||||||
|
// da želimo nastaviti statično (type: 'static') razmerje stranic — tudi, če funkcijo kličemo tu oz. v ArDetect.
|
||||||
|
//
|
||||||
|
// IMPORTANT NOTE: lastAr needs to be set after _res_setAr() is called, as _res_setAr() assumes we're
|
||||||
|
// setting a static aspect ratio (even if the function is called from here or ArDetect).
|
||||||
|
|
||||||
|
let fileAr = this.conf.video.videoWidth / this.conf.video.videoHeight;
|
||||||
|
|
||||||
|
if (ar.type === AspectRatioType.FitWidth){
|
||||||
|
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
|
||||||
|
}
|
||||||
|
else if(ar.type === AspectRatioType.FitHeight){
|
||||||
|
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
|
||||||
|
}
|
||||||
|
else if(ar.type === AspectRatioType.Reset){
|
||||||
|
this.logger.log('info', 'debug', "[Scaler.js::modeToAr] Using original aspect ratio -", fileAr);
|
||||||
|
ar.ratio = fileAr;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ar;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateAr(ar) {
|
||||||
|
if (!ar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some options require a bit more testing re: whether they make sense
|
||||||
|
// if they don't, we refuse to update aspect ratio until they do
|
||||||
|
if (ar.type === AspectRatioType.Automatic || ar.type === AspectRatioType.Fixed) {
|
||||||
|
if (!ar.ratio || isNaN(ar.ratio)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update aspect ratio if there's a difference between the old and the new state
|
||||||
|
if (!this.lastAr || ar.type !== this.lastAr.type || ar.ratio !== this.lastAr.ratio) {
|
||||||
|
this.setAr(ar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAr(ar: {type: any, ratio?: number}, lastAr?: {type: any, ratio?: number}) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.video.videoWidth || !this.video.videoHeight) {
|
||||||
|
this.logger.log('warning', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.');
|
||||||
|
this.conf.videoUnloaded();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('info', 'debug', '[Resizer::setAr] <rid:'+this.resizerId+'> trying to set ar. New ar:', ar)
|
||||||
|
|
||||||
|
if (ar == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteSettings = this.settings.active.sites[window.location.hostname];
|
||||||
|
let stretchFactors: {xFactor: number, yFactor: number, arCorrectionFactor?: number, ratio?: number} | any;
|
||||||
|
|
||||||
|
// reset zoom, but only on aspect ratio switch. We also know that aspect ratio gets converted to
|
||||||
|
// AspectRatioType.Fixed when zooming, so let's keep that in mind
|
||||||
|
if (
|
||||||
|
(ar.type !== AspectRatioType.Fixed && ar.type !== AspectRatioType.Manual) // anything not these two _always_ changes AR
|
||||||
|
|| ar.type !== this.lastAr.type // this also means aspect ratio has changed
|
||||||
|
|| ar.ratio !== this.lastAr.ratio // this also means aspect ratio has changed
|
||||||
|
) {
|
||||||
|
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 === AspectRatioType.Automatic ||
|
||||||
|
ar.type === AspectRatioType.Reset ||
|
||||||
|
ar.type === AspectRatioType.Initial ) {
|
||||||
|
// reset/undo default
|
||||||
|
this.conf.pageInfo.updateCurrentCrop(undefined);
|
||||||
|
} else {
|
||||||
|
this.conf.pageInfo.updateCurrentCrop(ar);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastAr) {
|
||||||
|
this.lastAr = this.calculateRatioForLegacyOptions(lastAr);
|
||||||
|
ar = this.calculateRatioForLegacyOptions(ar);
|
||||||
|
} else {
|
||||||
|
// NOTE: "fitw" "fith" and "reset" should ignore ar.ratio bit, but
|
||||||
|
// I'm not sure whether they do. Check that.
|
||||||
|
ar = this.calculateRatioForLegacyOptions(ar);
|
||||||
|
if (! ar) {
|
||||||
|
this.logger.log('info', 'resizer', `[Resizer::setAr] <${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastAr = {type: ar.type, ratio: ar.ratio}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (this.extensionMode === ExtensionMode.Basic && !PlayerData.isFullScreen() && ar.type !== AspectRatioType.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// pause AR on:
|
||||||
|
// * ar.type NOT automatic
|
||||||
|
// * ar.type is auto, but stretch is set to basic basic stretch
|
||||||
|
//
|
||||||
|
// unpause when using other modes
|
||||||
|
if (ar.type !== AspectRatioType.Automatic || this.stretcher.mode === StretchType.Basic) {
|
||||||
|
this.conf?.arDetector?.pause();
|
||||||
|
} else {
|
||||||
|
if (this.lastAr.type === AspectRatioType.Automatic) {
|
||||||
|
this.conf?.arDetector?.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do stretch thingy
|
||||||
|
if (this.stretcher.mode === StretchType.NoStretch
|
||||||
|
|| this.stretcher.mode === StretchType.Conditional
|
||||||
|
|| this.stretcher.mode === StretchType.FixedSource){
|
||||||
|
|
||||||
|
stretchFactors = this.scaler.calculateCrop(ar);
|
||||||
|
|
||||||
|
if(! stretchFactors || stretchFactors.error){
|
||||||
|
this.logger.log('error', 'debug', `[Resizer::setAr] <rid:${this.resizerId}> failed to set AR due to problem with calculating crop. Error:`, stretchFactors?.error);
|
||||||
|
if (stretchFactors?.error === 'no_video'){
|
||||||
|
this.conf.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we could have issued calculate crop too early. Let's tell VideoData that there's something wrong
|
||||||
|
// and exit this function. When <video> will receive onloadeddata or ontimeupdate (receiving either
|
||||||
|
// of the two means video is loaded or playing, and that means video has proper dimensions), it will
|
||||||
|
// try to reset or re-apply aspect ratio when the video is finally ready.
|
||||||
|
if (stretchFactors?.error === 'illegal_video_dimensions') {
|
||||||
|
this.conf.videoDimensionsLoaded = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stretcher.mode === StretchType.Conditional){
|
||||||
|
this.stretcher.applyConditionalStretch(stretchFactors, ar.ratio);
|
||||||
|
} else if (this.stretcher.mode === StretchType.FixedSource) {
|
||||||
|
this.stretcher.applyStretchFixedSource(stretchFactors);
|
||||||
|
}
|
||||||
|
this.logger.log('info', 'debug', "[Resizer::setAr] Processed stretch factors for ",
|
||||||
|
this.stretcher.mode === StretchType.NoStretch ? 'stretch-free crop.' :
|
||||||
|
this.stretcher.mode === StretchType.Conditional ? 'crop with conditional StretchType.' : 'crop with fixed stretch',
|
||||||
|
'Stretch factors are:', stretchFactors
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (this.stretcher.mode === StretchType.Hybrid) {
|
||||||
|
stretchFactors = this.stretcher.calculateStretch(ar.ratio);
|
||||||
|
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for hybrid stretch/crop. Stretch factors are:', stretchFactors);
|
||||||
|
} else if (this.stretcher.mode === StretchType.Fixed) {
|
||||||
|
stretchFactors = this.stretcher.calculateStretchFixed(ar.ratio)
|
||||||
|
} else if (this.stretcher.mode === StretchType.Basic) {
|
||||||
|
stretchFactors = this.stretcher.calculateBasicStretch();
|
||||||
|
this.logger.log('info', 'debug', '[Resizer::setAr] Processed stretch factors for basic StretchType. Stretch factors are:', stretchFactors);
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.zoom.applyZoom(stretchFactors);
|
||||||
|
|
||||||
|
this.stretcher.chromeBugMitigation(stretchFactors);
|
||||||
|
|
||||||
|
let translate = this.computeOffsets(stretchFactors);
|
||||||
|
|
||||||
|
this.applyCss(stretchFactors, translate);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
toFixedAr() {
|
||||||
|
// converting to fixed AR means we also turn off autoAR
|
||||||
|
this.setAr({
|
||||||
|
ratio: this.lastAr.ratio,
|
||||||
|
type: AspectRatioType.Fixed
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLastAr() {
|
||||||
|
this.lastAr = {type: AspectRatioType.Initial};
|
||||||
|
}
|
||||||
|
|
||||||
|
setLastAr(override){
|
||||||
|
this.lastAr = override;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastAr(){
|
||||||
|
return this.lastAr;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStretchMode(stretchMode, fixedStretchRatio?){
|
||||||
|
this.stretcher.setStretchMode(stretchMode, fixedStretchRatio);
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
panHandler(event, forcePan) {
|
||||||
|
if (this.canPan || forcePan) {
|
||||||
|
if(!this.conf.player || !this.conf.player.element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// dont allow weird floats
|
||||||
|
this.videoAlignment = VideoAlignmentType.Center;
|
||||||
|
|
||||||
|
// because non-fixed aspect ratios reset panning:
|
||||||
|
if (this.lastAr.type !== AspectRatioType.Fixed) {
|
||||||
|
this.toFixedAr();
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = this.conf.player.element;
|
||||||
|
|
||||||
|
const relativeX = (event.pageX - player.offsetLeft) / player.offsetWidth;
|
||||||
|
const relativeY = (event.pageY - player.offsetTop) / player.offsetHeight;
|
||||||
|
|
||||||
|
this.logger.log('info', 'mousemove', "[Resizer::panHandler] mousemove.pageX, pageY:", event.pageX, event.pageY, "\nrelativeX/Y:", relativeX, relativeY)
|
||||||
|
|
||||||
|
this.setPan(relativeX, relativeY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPan() {
|
||||||
|
this.pan = {x: 0, y: 0};
|
||||||
|
this.videoAlignment = this.settings.getDefaultVideoAlignment(window.location.hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if(! this.pan){
|
||||||
|
this.pan = {x: 0, y: 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.settings.active.miscSettings.mousePanReverseMouse) {
|
||||||
|
this.pan.relativeOffsetX = (relativeMousePosX * 1.1) - 0.55;
|
||||||
|
this.pan.relativeOffsetY = (relativeMousePosY * 1.1) - 0.55;
|
||||||
|
} else {
|
||||||
|
this.pan.relativeOffsetX = -(relativeMousePosX * 1.1) + 0.55;
|
||||||
|
this.pan.relativeOffsetY = -(relativeMousePosY * 1.1) + 0.55;
|
||||||
|
}
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
setVideoAlignment(videoAlignment) {
|
||||||
|
this.videoAlignment = videoAlignment;
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
this.logger.log('info', 'debug', "[Resizer::restore] <rid:"+this.resizerId+"> attempting to restore aspect ratio", {'lastAr': this.lastAr} );
|
||||||
|
|
||||||
|
// this is true until we verify that css has actually been applied
|
||||||
|
if(this.lastAr.type === AspectRatioType.Initial){
|
||||||
|
this.setAr({type: AspectRatioType.Reset});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (this.lastAr?.ratio === null) {
|
||||||
|
// if this is the case, we do nothing as we have the correct aspect ratio
|
||||||
|
// throw "Last ar is null!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setAr(this.lastAr, this.lastAr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset(){
|
||||||
|
this.setStretchMode(this.settings.active.sites[window.location.hostname]?.stretch ?? this.settings.active.sites['@global'].stretch);
|
||||||
|
this.zoom.setZoom(1);
|
||||||
|
this.resetPan();
|
||||||
|
this.setAr({type: AspectRatioType.Reset});
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanMode(mode) {
|
||||||
|
if (mode === 'enable') {
|
||||||
|
this.canPan = true;
|
||||||
|
} else if (mode === 'disable') {
|
||||||
|
this.canPan = false;
|
||||||
|
} else if (mode === 'toggle') {
|
||||||
|
this.canPan = !this.canPan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setZoom(zoomLevel, no_announce) {
|
||||||
|
this.zoom.setZoom(zoomLevel, no_announce);
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomStep(step){
|
||||||
|
this.zoom.zoomStep(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetZoom(){
|
||||||
|
this.zoom.setZoom(1);
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCrop(){
|
||||||
|
this.setAr({type: AspectRatioType.Reset});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetStretch(){
|
||||||
|
this.stretcher.setStretchMode(StretchType.NoStretch);
|
||||||
|
this.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// mostly internal stuff
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the video file _as displayed_ on the screen.
|
||||||
|
* Consider the following example:
|
||||||
|
*
|
||||||
|
* * player dimensions are 2560x1080
|
||||||
|
* * <video> is child of player
|
||||||
|
* * <video> has the following css: {width: 100%, height: 100%}
|
||||||
|
* * video file dimensions are 1280x720
|
||||||
|
*
|
||||||
|
* CSS will ensure that the dimensions of <video> tag are equal to the dimension of the
|
||||||
|
* player element — that is, 2560x1080px. This is no bueno, because the browser will upscale
|
||||||
|
* the video file to take up as much space as it can (without stretching it). This means
|
||||||
|
* we'll get a 1920x1080 video (as displayed) and a letterbox.
|
||||||
|
*
|
||||||
|
* We can't get that number out of anywhere: video.videoWidth will return 1280 (video file
|
||||||
|
* dimensions) and .offsetWidth (and the likes) will return the <video> tag dimension. Neither
|
||||||
|
* will return the actual size of video as displayed, which we need in order to calculate the
|
||||||
|
* extra space to the left and right of the video.
|
||||||
|
*
|
||||||
|
* We make the assumption of the
|
||||||
|
*/
|
||||||
|
computeVideoDisplayedDimensions() {
|
||||||
|
const offsetWidth = this.conf.video.offsetWidth;
|
||||||
|
const offsetHeight = this.conf.video.offsetHeight;
|
||||||
|
|
||||||
|
const scaleX = offsetWidth / this.conf.video.videoWidth;
|
||||||
|
const scaleY = offsetHeight / this.conf.video.videoHeight;
|
||||||
|
|
||||||
|
// if differences between the scale factors are minimal, we presume offsetWidth and
|
||||||
|
// offsetHeight are the accurate enough for our needs
|
||||||
|
if (Math.abs(scaleX - scaleY) < 0.02) {
|
||||||
|
return {
|
||||||
|
realVideoWidth: offsetWidth,
|
||||||
|
realVideoHeight: offsetHeight,
|
||||||
|
marginX: 0,
|
||||||
|
marginY: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we're still here, we need to calculate real video dimensions
|
||||||
|
const diffX = Math.abs(scaleY * this.conf.video.videoWidth - offsetWidth);
|
||||||
|
const diffY = Math.abs(scaleX * this.conf.video.videoHeight - offsetHeight);
|
||||||
|
|
||||||
|
// in this case, we want to base our real dimensions off scaleX
|
||||||
|
// otherwise, we want to base it off scaleY
|
||||||
|
if (diffX < diffY) {
|
||||||
|
const realHeight = this.conf.video.videoHeight * scaleX;
|
||||||
|
return {
|
||||||
|
realVideoWidth: offsetWidth,
|
||||||
|
realVideoHeight: realHeight,
|
||||||
|
marginX: 0,
|
||||||
|
marginY: (offsetHeight - realHeight) * 0.5
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const realWidth = this.conf.video.videoWidth * scaleY;
|
||||||
|
return {
|
||||||
|
realVideoWidth: realWidth,
|
||||||
|
realVideoHeight: offsetHeight,
|
||||||
|
marginX: (offsetWidth - realWidth) * 0.5,
|
||||||
|
marginY: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sometimes, sites (e.g. new reddit) will guarantee that video fits width of its container
|
||||||
|
* and let the browser figure out the height through the magic of height:auto. This is bad,
|
||||||
|
* because our addon generally relies of videos always being 100% of the height of the
|
||||||
|
* container.
|
||||||
|
*
|
||||||
|
* This sometimes leads to a situation where realVideoHeight and realVideoWidth — at least
|
||||||
|
* one of which should be roughly equal to the player width or hight with the other one being
|
||||||
|
* either smaller or equal — are both smaller than player width or height; and sometimes
|
||||||
|
* rather substantially. Fortunately for us, realVideo[Width|Height] and player dimensions
|
||||||
|
* never lie, which allows us to calculate the extra scale factor we need.
|
||||||
|
*
|
||||||
|
* Returned factor for this function should do fit:contain, not fit:cover.
|
||||||
|
* @param realVideoWidth real video width
|
||||||
|
* @param realVideoHeight real video height
|
||||||
|
* @param playerWidth player width
|
||||||
|
* @param playerHeight player height
|
||||||
|
* @param mode whether to
|
||||||
|
*/
|
||||||
|
computeAutoHeightCompensationFactor(realVideoWidth: number, realVideoHeight: number, playerWidth: number, playerHeight: number, mode: 'height' | 'width'): number {
|
||||||
|
const widthFactor = playerWidth / realVideoWidth;
|
||||||
|
const heightFactor = playerHeight / realVideoHeight;
|
||||||
|
|
||||||
|
return mode === 'height' ? heightFactor : widthFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
computeOffsets(stretchFactors: VideoDimensions){
|
||||||
|
this.logger.log('info', 'debug', "[Resizer::computeOffsets] <rid:"+this.resizerId+"> video will be aligned to ", this.settings.active.sites['@global'].videoAlignment);
|
||||||
|
|
||||||
|
const {realVideoWidth, realVideoHeight, marginX, marginY} = this.computeVideoDisplayedDimensions();
|
||||||
|
|
||||||
|
// correct any remaining element size discrepencies (applicable only to certain crop strategies!)
|
||||||
|
// NOTE: it's possible that we might also need to apply a similar measure for CropPillarbox strategy
|
||||||
|
// (but we'll wait for bug reports before doing so).
|
||||||
|
// We also don't compensate for height:auto if height is provided via element style
|
||||||
|
let autoHeightCompensationFactor;
|
||||||
|
if (
|
||||||
|
stretchFactors.cropStrategy === CropStrategy.CropLetterbox
|
||||||
|
&& (!stretchFactors.styleHeightCompensationFactor || stretchFactors.styleHeightCompensationFactor === 1)
|
||||||
|
) {
|
||||||
|
autoHeightCompensationFactor = this.computeAutoHeightCompensationFactor(realVideoWidth, realVideoHeight, this.conf.player.dimensions.width, this.conf.player.dimensions.height, 'height');
|
||||||
|
stretchFactors.xFactor *= autoHeightCompensationFactor;
|
||||||
|
stretchFactors.yFactor *= autoHeightCompensationFactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wdiff = this.conf.player.dimensions.width - realVideoWidth;
|
||||||
|
const hdiff = this.conf.player.dimensions.height - realVideoHeight;
|
||||||
|
|
||||||
|
if (wdiff < 0 && hdiff < 0 && this.zoom.scale > 1) {
|
||||||
|
this.conf.resizer.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
const wdiffAfterZoom = realVideoWidth * stretchFactors.xFactor - this.conf.player.dimensions.width;
|
||||||
|
const hdiffAfterZoom = realVideoHeight * stretchFactors.yFactor - this.conf.player.dimensions.height;
|
||||||
|
|
||||||
|
const translate = {
|
||||||
|
x: wdiff * 0.5,
|
||||||
|
y: hdiff * 0.5,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (this.pan.relativeOffsetX || this.pan.relativeOffsetY) {
|
||||||
|
// don't offset when video is smaller than player
|
||||||
|
if(wdiffAfterZoom >= 0 || hdiffAfterZoom >= 0) {
|
||||||
|
translate.x += wdiffAfterZoom * this.pan.relativeOffsetX * this.zoom.scale;
|
||||||
|
translate.y += hdiffAfterZoom * this.pan.relativeOffsetY * this.zoom.scale;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.videoAlignment == VideoAlignmentType.Left) {
|
||||||
|
translate.x += wdiffAfterZoom * 0.5;
|
||||||
|
}
|
||||||
|
else if (this.videoAlignment == VideoAlignmentType.Right) {
|
||||||
|
translate.x -= wdiffAfterZoom * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
'info', ['debug', 'resizer'], "[Resizer::_res_computeOffsets] <rid:"+this.resizerId+"> calculated offsets:",
|
||||||
|
'\n\n---- elements ----',
|
||||||
|
'\nplayer element: ', this.conf.player.element,
|
||||||
|
'\nvideo element: ', this.conf.video,
|
||||||
|
'\n\n---- data in ----',
|
||||||
|
'\nplayer dimensions: ', {w: this.conf.player.dimensions.width, h: this.conf.player.dimensions.height},
|
||||||
|
'\nvideo dimensions: ', {w: this.conf.video.offsetWidth, h: this.conf.video.offsetHeight},
|
||||||
|
'\nreal video dimensions:', {w: realVideoWidth, h: realVideoHeight},
|
||||||
|
'\nauto compensation: ', 'x', autoHeightCompensationFactor,
|
||||||
|
'\nstretch factors: ', stretchFactors,
|
||||||
|
'\npan & zoom: ', this.pan, this.zoom.scale,
|
||||||
|
'\nwdiff, hdiff: ', wdiff, 'x', hdiff,
|
||||||
|
'\nwdiff, hdiffAfterZoom:', wdiffAfterZoom, 'x', hdiffAfterZoom,
|
||||||
|
'\n\n---- data out ----\n',
|
||||||
|
'translate:', translate
|
||||||
|
);
|
||||||
|
|
||||||
|
// by the way, let's do a quick sanity check whether video player is doing any fuckies wuckies
|
||||||
|
// fucky wucky examples:
|
||||||
|
//
|
||||||
|
// * video width is bigger than player width AND video height is bigger than player height
|
||||||
|
// * video width is smaller than player width AND video height is smaller than player height
|
||||||
|
//
|
||||||
|
// In both examples, at most one of the two conditions can be true at the same time. If both
|
||||||
|
// conditions are true at the same time, we need to go 'chiny reckon' and recheck our player
|
||||||
|
// element. Chances are our video is not getting aligned correctly
|
||||||
|
if (
|
||||||
|
(this.conf.video.offsetWidth > this.conf.player.dimensions.width && this.conf.video.offsetHeight > this.conf.player.dimensions.height) ||
|
||||||
|
(this.conf.video.offsetWidth < this.conf.player.dimensions.width && this.conf.video.offsetHeight < this.conf.player.dimensions.height)
|
||||||
|
) {
|
||||||
|
this.logger.log('warn', ['debugger', 'resizer'], `[Resizer::_res_computeOffsets] <rid:${this.resizerId}> We are getting some incredibly funny results here.\n\n`,
|
||||||
|
`Video seems to be both wider and taller (or shorter and narrower) than player element at the same time. This is super duper not supposed to happen.\n\n`,
|
||||||
|
`Player element needs to be checked.`
|
||||||
|
)
|
||||||
|
if (this.conf.player.checkPlayerSizeChange()) {
|
||||||
|
this.conf.player.onPlayerDimensionsChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region css handling
|
||||||
|
buildStyleArray(existingStyleString, extraStyleString) {
|
||||||
|
if (existingStyleString) {
|
||||||
|
const styleArray = existingStyleString.split(";");
|
||||||
|
|
||||||
|
if (extraStyleString) {
|
||||||
|
const extraCss = extraStyleString.split(';');
|
||||||
|
let dup = false;
|
||||||
|
|
||||||
|
for (const ecss of extraCss) {
|
||||||
|
for (let i in styleArray) {
|
||||||
|
if (ecss.split(':')[0].trim() === styleArray[i].split(':')[0].trim()) {
|
||||||
|
dup = true;
|
||||||
|
styleArray[i] = ecss;
|
||||||
|
}
|
||||||
|
if (dup) {
|
||||||
|
dup = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
styleArray.push(ecss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i in styleArray) {
|
||||||
|
styleArray[i] = styleArray[i].trim();
|
||||||
|
// some sites do 'top: 50%; left: 50%; transform: <transform>' to center videos.
|
||||||
|
// we dont wanna, because we already center videos on our own
|
||||||
|
if (styleArray[i].startsWith("transform:")
|
||||||
|
|| styleArray[i].startsWith("top:")
|
||||||
|
|| styleArray[i].startsWith("left:")
|
||||||
|
|| styleArray[i].startsWith("right:")
|
||||||
|
|| styleArray[i].startsWith("bottom:")
|
||||||
|
|| styleArray[i].startsWith("margin")
|
||||||
|
){
|
||||||
|
delete styleArray[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styleArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
buildStyleString(styleArray) {
|
||||||
|
let styleString = '';
|
||||||
|
|
||||||
|
for(let i in styleArray) {
|
||||||
|
if(styleArray[i]) {
|
||||||
|
styleString += styleArray[i] + " !important; ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return styleString;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyCss(stretchFactors, translate){
|
||||||
|
// apply extra CSS here. In case of duplicated properties, extraCss overrides
|
||||||
|
// default styleString
|
||||||
|
if (! this.video) {
|
||||||
|
this.logger.log('warn', 'debug', "[Resizer::applyCss] <rid:"+this.resizerId+"> Video went missing, doing nothing.");
|
||||||
|
|
||||||
|
this.conf.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log('info', ['debug', 'resizer'], "[Resizer::applyCss] <rid:"+this.resizerId+"> will apply css.", {stretchFactors, translate});
|
||||||
|
|
||||||
|
// save stuff for quick tests (before we turn numbers into css values):
|
||||||
|
this.currentVideoSettings = {
|
||||||
|
validFor: this.conf.player.dimensions,
|
||||||
|
// videoWidth: dimensions.width,
|
||||||
|
// videoHeight: dimensions.height
|
||||||
|
}
|
||||||
|
|
||||||
|
let extraStyleString;
|
||||||
|
try {
|
||||||
|
extraStyleString = this.settings.active.sites[window.location.hostname].DOM.video.additionalCss;
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing. It's ok if no special settings are defined for this site, we'll just do defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleArray = this.buildStyleArray('', extraStyleString)
|
||||||
|
|
||||||
|
// add remaining elements
|
||||||
|
if (stretchFactors) {
|
||||||
|
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
|
||||||
|
// restrictions. If site has dumb shit like 'max-width: 100%' and 'max-height: 100vh' in their CSS, that
|
||||||
|
// shit will prevent us from applying desired crop. This means we need to tell websites to fuck off with
|
||||||
|
// that crap. We know better.
|
||||||
|
styleArray.push("max-width: none !important; max-height: none !important;");
|
||||||
|
}
|
||||||
|
const styleString = `${this.buildStyleString(styleArray)}${extraStyleString || ''}`; // string returned by buildStyleString() should end with ; anyway
|
||||||
|
|
||||||
|
// build style string back
|
||||||
|
this.setStyleString(styleString);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStyleString (styleString) {
|
||||||
|
this.currentCssValidFor = this.conf.player.dimensions;
|
||||||
|
const newCssString = this.prepareCss(styleString);
|
||||||
|
|
||||||
|
// inject new CSS or replace existing one
|
||||||
|
if (!this.userCss) {
|
||||||
|
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Setting new css: ", newCssString);
|
||||||
|
|
||||||
|
this.injectCss(newCssString);
|
||||||
|
this.userCss = newCssString;
|
||||||
|
} else if (newCssString !== this.userCss) {
|
||||||
|
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Replacing css.\nOld string:", this.userCss, "\nNew string:", newCssString);
|
||||||
|
// we only replace css if it
|
||||||
|
this.replaceCss(this.userCss, newCssString);
|
||||||
|
this.userCss = newCssString;
|
||||||
|
} else {
|
||||||
|
this.logger.log('info', ['debug', 'resizer'], "[Resizer::setStyleString] <rid:"+this.resizerId+"> Existing css is still valid, doing nothing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Resizer;
|
@ -5,6 +5,32 @@ import VideoData from '../video-data/VideoData';
|
|||||||
import Logger from '../Logger';
|
import Logger from '../Logger';
|
||||||
|
|
||||||
|
|
||||||
|
export enum CropStrategy {
|
||||||
|
/**
|
||||||
|
* Nomenclature explained:
|
||||||
|
*
|
||||||
|
* SP - stream AR < player AR
|
||||||
|
* PS - the opposite of ↑
|
||||||
|
*
|
||||||
|
* ArDominant - given aspect ratio is bigger than stream AR and player AR
|
||||||
|
* PSDominant - stream AR or player AR are bigger than given aspect ratio
|
||||||
|
*/
|
||||||
|
CropLetterbox = 1,
|
||||||
|
NoCropPillarbox = 2,
|
||||||
|
NoCropLetterbox = 3,
|
||||||
|
CropPillarbox = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VideoDimensions = {
|
||||||
|
xFactor?: number;
|
||||||
|
yFactor?: number;
|
||||||
|
cropStrategy?: number;
|
||||||
|
arCorrectionFactor?: number;
|
||||||
|
styleHeightCompensationFactor?: number;
|
||||||
|
actualWidth?: number;
|
||||||
|
actualHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
// računa velikost videa za približevanje/oddaljevanje
|
// računa velikost videa za približevanje/oddaljevanje
|
||||||
// does video size calculations for zooming/cropping
|
// does video size calculations for zooming/cropping
|
||||||
|
|
||||||
@ -72,7 +98,7 @@ class Scaler {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
calculateCrop(ar) {
|
calculateCrop(ar: {type: AspectRatioType, ratio?: number}) {
|
||||||
/**
|
/**
|
||||||
* STEP 1: NORMALIZE ASPECT RATIO
|
* STEP 1: NORMALIZE ASPECT RATIO
|
||||||
*
|
*
|
||||||
@ -151,12 +177,13 @@ class Scaler {
|
|||||||
|
|
||||||
this.logger.log('info', 'scaler', "[Scaler::calculateCrop] ar is " ,ar.ratio, ", file ar is", streamAr, ", this.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
|
this.logger.log('info', 'scaler', "[Scaler::calculateCrop] ar is " ,ar.ratio, ", file ar is", streamAr, ", this.conf.player.dimensions are ", this.conf.player.dimensions.width, "×", this.conf.player.dimensions.height, "| obj:", this.conf.player.dimensions);
|
||||||
|
|
||||||
const videoDimensions = {
|
const videoDimensions: VideoDimensions = {
|
||||||
xFactor: 1,
|
xFactor: 1,
|
||||||
yFactor: 1,
|
yFactor: 1,
|
||||||
actualWidth: 0, // width of the video (excluding pillarbox) when <video> tag height is equal to width
|
actualWidth: 0, // width of the video (excluding pillarbox) when <video> tag height is equal to width
|
||||||
actualHeight: 0, // height of the video (excluding letterbox) when <video> tag height is equal to height
|
actualHeight: 0, // height of the video (excluding letterbox) when <video> tag height is equal to height
|
||||||
arCorrectionFactor: arCorrectionFactor,
|
arCorrectionFactor: arCorrectionFactor,
|
||||||
|
styleHeightCompensationFactor: heightCompensationFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
this.calculateCropCore(videoDimensions, ar.ratio, streamAr, playerAr)
|
this.calculateCropCore(videoDimensions, ar.ratio, streamAr, playerAr)
|
||||||
@ -172,18 +199,20 @@ class Scaler {
|
|||||||
* @param {*} streamAr
|
* @param {*} streamAr
|
||||||
* @param {*} playerAr
|
* @param {*} playerAr
|
||||||
*/
|
*/
|
||||||
calculateCropCore(videoDimensions, ar, streamAr, playerAr) {
|
calculateCropCore(videoDimensions: VideoDimensions, ar: number, streamAr: number, playerAr: number) {
|
||||||
if (streamAr < playerAr) {
|
if (streamAr < playerAr) {
|
||||||
if (streamAr < ar){
|
if (streamAr < ar){
|
||||||
// in this situation we have to crop letterbox on top/bottom of the player
|
// in this situation we have to crop letterbox on top/bottom of the player
|
||||||
// we cut it, but never more than the player
|
// we cut it, but never more than the player
|
||||||
videoDimensions.xFactor = Math.min(ar, playerAr) / streamAr;
|
videoDimensions.xFactor = Math.min(ar, playerAr) / streamAr;
|
||||||
videoDimensions.yFactor = videoDimensions.xFactor;
|
videoDimensions.yFactor = videoDimensions.xFactor;
|
||||||
|
videoDimensions.cropStrategy = CropStrategy.CropLetterbox;
|
||||||
} else {
|
} else {
|
||||||
// in this situation, we would be cutting pillarbox. Inside horizontal player.
|
// in this situation, we would be cutting pillarbox. Inside horizontal player.
|
||||||
// I don't think so. Except exceptions, we'll wait for bug reports.
|
// I don't think so. Except exceptions, we'll wait for bug reports.
|
||||||
videoDimensions.xFactor = 1;
|
videoDimensions.xFactor = 1;
|
||||||
videoDimensions.yFactor = 1;
|
videoDimensions.yFactor = 1;
|
||||||
|
videoDimensions.cropStrategy = CropStrategy.NoCropPillarbox;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (streamAr < ar || playerAr < ar){
|
if (streamAr < ar || playerAr < ar){
|
||||||
@ -191,10 +220,12 @@ class Scaler {
|
|||||||
// this means we simply don't crop anything _at all_
|
// this means we simply don't crop anything _at all_
|
||||||
videoDimensions.xFactor = 1;
|
videoDimensions.xFactor = 1;
|
||||||
videoDimensions.yFactor = 1;
|
videoDimensions.yFactor = 1;
|
||||||
|
videoDimensions.cropStrategy = CropStrategy.NoCropLetterbox;
|
||||||
} else {
|
} else {
|
||||||
// meant for handling pillarbox crop. not quite implemented.
|
// meant for handling pillarbox crop. not quite implemented.
|
||||||
videoDimensions.xFactor = streamAr / Math.min(ar.ratio, playerAr);
|
videoDimensions.xFactor = streamAr / Math.min(ar, playerAr);
|
||||||
videoDimensions.yFactor = videoDimensions.xFactor;
|
videoDimensions.yFactor = videoDimensions.xFactor;
|
||||||
|
videoDimensions.cropStrategy = CropStrategy.CropPillarbox;
|
||||||
// videoDimensions.xFactor = Math.max(ar.ratio, playerAr) * fileAr;
|
// videoDimensions.xFactor = Math.max(ar.ratio, playerAr) * fileAr;
|
||||||
// videoDimensions.yFactor = videoDimensions.xFactor;
|
// videoDimensions.yFactor = videoDimensions.xFactor;
|
||||||
}
|
}
|
||||||
|
@ -41,13 +41,13 @@ class UwUi {
|
|||||||
if (!this.logger) {
|
if (!this.logger) {
|
||||||
const loggingOptions = {
|
const loggingOptions = {
|
||||||
isContentScript: true,
|
isContentScript: true,
|
||||||
allowLogging: true,
|
allowLogging: false,
|
||||||
useConfFromStorage: true,
|
useConfFromStorage: true,
|
||||||
fileOptions: {
|
fileOptions: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
consoleOptions: {
|
consoleOptions: {
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"debug": true,
|
"debug": true,
|
||||||
"init": true,
|
"init": true,
|
||||||
"settings": true,
|
"settings": true,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Ultrawidify",
|
"name": "Ultrawidify",
|
||||||
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
|
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
|
||||||
"version": "5.0.0",
|
"version": "5.0.0.1",
|
||||||
"applications": {
|
"applications": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
"id": "{cf02b1a7-a01a-4e37-a609-516a283f1ed3}"
|
"id": "{cf02b1a7-a01a-4e37-a609-516a283f1ed3}"
|
||||||
|
@ -108,7 +108,7 @@
|
|||||||
<div class="flex flex-input">
|
<div class="flex flex-input">
|
||||||
<input type="number"
|
<input type="number"
|
||||||
step="any"
|
step="any"
|
||||||
:value="settings.active.StretchType.conditionalDifferencePercent"
|
:value="settings.active.stretch.conditionalDifferencePercent"
|
||||||
@input="updateStretchThreshold($event.target.value)"
|
@input="updateStretchThreshold($event.target.value)"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@ -166,9 +166,9 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
Stretch: Stretch,
|
StretchType: StretchType,
|
||||||
ExtensionMode: ExtensionMode,
|
ExtensionMode: ExtensionMode,
|
||||||
VideoAlignment: VideoAlignment,
|
VideoAlignmentType: VideoAlignmentType,
|
||||||
stretchThreshold: 0,
|
stretchThreshold: 0,
|
||||||
corruptedSettingsError: false,
|
corruptedSettingsError: false,
|
||||||
downloadPermissionError: false,
|
downloadPermissionError: false,
|
||||||
@ -198,7 +198,7 @@ export default {
|
|||||||
if (!newThreshold || isNaN(newThreshold)) {
|
if (!newThreshold || isNaN(newThreshold)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.settings.active.StretchType.conditionalDifferencePercent = newThreshold;
|
this.settings.active.stretch.conditionalDifferencePercent = newThreshold;
|
||||||
this.settings.save();
|
this.settings.save();
|
||||||
},
|
},
|
||||||
resetSettings() {
|
resetSettings() {
|
||||||
|
@ -39,7 +39,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addonVersion: '[extension version not loaded. This is a bug.]',
|
addonVersion: '[extension version not loaded. This is a bug.]',
|
||||||
mailtoLink: 'mailto:tamius.han@gmail.com',
|
mailtoLink: 'mailto:tamius.han+ultrawidify@gmail.com',
|
||||||
redditLink: '',
|
redditLink: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -121,7 +121,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
Stretch: Stretch,
|
StretchType: StretchType,
|
||||||
action: {
|
action: {
|
||||||
name: 'New action',
|
name: 'New action',
|
||||||
label: 'New action',
|
label: 'New action',
|
||||||
|
@ -121,7 +121,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
Stretch: Stretch,
|
StretchType: StretchType,
|
||||||
tableVisibility: {
|
tableVisibility: {
|
||||||
crop: true,
|
crop: true,
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ import StretchType from '../../../common/enums/StretchType.enum';
|
|||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
Stretch: Stretch,
|
StretchType: StretchType,
|
||||||
ActionList: ActionList,
|
ActionList: ActionList,
|
||||||
selectedAction: undefined,
|
selectedAction: undefined,
|
||||||
selectedArgument: undefined,
|
selectedArgument: undefined,
|
||||||
|
@ -216,12 +216,13 @@ import Comms from '../ext/lib/comms/Comms';
|
|||||||
import VideoPanel from './panels/VideoPanel';
|
import VideoPanel from './panels/VideoPanel';
|
||||||
import PerformancePanel from './panels/PerformancePanel';
|
import PerformancePanel from './panels/PerformancePanel';
|
||||||
import Settings from '../ext/lib/Settings';
|
import Settings from '../ext/lib/Settings';
|
||||||
import ExecAction from './js/ExecAction.js';
|
import ExecAction from './js/ExecAction';
|
||||||
import DefaultSettingsPanel from './panels/DefaultSettingsPanel';
|
import DefaultSettingsPanel from './panels/DefaultSettingsPanel';
|
||||||
import AboutPanel from './panels/AboutPanel';
|
import AboutPanel from './panels/AboutPanel';
|
||||||
import ExtensionMode from '../common/enums/ExtensionMode.enum';
|
import ExtensionMode from '../common/enums/ExtensionMode.enum';
|
||||||
import Logger from '../ext/lib/Logger';
|
import Logger from '../ext/lib/Logger';
|
||||||
import {ChromeShittinessMitigations as CSM} from '../common/js/ChromeShittinessMitigations';
|
import {ChromeShittinessMitigations as CSM} from '../common/js/ChromeShittinessMitigations';
|
||||||
|
import { browser } from 'webextension-polyfill-ts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
@ -260,7 +261,8 @@ export default {
|
|||||||
await this.settings.init();
|
await this.settings.init();
|
||||||
this.settingsInitialized = true;
|
this.settingsInitialized = true;
|
||||||
|
|
||||||
const port = BrowserDetect.firefox ? browser.runtime.connect({name: 'popup-port'}) : chrome.runtime.connect({name: 'popup-port'});
|
// const port = BrowserDetect.firefox ? browser.runtime.connect({name: 'popup-port'}) : chrome.runtime.connect({name: 'popup-port'});
|
||||||
|
const port = browser.runtime.connect({name: 'popup-port'});
|
||||||
port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
|
port.onMessage.addListener( (m,p) => this.processReceivedMessage(m,p));
|
||||||
CSM.setProperty('port', port);
|
CSM.setProperty('port', port);
|
||||||
|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import Comms from '../../ext/lib/comms/Comms';
|
import { browser } from '../../../node_modules/webextension-polyfill-ts/lib/index';
|
||||||
|
import Settings from '../../ext/lib/Settings';
|
||||||
|
|
||||||
class ExecAction {
|
class ExecAction {
|
||||||
|
settings: Settings;
|
||||||
|
site: any;
|
||||||
|
|
||||||
constructor(settings, site) {
|
constructor(settings, site) {
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.site = site;
|
this.site = site;
|
||||||
@ -24,7 +28,7 @@ class ExecAction {
|
|||||||
arg: cmd.arg,
|
arg: cmd.arg,
|
||||||
customArg: cmd.customArg
|
customArg: cmd.customArg
|
||||||
}
|
}
|
||||||
Comms.sendMessage(message);
|
browser.runtime.sendMessage(message);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// set-ar-persistence sends stuff to content scripts as well (!)
|
// set-ar-persistence sends stuff to content scripts as well (!)
|
||||||
@ -41,7 +45,7 @@ class ExecAction {
|
|||||||
}
|
}
|
||||||
// this hopefully delays settings.save() until current crops are saved on the site
|
// this hopefully delays settings.save() until current crops are saved on the site
|
||||||
// and thus avoid any fucky-wuckies
|
// and thus avoid any fucky-wuckies
|
||||||
await Comms.sendMessage(message);
|
await browser.runtime.sendMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let site = this.site;
|
let site = this.site;
|
@ -2,16 +2,28 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>What's new</h2>
|
<h2>What's new</h2>
|
||||||
<p>Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md">is available here</a>.</p>
|
<p>Full changelog for older versions <a href="https://github.com/tamius-han/ultrawidify/blob/master/CHANGELOG.md">is available here</a>.</p>
|
||||||
<p class="label">4.5.3</p>
|
<p class="label">5.0.0</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
Provided a workaround for the fullscreen stretching bug Chrome 88 (or a recent Windows 10 update) introduced for nVidia users using hardware acceleration on Windows 10. In order to mitigate this bug, Ultrawidify needs to keep a 5-10 px wide black border while watching videos in full screen. This bug is also present in Edge.
|
Under the hood: the extension has been moved over to typescript (except UI bits, which remain javascript).
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>[4.5.3.1]</b> Fixed binding for letterbox misalignment treshold binding in settings.
|
webextension-polyfill is now used everywhere (if only because typescript throws a hissy fit with <code>browser</code> and <code>chrome</code> otherwise) (<a href="https://github.com/tamius-han/ultrawidify/issues/114">#114</a>).
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<b>[4.5.3.2]</b> Removed false positive "this extension can't work due to DRM" notifications.
|
Fix some bugs that I didn't even know I had, but typescript kinda shone some light on them
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Manual zoom and panning are now back. (<a href="https://github.com/tamius-han/ultrawidify/issues/135">#135</a> and <a href="https://github.com/tamius-han/ultrawidify/issues/138">#138</a>)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Fix issue when video would be scaled incorrectly if video element uses <code>height:auto</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>[5.0.0.1]</b> Fixed the issue where settings were reset on page load.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>[5.0.0.1]</b> Fixed the issue where settings page wouldn't load.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
6
src/shims-vue.d.ts
vendored
Normal file
6
src/shims-vue.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
"outDir": "./ts-out",
|
"outDir": "./ts-out",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"target": "es2018",
|
"target": "es2018",
|
||||||
|
Loading…
Reference in New Issue
Block a user