2018-12-30 23:16:09 +01:00
import Debug from '../conf/Debug' ;
import currentBrowser from '../conf/BrowserDetect' ;
import ExtensionConf from '../conf/ExtensionConf' ;
2021-02-08 23:04:54 +01:00
import ExtensionMode from '../../common/enums/ExtensionMode.enum' ;
2021-02-09 00:37:54 +01:00
import ObjectCopy from './ObjectCopy' ;
2021-02-08 23:04:54 +01:00
import StretchType from '../../common/enums/StretchType.enum' ;
import VideoAlignmentType from '../../common/enums/VideoAlignmentType.enum' ;
2019-07-05 23:45:29 +02:00
import ExtensionConfPatch from '../conf/ExtConfPatches' ;
2021-02-08 23:04:54 +01:00
import CropModePersistence from '../../common/enums/CropModePersistence.enum' ;
2020-12-23 02:01:52 +01:00
import BrowserDetect from '../conf/BrowserDetect' ;
2021-02-09 00:37:54 +01:00
import Logger from './Logger' ;
import SettingsInterface from '../../common/interfaces/SettingsInterface' ;
2021-11-23 01:32:42 +01:00
import AspectRatioType from '../../common/enums/AspectRatioType.enum' ;
2023-01-07 18:57:47 +01:00
import { SiteSettings } from './settings/SiteSettings' ;
2018-12-30 23:16:09 +01:00
2020-04-13 15:20:29 +02:00
if ( process . env . CHANNEL !== 'stable' ) {
2020-12-03 01:05:39 +01:00
console . info ( "Loading Settings" ) ;
2020-04-13 15:20:29 +02:00
}
2019-06-03 00:37:19 +02:00
2021-02-09 00:37:54 +01:00
2018-08-05 23:48:56 +02:00
class Settings {
2021-02-09 00:37:54 +01:00
//#region flags
useSync : boolean = false ;
version : string ;
//#endregion
//#region helper classes
logger : Logger ;
//#endregion
//#region data
default : SettingsInterface ; // default settings
active : SettingsInterface ; // currently active settings
//#endregion
//#region callbacks
onSettingsChanged : any ;
afterSettingsSaved : any ;
2024-12-26 14:58:14 +01:00
onChangedCallbacks : any [ ] = [ ] ;
afterSettingsChangedCallbacks : any [ ] = [ ] ;
2021-02-09 00:37:54 +01:00
//#endregion
2018-08-05 23:48:56 +02:00
2019-09-03 00:28:35 +02:00
constructor ( options ) {
// Options: activeSettings, updateCallback, logger
2020-12-23 02:01:52 +01:00
this . logger = options ? . logger ;
this . onSettingsChanged = options ? . onSettingsChanged ;
this . afterSettingsSaved = options ? . afterSettingsSaved ;
this . active = options ? . activeSettings ? ? undefined ;
2018-08-05 23:48:56 +02:00
this . default = ExtensionConf ;
2019-08-25 01:52:04 +02:00
this . default [ 'version' ] = this . getExtensionVersion ( ) ;
2018-08-23 01:04:37 +02:00
2024-06-03 00:15:23 +02:00
chrome . storage . onChanged . addListener ( ( changes , area ) = > { this . storageChangeListener ( changes , area ) } ) ;
2019-10-24 00:44:27 +02:00
}
2023-01-07 03:06:37 +01:00
private storageChangeListener ( changes , area ) {
2020-01-29 01:36:09 +01:00
if ( ! changes . uwSettings ) {
return ;
}
2020-12-23 02:01:52 +01: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 ) ;
2020-02-09 01:16:46 +01:00
// if (changes['uwSettings'] && changes['uwSettings'].newValue) {
2020-12-23 02:01:52 +01:00
// this.logger?.log('info', 'settings',"[Settings::<storage/on change>] new settings object:", JSON.parse(changes.uwSettings.newValue));
2020-02-09 01:16:46 +01:00
// }
2019-10-24 21:13:45 +02:00
const parsedSettings = JSON . parse ( changes . uwSettings . newValue ) ;
2020-02-09 01:16:46 +01:00
this . setActive ( parsedSettings ) ;
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'debug' , 'Does parsedSettings.preventReload exist?' , parsedSettings . preventReload , "Does callback exist?" , ! ! this . onSettingsChanged ) ;
2019-10-24 00:44:27 +02:00
2024-12-26 14:58:14 +01:00
if ( ! parsedSettings . preventReload ) {
2019-10-24 00:44:27 +02:00
try {
2024-12-26 14:58:14 +01:00
for ( const fn of this . onChangedCallbacks ) {
try {
fn ( ) ;
} catch ( e ) {
this . logger ? . log ( 'warn' , 'settings' , "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error." , e )
}
}
if ( this . onSettingsChanged ) {
this . onSettingsChanged ( ) ;
}
2021-04-01 21:44:07 +02:00
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'settings' , '[Settings] Update callback finished.' )
2019-10-24 00:44:27 +02:00
} catch ( e ) {
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'error' , 'settings' , "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:" , e )
2019-10-24 00:44:27 +02:00
}
2018-08-23 01:04:37 +02:00
}
2024-12-26 14:58:14 +01:00
for ( const fn of this . afterSettingsChangedCallbacks ) {
try {
fn ( ) ;
} catch ( e ) {
this . logger ? . log ( 'warn' , 'settings' , "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error." , e )
}
}
2020-12-21 23:27:45 +01:00
if ( this . afterSettingsSaved ) {
this . afterSettingsSaved ( ) ;
}
2018-08-05 23:48:56 +02:00
}
2021-02-09 00:37:54 +01:00
static getExtensionVersion ( ) : string {
2024-06-03 00:15:23 +02:00
return chrome . runtime . getManifest ( ) . version ;
2020-03-02 22:35:34 +01:00
}
2021-02-09 00:37:54 +01:00
getExtensionVersion ( ) : string {
2021-11-23 01:32:42 +01:00
return Settings . getExtensionVersion ( ) ;
2019-08-25 01:52:04 +02:00
}
2023-01-07 03:06:37 +01:00
private compareExtensionVersions ( a , b ) {
2019-09-17 22:14:42 +02:00
let aa = a . split ( '.' ) ;
let bb = b . split ( '.' ) ;
2021-11-23 01:32:42 +01:00
2019-09-03 00:28:35 +02:00
if ( + aa [ 0 ] !== + bb [ 0 ] ) {
// difference on first digit
2019-09-17 22:14:42 +02:00
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
2019-09-17 22:14:42 +02:00
return + aa [ 1 ] - + bb [ 1 ] ;
2019-09-03 00:28:35 +02:00
} if ( + aa [ 2 ] !== + bb [ 2 ] ) {
2019-09-17 22:14:42 +02:00
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
2019-09-17 22:14:42 +02:00
// 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 ] ;
2021-11-23 01:32:42 +01:00
// also, the fourth digit can start with a letter.
// versions that start with a letter are ranked lower than
2019-09-17 22:14:42 +02:00
// versions x.x.x.0
2021-02-09 00:37:54 +01:00
if (
( isNaN ( + aa [ 3 ] ) && ! isNaN ( + bb [ 3 ] ) )
|| ( ! isNaN ( + aa [ 3 ] ) && isNaN ( + bb [ 3 ] ) )
) {
2019-09-17 22:14:42 +02:00
return isNaN ( + aa [ 3 ] ) ? - 1 : 1 ;
}
2021-11-23 01:32:42 +01:00
// at this point, either both version numbers are a NaN or
2019-09-17 22:14:42 +02:00
// 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 , '' ) ) ;
}
}
}
2023-01-07 03:06:37 +01:00
private getPrereleaseVersionHierarchy ( version ) {
2019-09-17 22:14:42 +02:00
if ( version . startsWith ( 'dev' ) ) {
return 0 ;
2019-09-03 00:28:35 +02:00
}
2019-09-17 22:14:42 +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-17 22:14:42 +02:00
2023-01-07 03:06:37 +01:00
private sortConfPatches ( patchesIn ) {
2019-09-17 22:14:42 +02:00
return patchesIn . sort ( ( a , b ) = > this . compareExtensionVersions ( a . forVersion , b . forVersion ) ) ;
2019-09-03 00:28:35 +02:00
}
2023-01-07 03:06:37 +01:00
private findFirstNecessaryPatch ( version , extconfPatches ) {
2019-09-03 00:28:35 +02:00
const sorted = this . sortConfPatches ( extconfPatches ) ;
2019-09-17 22:14:42 +02:00
return sorted . findIndex ( x = > this . compareExtensionVersions ( x . forVersion , version ) > 0 ) ;
2019-09-03 00:28:35 +02:00
}
2023-01-07 03:06:37 +01:00
private applySettingsPatches ( oldVersion , patches ) {
2019-09-03 00:28:35 +02:00
let index = this . findFirstNecessaryPatch ( oldVersion , patches ) ;
if ( index === - 1 ) {
2020-12-23 02:01:52 +01:00
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
2025-01-01 22:15:22 +01:00
try {
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 ) {
2019-11-02 01:05:36 +01:00
2025-01-01 22:15:22 +01:00
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 ) ;
2024-12-26 14:58:14 +01:00
2025-01-01 22:15:22 +01:00
}
2019-11-02 01:05:36 +01:00
}
2025-01-01 22:15:22 +01:00
index ++ ;
}
} catch ( e ) {
2025-01-05 00:38:11 +01:00
console . error ( 'Failed to upgrade settings.' , e ) ;
2025-01-01 22:15:22 +01:00
this . setActive ( this . getDefaultSettings ( ) ) ;
this . save ( ) ;
2019-09-17 22:14:42 +02:00
}
2019-09-03 00:28:35 +02:00
}
2018-08-05 23:48:56 +02:00
async init() {
2018-08-21 23:48:47 +02:00
const settings = await this . get ( ) ;
2019-09-17 22:14:42 +02:00
this . version = this . getExtensionVersion ( ) ;
2019-08-28 18:28:22 +02:00
// |—> on first setup, settings is undefined & settings.version is haram
2019-08-31 18:22:13 +02:00
// | since new installs ship with updates by default, no patching is
// | needed. In this case, we assume we're on the current version
2019-09-17 22:14:42 +02:00
const oldVersion = ( settings && settings . version ) || this . version ;
2018-08-05 23:48:56 +02:00
2019-10-27 00:10:49 +02:00
if ( settings ) {
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'settings' , "[Settings::init] Configuration fetched from storage:" , settings ,
2019-09-03 22:17:10 +02:00
"\nlast saved with:" , settings . version ,
2019-09-17 22:14:42 +02:00
"\ncurrent version:" , this . version
2019-09-03 22:17:10 +02:00
) ;
2018-08-05 23:48:56 +02:00
}
2019-10-27 00:10:49 +02:00
// if (Debug.flushStoredSettings) {
2020-12-23 02:01:52 +01:00
// this.logger?.log('info', 'settings', "%c[Settings::init] Debug.flushStoredSettings is true. Using default settings", "background: #d00; color: #ffd");
2019-10-27 00:10:49 +02:00
// 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;
// }
2018-08-05 23:48:56 +02:00
// 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 ) ) {
2020-12-23 02:01:52 +01:00
this . logger ? . log (
2021-11-23 01:32:42 +01:00
'info' ,
'settings' ,
'[Settings::init] settings don\'t exist. Using defaults.\n#keys:' ,
settings ? Object . keys ( settings ) . length : 0 ,
'\nsettings:' ,
2019-09-25 07:10:36 +02:00
settings
) ;
2018-08-05 23:48:56 +02:00
this . active = this . getDefaultSettings ( ) ;
2019-09-17 22:14:42 +02:00
this . active . version = this . version ;
await this . save ( ) ;
2018-08-05 23:48:56 +02:00
return this . active ;
}
2019-09-17 22:14:42 +02:00
// if there's settings, set saved object as active settings
this . active = settings ;
// 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
2019-09-17 22:14:42 +02:00
// check if extension has been updated. If not, return settings as they were retrieved
if ( this . active . version === this . version ) {
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'settings' , "[Settings::init] extension was saved with current version of ultrawidify. Returning object as-is." ) ;
2018-08-05 23:48:56 +02:00
return this . active ;
}
2019-09-17 22:14:42 +02:00
// 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 ;
2018-08-05 23:48:56 +02:00
// 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.
2019-06-05 23:36:47 +02:00
const patched = ObjectCopy . addNew ( settings , this . default ) ;
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'settings' , "[Settings.init] Results from ObjectCopy.addNew()?" , patched , "\n\nSettings from storage" , settings , "\ndefault?" , this . default ) ;
2019-06-05 23:36:47 +02:00
2019-09-17 22:14:42 +02:00
if ( patched ) {
2019-06-05 23:36:47 +02:00
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
2019-07-07 15:12:15 +02:00
this . active . whatsNewChecked = false ;
2019-08-25 01:52:04 +02:00
// update settings version to current
2021-11-23 01:32:42 +01:00
this . active . version = this . version ;
2019-07-07 15:12:15 +02:00
2019-09-17 22:14:42 +02:00
await this . save ( ) ;
2018-08-05 23:48:56 +02:00
return this . active ;
}
async get ( ) {
2019-05-10 19:21:17 +02:00
let ret ;
2021-11-23 01:32:42 +01:00
2024-06-03 00:15:23 +02:00
ret = await chrome . storage . local . get ( 'uwSettings' ) ;
2019-05-10 19:21:17 +02:00
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'settings' , 'Got settings:' , ret && ret . uwSettings && JSON . parse ( ret . uwSettings ) ) ;
2019-06-08 03:45:35 +02:00
2019-05-10 19:21:17 +02:00
try {
return JSON . parse ( ret . uwSettings ) ;
} catch ( e ) {
return undefined ;
2018-08-05 23:48:56 +02:00
}
}
2021-02-09 00:37:54 +01:00
async set ( extensionConf , options ? ) {
2019-11-02 01:05:36 +01:00
if ( ! options || ! options . forcePreserveVersion ) {
extensionConf . version = this . version ;
}
2019-09-17 22:14:42 +02:00
2020-12-23 02:01:52 +01:00
this . logger ? . log ( 'info' , 'settings' , "[Settings::set] setting new settings:" , extensionConf )
2018-08-23 01:04:37 +02:00
2024-06-03 00:15:23 +02:00
return chrome . storage . local . set ( { 'uwSettings' : JSON . stringify ( extensionConf ) } ) ;
2018-08-05 23:48:56 +02:00
}
2018-08-07 23:31:28 +02:00
async setActive ( activeSettings ) {
this . active = activeSettings ;
}
2025-01-25 21:17:20 +01:00
/ * *
* Sets value of a prop at given path .
* @param propPath path to property we want to set . If prop path does not exist ,
* this function will recursively create it . It is assumed that uninitialized properties
* are objects .
* @param value
* /
async setProp ( propPath : string | string [ ] , value : any , options ? : { forceReload? : boolean } , currentPath? : any ) {
if ( ! Array . isArray ( propPath ) ) {
propPath = propPath . split ( '.' ) ;
}
if ( ! currentPath ) {
currentPath = this . active ;
}
const currentProp = propPath . shift ( ) ;
if ( propPath . length ) {
if ( ! currentPath [ currentProp ] ) {
currentPath [ currentProp ] = { } ;
}
2025-01-22 11:26:25 +01:00
return this . setProp ( propPath , value , options , currentPath [ currentProp ] ) ;
2025-01-25 21:17:20 +01:00
} else {
currentPath [ currentProp ] = value ;
if ( options ? . forceReload ) {
2025-01-22 11:26:25 +01:00
return this . save ( ) ;
2025-01-25 21:17:20 +01:00
} else {
2025-01-22 11:26:25 +01:00
return this . saveWithoutReload ( ) ;
2025-01-25 21:17:20 +01:00
}
}
2018-08-07 23:31:28 +02:00
}
2021-02-09 00:37:54 +01:00
async save ( options ? ) {
2019-06-08 03:45:35 +02:00
if ( Debug . debug && Debug . storage ) {
2018-08-23 01:04:37 +02:00
console . log ( "[Settings::save] Saving active settings:" , this . active ) ;
}
2019-10-24 21:13:45 +02:00
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() {
2019-10-24 21:13:45 +02:00
this . active . preventReload = true ;
await this . set ( this . active ) ;
2019-10-24 00:44:27 +02:00
}
2019-04-12 00:49:56 +02:00
async rollback() {
this . active = await this . get ( ) ;
}
2018-08-05 23:48:56 +02:00
getDefaultSettings() {
return JSON . parse ( JSON . stringify ( this . default ) ) ;
}
2021-11-23 01:32:42 +01:00
/ * *
* Gets default site configuration . Only returns essential settings .
* @returns
* /
getDefaultSiteConfiguration() {
return {
type : 'user-added' ,
defaultCrop : {
type : AspectRatioType . Automatic , // AARD is enabled by default
} ,
defaultStretch : {
type : StretchType . NoStretch , // because we aren't uncultured savages
} ,
}
}
2023-01-07 18:57:47 +01:00
getSiteSettings ( site : string = window . location . hostname ) : SiteSettings {
return new SiteSettings ( this , site ) ;
2022-06-14 00:26:06 +02:00
}
2024-12-26 14:58:14 +01:00
listenOnChange ( fn : ( ) = > void ) : void {
this . onChangedCallbacks . push ( fn ) ;
}
listenAfterChange ( fn : ( ) = > void ) : void {
this . afterSettingsChangedCallbacks . push ( fn ) ;
}
2018-08-05 23:48:56 +02:00
}
2018-12-30 23:16:09 +01:00
2018-12-31 01:03:07 +01:00
export default Settings ;
2020-12-03 01:05:39 +01:00
if ( process . env . CHANNEL !== 'stable' ) {
console . info ( "Settings loaded" ) ;
}