ultrawidify/src/ext/lib/Settings.js

585 lines
20 KiB
JavaScript
Raw Normal View History

import Debug from '../conf/Debug';
import currentBrowser from '../conf/BrowserDetect';
import ExtensionConf from '../conf/ExtensionConf';
2019-01-20 23:01:45 +01:00
import ExtensionMode from '../../common/enums/extension-mode.enum';
import ObjectCopy from '../lib/ObjectCopy';
2019-02-13 23:58:19 +01:00
import Stretch from '../../common/enums/stretch.enum';
import VideoAlignment from '../../common/enums/video-alignment.enum';
2019-07-05 23:45:29 +02:00
import ExtensionConfPatch from '../conf/ExtConfPatches';
import CropModePersistence from '../../common/enums/crop-mode-persistence.enum';
2020-04-13 15:20:29 +02:00
if(process.env.CHANNEL !== 'stable'){
console.log("Loading Settings");
}
class Settings {
2019-09-03 00:28:35 +02:00
constructor(options) {
// Options: activeSettings, updateCallback, logger
2019-09-03 23:01:23 +02:00
this.logger = options.logger;
this.onSettingsChanged = options.onSettingsChanged;
this.active = options.activeSettings ?? undefined;
this.default = ExtensionConf;
2019-08-25 01:52:04 +02:00
this.default['version'] = this.getExtensionVersion();
2018-08-21 23:48:47 +02:00
this.useSync = false;
2018-08-22 22:34:07 +02:00
this.version = undefined;
2019-10-24 00:44:27 +02:00
if (currentBrowser.firefox) {
2019-10-27 00:10:49 +02:00
browser.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
} else if (currentBrowser.chrome) {
2019-10-27 00:10:49 +02:00
chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)});
2019-10-24 00:44:27 +02:00
}
}
storageChangeListener(changes, area) {
if (!changes.uwSettings) {
return;
}
2019-10-24 00:44:27 +02:00
this.logger.log('info', 'settings', "[Settings::<storage/on change>] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area);
// if (changes['uwSettings'] && changes['uwSettings'].newValue) {
// this.logger.log('info', 'settings',"[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
// }
const parsedSettings = JSON.parse(changes.uwSettings.newValue);
this.setActive(parsedSettings);
this.logger.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged);
2019-10-24 00:44:27 +02:00
if (!parsedSettings.preventReload && this.onSettingsChanged) {
2019-10-24 00:44:27 +02:00
try {
this.onSettingsChanged();
this.logger.log('info', 'settings', '[Settings] Update callback finished.')
2019-10-24 00:44:27 +02:00
} catch (e) {
this.logger.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e)
2019-10-24 00:44:27 +02:00
}
}
}
static getExtensionVersion() {
2019-08-25 01:52:04 +02:00
if (currentBrowser.firefox) {
return browser.runtime.getManifest().version;
} else if (currentBrowser.chrome) {
return chrome.runtime.getManifest().version;
}
}
getExtensionVersion() {
return Settings.getExtensionVersion();
2019-08-25 01:52:04 +02:00
}
2019-09-03 00:28:35 +02:00
compareExtensionVersions(a, b) {
let aa = a.split('.');
let bb = b.split('.');
2019-09-03 00:28:35 +02:00
if (+aa[0] !== +bb[0]) {
// difference on first digit
return +aa[0] - +bb[0];
2019-09-03 00:28:35 +02:00
} if (+aa[1] !== +bb[1]) {
// first digit same, difference on second digit
return +aa[1] - +bb[1];
2019-09-03 00:28:35 +02:00
} if (+aa[2] !== +bb[2]) {
return +aa[2] - +bb[2];
2019-09-03 00:28:35 +02:00
// first two digits the same, let's check the third digit
} else {
// fourth digit is optional. When not specified, 0 is implied
// btw, ++(aa[3] || 0) - ++(bb[3] || 0) doesn't work
// Since some things are easier if we actually have a value for
// the fourth digit, we turn a possible undefined into a zero
aa[3] = aa[3] === undefined ? 0 : aa[3];
bb[3] = bb[3] === undefined ? 0 : bb[3];
// also, the fourth digit can start with a letter.
// versions that start with a letter are ranked lower than
// versions x.x.x.0
if (isNaN(+aa[3]) ^ isNaN(+bb[3])) {
return isNaN(+aa[3]) ? -1 : 1;
}
// at this point, either both version numbers are a NaN or
// both versions are a number.
if (!isNaN(+aa[3])) {
return +aa[3] - +bb[3];
}
// letters have their own hierarchy:
// dev < a < b < rc
let av = this.getPrereleaseVersionHierarchy(aa[3]);
let bv = this.getPrereleaseVersionHierarchy(bb[3]);
if (av !== bv) {
return av - bv;
} else {
return +(aa[3].replace(/\D/g,'')) - +(bb[3].replace(/\D/g, ''));
}
}
}
getPrereleaseVersionHierarchy(version) {
if (version.startsWith('dev')) {
return 0;
2019-09-03 00:28:35 +02:00
}
if (version.startsWith('a')) {
return 1;
}
if (version.startsWith('b')) {
return 2;
}
return 3;
2019-09-03 00:28:35 +02:00
}
2019-09-03 00:28:35 +02:00
sortConfPatches(patchesIn) {
return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion));
2019-09-03 00:28:35 +02:00
}
findFirstNecessaryPatch(version, extconfPatches) {
const sorted = this.sortConfPatches(extconfPatches);
return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0);
2019-09-03 00:28:35 +02:00
}
applySettingsPatches(oldVersion, patches) {
let index = this.findFirstNecessaryPatch(oldVersion, patches);
if (index === -1) {
this.logger.log('info','settings','[Settings::applySettingsPatches] There are no pending conf patches.');
2019-09-03 00:28:35 +02:00
return;
}
// apply all remaining patches
this.logger.log('info', 'settings', `[Settings::applySettingsPatches] There are ${patches.length - index} settings patches to apply`);
while (index < patches.length) {
2019-11-02 01:05:36 +01:00
const updateFn = patches[index].updateFn;
2019-09-03 00:28:35 +02:00
delete patches[index].forVersion;
2019-11-02 01:05:36 +01:00
delete patches[index].updateFn;
if (Object.keys(patches[index]).length > 0) {
ObjectCopy.overwrite(this.active, patches[index]);
}
if (updateFn) {
try {
updateFn(this.active, this.getDefaultSettings());
} catch (e) {
this.logger.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e);
}
}
index++;
}
2019-09-03 00:28:35 +02:00
}
async init() {
2018-08-21 23:48:47 +02:00
const settings = await this.get();
this.version = this.getExtensionVersion();
// |—> on first setup, settings is undefined & settings.version is haram
// | since new installs ship with updates by default, no patching is
// | needed. In this case, we assume we're on the current version
const oldVersion = (settings && settings.version) || this.version;
2019-10-27 00:10:49 +02:00
if (settings) {
this.logger.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings,
"\nlast saved with:", settings.version,
"\ncurrent version:", this.version
);
}
2019-10-27 00:10:49 +02:00
// if (Debug.flushStoredSettings) {
// this.logger.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd");
// Debug.flushStoredSettings = false; // don't do it again this session
// this.active = this.getDefaultSettings();
// this.active.version = this.version;
// this.set(this.active);
// return this.active;
// }
// if there's no settings saved, return default settings.
2018-08-07 23:31:28 +02:00
if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) {
2019-09-25 07:10:36 +02:00
this.logger.log(
'info',
'settings',
'[Settings::init] settings don\'t exist. Using defaults.\n#keys:',
settings ? Object.keys(settings).length : 0,
2019-09-25 07:10:36 +02:00
'\nsettings:',
settings
);
this.active = this.getDefaultSettings();
this.active.version = this.version;
await this.save();
return this.active;
}
// if there's settings, set saved object as 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
// this should only happen on first extension initialization
if (!this.active.version) {
this.active.version = this.version;
await this.save();
return this.active;
}
2019-08-25 01:52:04 +02:00
// check if extension has been updated. If not, return settings as they were retrieved
if (this.active.version === this.version) {
this.logger.log('info', 'settings', "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is.");
return this.active;
}
// This means extension update happened.
// btw fun fact — we can do version rollbacks, which might come in handy while testing
this.active.version = this.version;
// if extension has been updated, update existing settings with any options added in the
// new version. In addition to that, we remove old keys that are no longer used.
const patched = ObjectCopy.addNew(settings, this.default);
this.logger.log('info', 'settings',"[Settings.init] Results from ObjectCopy.addNew()?", patched, "\n\nSettings from storage", settings, "\ndefault?", this.default);
if (patched) {
this.active = patched;
}
2018-08-22 22:34:07 +02:00
2019-07-05 23:45:29 +02:00
// in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch
2019-09-03 00:28:35 +02:00
this.applySettingsPatches(oldVersion, ExtensionConfPatch);
2019-07-05 23:45:29 +02:00
2019-09-03 00:28:35 +02:00
// set 'whatsNewChecked' flag to false when updating, always
this.active.whatsNewChecked = false;
2019-08-25 01:52:04 +02:00
// update settings version to current
this.active.version = this.version;
await this.save();
return this.active;
}
async get() {
2019-05-10 19:21:17 +02:00
let ret;
if (currentBrowser.firefox) {
2019-05-10 19:21:17 +02:00
ret = await browser.storage.local.get('uwSettings');
} else if (currentBrowser.chrome) {
2019-05-10 19:21:17 +02:00
ret = await new Promise( (resolve, reject) => {
chrome.storage.local.get('uwSettings', (res) => resolve(res));
2018-08-29 00:41:26 +02:00
});
} else if (currentBrowser.edge) {
2019-05-10 19:21:17 +02:00
ret = await new Promise( (resolve, reject) => {
browser.storage.local.get('uwSettings', (res) => resolve(res));
2018-09-25 23:37:08 +02:00
});
2019-05-10 19:21:17 +02:00
}
this.logger.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings));
2019-05-10 19:21:17 +02:00
try {
return JSON.parse(ret.uwSettings);
} catch(e) {
return undefined;
}
}
fixSitesSettings(sites) {
for (const site in sites) {
if (site === '@global') {
continue;
}
if (sites[site].mode === undefined) {
sites[site].mode = ExtensionMode.Default;
}
if (sites[site].autoar === undefined) {
sites[site].autoar = ExtensionMode.Default;
}
if (sites[site].stretch === undefined) {
sites[site].stretch = Stretch.Default;
}
if (sites[site].videoAlignment === undefined) {
sites[site].videoAlignment = VideoAlignment.Default;
}
if (sites[site].keyboardShortcutsEnabled === undefined) {
sites[site].keyboardShortcutsEnabled = ExtensionMode.Default;
}
}
}
2019-11-02 01:05:36 +01:00
async set(extensionConf, options) {
if (!options || !options.forcePreserveVersion) {
extensionConf.version = this.version;
}
2020-01-04 15:46:43 +01:00
this.fixSitesSettings(extensionConf.sites);
this.logger.log('info', 'settings', "[Settings::set] setting new settings:", extensionConf)
if (currentBrowser.firefox || currentBrowser.edge) {
return browser.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
} else if (currentBrowser.chrome) {
return chrome.storage.local.set( {'uwSettings': JSON.stringify(extensionConf)});
}
}
2018-08-07 23:31:28 +02:00
async setActive(activeSettings) {
this.active = activeSettings;
}
async setProp(prop, value) {
this.active[prop] = value;
}
2019-11-02 01:05:36 +01:00
async save(options) {
if (Debug.debug && Debug.storage) {
console.log("[Settings::save] Saving active settings:", this.active);
}
this.active.preventReload = undefined;
2019-11-02 01:05:36 +01:00
await this.set(this.active, options);
2018-08-07 23:31:28 +02:00
}
2019-10-24 00:44:27 +02:00
async saveWithoutReload() {
this.active.preventReload = true;
await this.set(this.active);
2019-10-24 00:44:27 +02:00
}
async rollback() {
this.active = await this.get();
}
getDefaultSettings() {
return JSON.parse(JSON.stringify(this.default));
}
// -----------------------------------------
// Nastavitve za posamezno stran
// Config for a given page:
//
// <hostname> : {
// status: <option> // should extension work on this site?
// arStatus: <option> // should we do autodetection on this site?
// statusEmbedded: <option> // reserved for future... maybe
// }
//
// Veljavne vrednosti za možnosti
// Valid values for options:
//
// status, arStatus, statusEmbedded:
//
// * enabled — always allow
2018-11-02 02:52:01 +01:00
// * basic — only allow fullscreen
// * default — allow if default is to allow, block if default is to block
// * disabled — never allow
getActionsForSite(site) {
if (!site) {
return this.active.actions;
}
if (this.active.sites[site] && this.active.sites[site].actions && this.active.sites[site].actions.length > 0) {
return this.active.sites[site].actions;
}
return this.active.actions;
}
2018-11-02 02:52:01 +01:00
getExtensionMode(site) {
if (!site) {
site = window.location.hostname;
if (!site) {
this.logger.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active);
2018-11-02 02:52:01 +01:00
return ExtensionMode.Disabled;
}
}
try {
// if site-specific settings don't exist for the site, we use default mode:
if (! this.active.sites[site]) {
return this.getExtensionMode('@global');
2018-11-02 02:52:01 +01:00
}
if (this.active.sites[site].mode === ExtensionMode.Enabled) {
2019-01-20 22:59:06 +01:00
return ExtensionMode.Enabled;
} else if (this.active.sites[site].mode === ExtensionMode.Basic) {
return ExtensionMode.Basic;
} else if (this.active.sites[site].mode === ExtensionMode.Disabled) {
2018-11-02 02:52:01 +01:00
return ExtensionMode.Disabled;
} else {
if (site !== '@global') {
return this.getExtensionMode('@global');
} else {
return ExtensionMode.Disabled;
}
2018-11-02 02:52:01 +01:00
}
} catch(e){
this.logger.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\n\nerror:", e, "\n\nSettings object:", this)
2018-11-02 02:52:01 +01:00
return ExtensionMode.Disabled;
}
}
canStartExtension(site) {
// returns 'true' if extension can be started on a given site. Returns false if we shouldn't run.
if (!site) {
site = window.location.hostname;
if (!site) {
this.logger.log('info', 'settings', `[Settings::canStartExtension] window.location.hostname is null or undefined: ${window.location.hostname} \nactive settings:`, this.active);
return false;
}
}
2018-11-02 02:52:01 +01:00
// if (Debug.debug) {
// // let's just temporarily disable debugging while recursively calling
// // this function to get extension status on current site without duplo
// // console logs (and without endless recursion)
// Debug.debug = false;
// const cse = this.canStartExtension(site);
// Debug.debug = true;
// }
2018-08-07 23:31:28 +02:00
try{
// if site is not defined, we use default mode:
if (! this.active.sites[site] || this.active.sites[site].mode === ExtensionMode.Default) {
return this.active.sites['@global'].mode === ExtensionMode.Enabled;
}
if (this.active.sites['@global'].mode === ExtensionMode.Enabled) {
return this.active.sites[site].mode !== ExtensionMode.Disabled;
} else if (this.active.sites['@global'].mode === ExtensionMode.Whitelist) {
return this.active.sites[site].mode === ExtensionMode.Enabled;
} else {
return false;
}
} catch(e) {
this.logger.log('error', 'settings', "[Settings.js::canStartExtension] Something went wrong — are settings defined/has init() been called?\nSettings object:", this);
2018-08-22 22:34:07 +02:00
return false;
}
}
2019-06-02 23:54:32 +02:00
keyboardShortcutsEnabled(site) {
if (!site) {
site = window.location.hostname;
}
if (!site) {
return false;
}
try {
if (!this.active.sites[site]
|| this.active.sites[site].keyboardShortcutsEnabled === undefined
|| this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Default) {
return this.keyboardShortcutsEnabled('@global');
} else {
return this.active.sites[site].keyboardShortcutsEnabled === ExtensionMode.Enabled;
}
} catch (e) {
this.logger.log('info', 'settings',"[Settings.js::keyboardDisabled] something went wrong:", e);
2019-06-02 23:54:32 +02:00
return false;
}
}
2018-09-16 14:14:16 +02:00
extensionEnabled(){
return this.active.sites['@global'] !== ExtensionMode.Disabled
2018-09-16 14:14:16 +02:00
}
extensionEnabledForSite(site) {
return this.canStartExtension(site);
}
canStartAutoAr(site) {
2019-05-26 02:53:29 +02:00
// 'site' argument is only ever used when calling this function recursively for debugging
if (!site) {
site = window.location.host;
if (!site) {
return false;
}
}
if (Debug.debug) {
// let's just temporarily disable debugging while recursively calling
// this function to get extension status on current site without duplo
// console logs (and without endless recursion)
Debug.debug = false;
const csar = this.canStartAutoAr(site);
Debug.debug = true;
this.logger.log('info', 'settings', "[Settings::canStartAutoAr] ----------------\nCAN WE START AUTOAR ON SITE", site,
"?\n\nsettings.active.sites[site]=", this.active.sites[site], "settings.active.sites[@global]=", this.active.sites['@global'],
"\nAutoar mode (global)?", this.active.sites['@global'].autoar,
`\nAutoar mode (${site})`, this.active.sites[site] ? this.active.sites[site].autoar : '<not defined>',
"\nCan autoar be started?", csar
);
}
// if site is not defined, we use default mode:
if (! this.active.sites[site]) {
return this.active.sites['@global'].autoar === ExtensionMode.Enabled;
}
if (this.active.sites['@global'].autoar === ExtensionMode.Enabled) {
return this.active.sites[site].autoar !== ExtensionMode.Disabled;
} else if (this.active.sites['@global'].autoar === ExtensionMode.Whitelist) {
this.logger.log('info', 'settings', "canStartAutoAr — can(not) start csar because extension is in whitelist mode, and this site is (not) equal to", ExtensionMode.Enabled)
return this.active.sites[site].autoar === ExtensionMode.Enabled;
} else {
this.logger.log('info', 'settings', "canStartAutoAr — cannot start csar because extension is globally disabled")
return false;
}
}
2019-02-13 23:58:19 +01:00
getDefaultOption(option) {
const allDefault = {
mode: ExtensionMode.Default,
autoar: ExtensionMode.Default,
autoarFallback: ExtensionMode.Default,
stretch: Stretch.Default,
videoAlignment: VideoAlignment.Default,
};
if (!option || allDefault[option] === undefined) {
return allDefault;
}
return allDefault[option];
}
getDefaultAr(site) {
// site = this.getSiteSettings(site);
// if (site.defaultAr) {
// return site.defaultAr;
// }
return this.active.miscSettings.defaultAr;
}
getDefaultStretchMode(site) {
2020-01-28 23:34:36 +01:00
if (site && this.active.sites[site]?.stretch !== Stretch.Default) {
return this.active.sites[site].stretch;
}
2019-01-20 23:05:04 +01:00
return this.active.sites['@global'].stretch;
}
getDefaultCropPersistenceMode(site) {
2020-01-28 23:34:36 +01:00
if (site && this.active.sites[site]?.cropModePersistence !== Stretch.Default) {
return this.active.sites[site].cropModePersistence;
}
// persistence mode thing is missing from settings by default
return this.active.sites['@global'].cropModePersistence || CropModePersistence.Disabled;
}
getDefaultVideoAlignment(site) {
2020-01-28 23:34:36 +01:00
if (this.active.sites[site]?.videoAlignment !== VideoAlignment.Default) {
return this.active.sites[site].videoAlignment;
}
2019-01-20 23:05:04 +01:00
return this.active.sites['@global'].videoAlignment;
}
}
export default Settings;