import Debug from '../conf/Debug'; import currentBrowser from '../conf/BrowserDetect'; import ExtensionConf from '../conf/ExtensionConf'; import ExtensionMode from '../../common/enums/extension-mode.enum'; import ObjectCopy from '../lib/ObjectCopy'; import Stretch from '../../common/enums/stretch.enum'; import VideoAlignment from '../../common/enums/video-alignment.enum'; import ExtensionConfPatch from '../conf/ExtConfPatches'; import CropModePersistence from '../../common/enums/crop-mode-persistence.enum'; if(process.env.CHANNEL !== 'stable'){ console.log("Loading Settings"); } class Settings { constructor(options) { // Options: activeSettings, updateCallback, logger this.logger = options.logger; this.onSettingsChanged = options.onSettingsChanged; this.active = options.activeSettings ?? undefined; this.default = ExtensionConf; this.default['version'] = this.getExtensionVersion(); this.useSync = false; this.version = undefined; if (currentBrowser.firefox) { browser.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)}); } else if (currentBrowser.chrome) { chrome.storage.onChanged.addListener((changes, area) => {this.storageChangeListener(changes, area)}); } } storageChangeListener(changes, area) { if (!changes.uwSettings) { return; } this.logger.log('info', 'settings', "[Settings::] Settings have been changed outside of here. Updating active settings. Changes:", changes, "storage area:", area); // if (changes['uwSettings'] && changes['uwSettings'].newValue) { // this.logger.log('info', 'settings',"[Settings::] new settings object:", JSON.parse(changes.uwSettings.newValue)); // } const parsedSettings = JSON.parse(changes.uwSettings.newValue); this.setActive(parsedSettings); this.logger.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged); if (!parsedSettings.preventReload && this.onSettingsChanged) { try { this.onSettingsChanged(); this.logger.log('info', 'settings', '[Settings] Update callback finished.') } catch (e) { this.logger.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e) } } } static getExtensionVersion() { if (currentBrowser.firefox) { return browser.runtime.getManifest().version; } else if (currentBrowser.chrome) { return chrome.runtime.getManifest().version; } } getExtensionVersion() { return Settings.getExtensionVersion(); } compareExtensionVersions(a, b) { let aa = a.split('.'); let bb = b.split('.'); if (+aa[0] !== +bb[0]) { // difference on first digit return +aa[0] - +bb[0]; } if (+aa[1] !== +bb[1]) { // first digit same, difference on second digit return +aa[1] - +bb[1]; } if (+aa[2] !== +bb[2]) { return +aa[2] - +bb[2]; // 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; } if (version.startsWith('a')) { return 1; } if (version.startsWith('b')) { return 2; } return 3; } sortConfPatches(patchesIn) { return patchesIn.sort( (a, b) => this.compareExtensionVersions(a.forVersion, b.forVersion)); } findFirstNecessaryPatch(version, extconfPatches) { const sorted = this.sortConfPatches(extconfPatches); return sorted.findIndex(x => this.compareExtensionVersions(x.forVersion, version) > 0); } 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.'); 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) { const updateFn = patches[index].updateFn; delete patches[index].forVersion; 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++; } } async init() { 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; if (settings) { this.logger.log('info', 'settings', "[Settings::init] Configuration fetched from storage:", settings, "\nlast saved with:", settings.version, "\ncurrent version:", this.version ); } // if (Debug.flushStoredSettings) { // this.logger.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd"); // Debug.flushStoredSettings = false; // don't do it again this session // this.active = this.getDefaultSettings(); // this.active.version = this.version; // this.set(this.active); // return this.active; // } // if there's no settings saved, return default settings. if(! settings || (Object.keys(settings).length === 0 && settings.constructor === Object)) { this.logger.log( 'info', 'settings', '[Settings::init] settings don\'t exist. Using defaults.\n#keys:', settings ? Object.keys(settings).length : 0, '\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; } // 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; } // in case settings in previous version contained a fucky wucky, we overwrite existing settings with a patch this.applySettingsPatches(oldVersion, ExtensionConfPatch); // set 'whatsNewChecked' flag to false when updating, always this.active.whatsNewChecked = false; // update settings version to current this.active.version = this.version; await this.save(); return this.active; } async get() { let ret; if (currentBrowser.firefox) { ret = await browser.storage.local.get('uwSettings'); } else if (currentBrowser.chrome) { ret = await new Promise( (resolve, reject) => { chrome.storage.local.get('uwSettings', (res) => resolve(res)); }); } else if (currentBrowser.edge) { ret = await new Promise( (resolve, reject) => { browser.storage.local.get('uwSettings', (res) => resolve(res)); }); } this.logger.log('info', 'settings', 'Got settings:', ret && ret.uwSettings && JSON.parse(ret.uwSettings)); 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; } } } async set(extensionConf, options) { if (!options || !options.forcePreserveVersion) { extensionConf.version = this.version; } 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)}); } } async setActive(activeSettings) { this.active = activeSettings; } async setProp(prop, value) { this.active[prop] = value; } async save(options) { if (Debug.debug && Debug.storage) { console.log("[Settings::save] Saving active settings:", this.active); } this.active.preventReload = undefined; await this.set(this.active, options); } async saveWithoutReload() { this.active.preventReload = true; await this.set(this.active); } async rollback() { this.active = await this.get(); } getDefaultSettings() { return JSON.parse(JSON.stringify(this.default)); } // ----------------------------------------- // Nastavitve za posamezno stran // Config for a given page: // // : { // status: