2024-10-05 23:17:20 +02:00
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
2025-06-25 00:38:01 +02:00
import deepmerge from '@mui/utils/deepmerge' ;
2026-03-11 00:11:29 +01:00
import type { AppMetadataKeys , IMetadataMigration } from '@/features/metadata/Metadata.types.ts' ;
2026-05-08 01:39:48 +02:00
import type { IReaderSettings , ReaderCustomFilter , SafeAreaInset } from '@/features/reader/Reader.types.ts' ;
2026-03-11 00:11:29 +01:00
import { ProgressBarPosition , ReaderPageScaleMode , ReadingMode } from '@/features/reader/Reader.types.ts' ;
2025-06-24 22:22:53 +02:00
import {
AUTO_SCROLL_SPEED ,
CUSTOM_FILTER ,
DEFAULT_READER_SETTINGS ,
IMAGE_PRE_LOAD_AMOUNT ,
PAGE_GAP ,
PROGRESS_BAR_SIZE ,
SCROLL_AMOUNT ,
2025-08-16 02:02:13 +02:00
} from '@/features/reader/settings/ReaderSettings.constants.tsx' ;
2025-06-25 00:38:01 +02:00
import { coerceIn , jsonSaveParse } from '@/lib/HelperFunctions.ts' ;
2025-08-15 22:02:58 +02:00
import { DOWNLOAD_AHEAD } from '@/features/downloads/Downloads.constants.ts' ;
import { MANGA_GRID_WIDTH } from '@/features/settings/Settings.constants.ts' ;
2026-03-11 00:11:29 +01:00
import type { SortSettings } from '@/features/migration/Migration.types.ts' ;
import type { ISourceMetadata } from '@/features/source/Source.types.ts' ;
import type { LibraryOptions } from '@/features/library/Library.types.ts' ;
import type { MetadataThemeSettings } from '@/features/theme/AppTheme.types.ts' ;
import type { TapZoneInvertMode } from '@/features/reader/tap-zones/TapZoneLayout.types.ts' ;
2026-01-22 00:12:22 +01:00
import { detectLocale , getISOLanguage } from '@/lib/ISOLanguageUtil.ts' ;
2026-03-11 00:11:29 +01:00
import type { I18nResourceCode } from '@/i18n' ;
import { i18nResources } from '@/i18n' ;
2026-01-25 17:31:02 +01:00
import { toUniqueLanguageCodes } from '@/base/utils/Languages.ts' ;
2026-05-09 13:00:36 +02:00
import { assertIsDefined } from '@/base/Asserts.ts' ;
const MATCH_ARRAY_NUMBERS = /^\[\d+(?:,\d+)*]$/g ;
2024-10-05 23:17:20 +02:00
2024-10-28 18:03:29 +01:00
export const APP_METADATA_KEY_PREFIX = 'webUI' ;
2024-10-05 23:17:20 +02:00
2025-06-25 00:38:01 +02:00
const convertToTypeNullAndUndefined = < T >( value : string , convertToType : ( value : string ) => T ) : NullAndUndefined < T > => {
2026-03-11 00:11:29 +01:00
if ( value === 'null' ) {
return null ;
}
if ( value === 'undefined' ) {
return undefined ;
}
2025-06-25 00:38:01 +02:00
return convertToType ( value );
};
const convertToString = ( value : string ) : string => value ;
2026-05-09 13:12:39 +02:00
const convertToNumber = ( value : string , defaultValue : number ) : number => {
if ( Number . isNaN ( + value )) {
return defaultValue ;
}
return + value ;
};
2025-06-25 00:38:01 +02:00
const convertToBoolean = ( value : string ) : boolean => value === 'true' ;
const convertToStringNullAndUndefined = ( value : string ) => convertToTypeNullAndUndefined ( value , convertToString );
const convertToBooleanNullAndUndefined = ( value : string ) => convertToTypeNullAndUndefined ( value , convertToBoolean );
2025-06-25 00:58:35 +02:00
const convertToObject = < T >( value : string , defaultValue : T ) : T => {
2025-06-29 02:32:16 +02:00
const convertedValue = jsonSaveParse < T >( value ) ?? defaultValue ;
2025-06-25 00:58:35 +02:00
2025-06-29 02:32:16 +02:00
if ( Array . isArray ( convertedValue )) {
return convertedValue ;
}
if ( defaultValue == null ) {
2025-06-25 00:58:35 +02:00
return convertedValue ;
}
return deepmerge ( defaultValue , convertedValue );
};
2025-06-25 00:38:01 +02:00
2025-06-24 22:22:53 +02:00
export const APP_METADATA : Record <
AppMetadataKeys ,
{
2025-06-25 00:38:01 +02:00
convert : ( value : string , defaultValue : any ) => any ;
toConstrainedValue ?: ( value : any ) => any ;
2025-06-24 22:22:53 +02:00
}
> = {
migration : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
2025-06-24 22:22:53 +02:00
},
deleteChaptersManuallyMarkedRead : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
deleteChaptersWhileReading : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
2025-06-24 22:22:53 +02:00
},
deleteChaptersWithBookmark : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
downloadAheadLimit : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
2025-06-28 03:09:44 +02:00
toConstrainedValue : ( value : number ) => {
const isDisabled = value === 0 ;
if ( isDisabled ) {
return value ;
}
return coerceIn ( value , DOWNLOAD_AHEAD . min , DOWNLOAD_AHEAD . max );
},
2025-06-24 22:22:53 +02:00
},
showAddToLibraryCategorySelectDialog : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
ignoreFilters : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
removeMangaFromCategories : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
showTabSize : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
devices : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < string [] >,
2025-06-24 22:22:53 +02:00
},
migrateChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
migrateCategories : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
migrateTracking : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
deleteChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
2026-03-07 03:07:40 +01:00
migrateMetadata : {
convert : convertToBoolean ,
},
2025-06-24 22:22:53 +02:00
migrateSortSettings : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < SortSettings >,
2025-06-24 22:22:53 +02:00
},
hideLibraryEntries : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
updateProgressAfterReading : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
updateProgressManualMarkRead : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
webUIInformAvailableUpdate : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
2026-01-25 17:53:32 +01:00
webUIInformVersionUpdated : {
convert : convertToBoolean ,
},
2025-06-24 22:22:53 +02:00
serverInformAvailableUpdate : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
2026-01-25 17:57:30 +01:00
serverInformVersionUpdated : {
convert : convertToBoolean ,
},
2025-06-24 22:22:53 +02:00
readerWidth : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < IReaderSettings [ 'readerWidth' ] >,
2025-06-24 22:22:53 +02:00
},
savedSearches : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < ISourceMetadata [ 'savedSearches' ] >,
2025-06-24 22:22:53 +02:00
},
showContinueReadingButton : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
showDownloadBadge : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
showUnreadBadge : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
gridLayout : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // GridLayout (enum)
2025-06-24 22:22:53 +02:00
},
sortBy : {
2025-06-25 00:38:01 +02:00
convert : convertToStringNullAndUndefined , // LibrarySortMode
2025-06-24 22:22:53 +02:00
},
sortDesc : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
hasDownloadedChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
hasBookmarkedChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
hasUnreadChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
hasReadChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
hasDuplicateChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
hasTrackerBinding : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < LibraryOptions [ 'hasTrackerBinding' ] >,
2025-06-24 22:22:53 +02:00
},
hasStatus : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < LibraryOptions [ 'hasStatus' ] >,
2025-06-24 22:22:53 +02:00
},
2026-07-20 15:09:06 -07:00
hasSource : {
convert : convertToObject < LibraryOptions [ 'hasSource' ] >,
},
2025-06-24 22:22:53 +02:00
customThemes : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < MetadataThemeSettings [ 'customThemes' ] >,
2025-06-24 22:22:53 +02:00
},
mangaThumbnailBackdrop : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
mangaDynamicColorSchemes : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
tapZoneLayout : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // TapZoneLayouts (enum)
2025-06-24 22:22:53 +02:00
},
tapZoneInvertMode : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < TapZoneInvertMode >, // TapZoneInvertMode (object)
2025-06-24 22:22:53 +02:00
},
readingDirection : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReadingDirection (enum)
2025-06-24 22:22:53 +02:00
},
progressBarType : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ProgressBarType (enum)
2025-06-24 22:22:53 +02:00
},
progressBarSize : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
toConstrainedValue : ( value : number ) => coerceIn ( value , PROGRESS_BAR_SIZE . min , PROGRESS_BAR_SIZE . max ),
2025-06-24 22:22:53 +02:00
},
progressBarPosition : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ProgressBarPosition (enum)
2025-06-24 22:22:53 +02:00
},
progressBarPositionAutoVertical : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // TProgressBarPositionAutoVertical (enum)
2025-06-24 22:22:53 +02:00
},
readingMode : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReadingMode (enum)
2025-06-24 22:22:53 +02:00
},
pageScaleMode : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReaderPageScaleMode (enum)
2025-06-24 22:22:53 +02:00
},
shouldOffsetDoubleSpreads : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
exitMode : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReaderExitMode (enum)
2025-06-24 22:22:53 +02:00
},
customFilter : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < ReaderCustomFilter >,
toConstrainedValue : ( value : ReaderCustomFilter ) : ReaderCustomFilter => ({
2025-06-24 22:22:53 +02:00
... value ,
brightness : {
... value . brightness ,
value : coerceIn ( value . brightness . value , CUSTOM_FILTER . brightness . min , CUSTOM_FILTER . brightness . max ),
},
contrast : {
... value . contrast ,
value : coerceIn ( value . contrast . value , CUSTOM_FILTER . contrast . min , CUSTOM_FILTER . contrast . max ),
},
saturate : {
... value . saturate ,
value : coerceIn ( value . saturate . value , CUSTOM_FILTER . saturate . min , CUSTOM_FILTER . saturate . max ),
},
hue : {
... value . hue ,
value : coerceIn ( value . hue . value , CUSTOM_FILTER . hue . min , CUSTOM_FILTER . hue . max ),
},
rgba : {
... value . rgba ,
value : {
... value . rgba . value ,
red : coerceIn ( value . rgba . value . red , CUSTOM_FILTER . rgba . red . min , CUSTOM_FILTER . rgba . red . max ),
green : coerceIn ( value . rgba . value . green , CUSTOM_FILTER . rgba . green . min , CUSTOM_FILTER . rgba . green . max ),
blue : coerceIn ( value . rgba . value . blue , CUSTOM_FILTER . rgba . blue . min , CUSTOM_FILTER . rgba . blue . max ),
alpha : coerceIn ( value . rgba . value . alpha , CUSTOM_FILTER . rgba . alpha . min , CUSTOM_FILTER . rgba . alpha . max ),
},
},
}),
},
shouldSkipDupChapters : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
2025-07-01 00:37:29 +02:00
shouldSkipFilteredChapters : {
convert : convertToBoolean ,
},
2025-06-24 22:22:53 +02:00
isStaticNav : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
overlayMode : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReaderOverlayMode (enum)
2025-06-24 22:22:53 +02:00
},
shouldStretchPage : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
shouldShowPageNumber : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
backgroundColor : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReaderBackgroundColor (enum)
2025-06-24 22:22:53 +02:00
},
2026-07-15 22:34:51 +02:00
useAutoBackgroundColorContinuousMode : {
convert : convertToBoolean ,
},
2025-06-24 22:22:53 +02:00
pageGap : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
toConstrainedValue : ( value : number ) => coerceIn ( value , PAGE_GAP . min , PAGE_GAP . max ),
2025-06-24 22:22:53 +02:00
},
hotkeys : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < IReaderSettings [ 'hotkeys' ] >,
2025-06-24 22:22:53 +02:00
},
imagePreLoadAmount : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
toConstrainedValue : ( value : number ) => coerceIn ( value , IMAGE_PRE_LOAD_AMOUNT . min , IMAGE_PRE_LOAD_AMOUNT . max ),
2025-06-24 22:22:53 +02:00
},
shouldUseAutoWebtoonMode : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
autoScroll : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < IReaderSettings [ 'autoScroll' ] >,
toConstrainedValue : ( value : IReaderSettings [ 'autoScroll' ]) : IReaderSettings [ 'autoScroll' ] => ({
2025-06-24 22:22:53 +02:00
... value ,
value : coerceIn ( value . value , AUTO_SCROLL_SPEED . min , AUTO_SCROLL_SPEED . max ),
}),
},
shouldShowReadingModePreview : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
shouldShowTapZoneLayoutPreview : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
shouldInformAboutMissingChapter : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
shouldInformAboutScanlatorChange : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
2026-07-20 17:35:36 +02:00
shouldWakeLockScreen : {
convert : convertToBoolean ,
},
2025-06-24 22:22:53 +02:00
hideHistory : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
scrollAmount : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber , // ReaderScrollAmount (enum)
toConstrainedValue : ( value : number ) => coerceIn ( value , SCROLL_AMOUNT . min , SCROLL_AMOUNT . max ),
2025-06-24 22:22:53 +02:00
},
reverse : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
bookmarked : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
downloaded : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
unread : {
2025-06-25 00:38:01 +02:00
convert : convertToBooleanNullAndUndefined ,
2025-06-24 22:22:53 +02:00
},
showChapterNumber : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
2026-01-25 17:31:02 +01:00
browseLanguages : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < string [] >,
2025-06-24 22:22:53 +02:00
},
showNsfw : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
shouldUseInfiniteScroll : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
shouldShowTransitionPage : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
appTheme : {
2025-06-25 00:38:01 +02:00
convert : convertToString ,
2025-06-24 22:22:53 +02:00
},
themeMode : {
2025-06-25 00:38:01 +02:00
convert : convertToString , // ThemeMode (enum)
2025-06-24 22:22:53 +02:00
},
shouldUsePureBlackMode : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
mangaGridItemWidth : {
2025-06-25 00:38:01 +02:00
convert : convertToNumber ,
toConstrainedValue : ( value : number ) => coerceIn ( value , MANGA_GRID_WIDTH . min , MANGA_GRID_WIDTH . max ),
2025-06-24 22:22:53 +02:00
},
isPinned : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
isEnabled : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
lastUsedSourceId : {
2025-06-25 00:38:01 +02:00
convert : convertToString ,
2025-06-24 22:22:53 +02:00
},
shouldShowOnlySourcesWithResults : {
2025-06-25 00:38:01 +02:00
convert : convertToBoolean ,
2025-06-24 22:22:53 +02:00
},
excludedScanlators : {
2025-06-25 00:38:01 +02:00
convert : convertToObject < string [] >,
2025-06-24 22:22:53 +02:00
},
2026-01-22 00:12:22 +01:00
locale : {
convert : convertToString ,
toConstrainedValue : ( value : string ) => {
const locale = getISOLanguage ( value ) ? . isoCode ;
2026-01-30 21:46:20 +01:00
if ( ! locale || ! i18nResources . includes ( locale as I18nResourceCode )) {
2026-01-22 00:12:22 +01:00
return detectLocale ();
}
return locale ;
},
},
2026-05-08 01:39:48 +02:00
safeAreaInset : {
convert : convertToObject < SafeAreaInset >,
},
2025-06-24 22:22:53 +02:00
} as const ;
2024-10-26 17:25:04 +02:00
2025-06-24 22:22:53 +02:00
export const VALID_APP_METADATA_KEYS = Object . keys ( APP_METADATA );
2024-10-26 17:25:04 +02:00
2024-10-05 23:17:20 +02:00
export const GLOBAL_METADATA_KEYS : AppMetadataKeys [] = [
2025-01-04 21:31:47 +01:00
// metadata applied migration id
'migration' ,
2024-10-05 23:17:20 +02:00
// downloads
'deleteChaptersManuallyMarkedRead' ,
'deleteChaptersWhileReading' ,
'deleteChaptersWithBookmark' ,
// library
'showAddToLibraryCategorySelectDialog' ,
'ignoreFilters' ,
'removeMangaFromCategories' ,
'showTabSize' ,
// library category options
// filter
'hasDownloadedChapters' ,
'hasBookmarkedChapters' ,
'hasUnreadChapters' ,
2024-12-27 03:32:30 +01:00
'hasReadChapters' ,
2024-10-05 23:17:20 +02:00
'hasDuplicateChapters' ,
'hasTrackerBinding' ,
'hasStatus' ,
2026-07-20 15:09:06 -07:00
'hasSource' ,
2024-10-05 23:17:20 +02:00
// sort
'sortBy' ,
'sortDesc' ,
// display
'showDownloadBadge' ,
'showUnreadBadge' ,
'showTabSize' ,
'showContinueReadingButton' ,
// client
'devices' ,
// migration
'migrateChapters' ,
'migrateCategories' ,
'migrateTracking' ,
'deleteChapters' ,
2026-03-07 03:07:40 +01:00
'migrateMetadata' ,
2024-10-05 23:17:20 +02:00
'migrateSortSettings' ,
// browse
'hideLibraryEntries' ,
2026-01-25 17:31:02 +01:00
'browseLanguages' ,
2025-04-21 19:18:42 +02:00
'showNsfw' ,
2025-05-17 23:37:00 +02:00
'lastUsedSourceId' ,
2025-05-18 00:59:01 +02:00
'shouldShowOnlySourcesWithResults' ,
2024-10-05 23:17:20 +02:00
2025-03-23 01:21:45 +01:00
// history
'hideHistory' ,
2024-10-05 23:17:20 +02:00
// tracking
'updateProgressAfterReading' ,
'updateProgressManualMarkRead' ,
// updates
'webUIInformAvailableUpdate' ,
2026-01-25 17:53:32 +01:00
'webUIInformVersionUpdated' ,
2024-10-05 23:17:20 +02:00
'serverInformAvailableUpdate' ,
2026-01-25 17:57:30 +01:00
'serverInformVersionUpdated' ,
2024-10-05 23:17:20 +02:00
// sources
'savedSearches' ,
2025-05-17 22:17:15 +02:00
'isPinned' ,
2025-06-23 02:18:31 +02:00
'isEnabled' ,
2024-10-05 23:17:20 +02:00
// themes
2026-01-22 00:12:22 +01:00
'locale' ,
2024-10-05 23:17:20 +02:00
'customThemes' ,
'mangaThumbnailBackdrop' ,
2025-01-26 03:53:19 +01:00
'mangaDynamicColorSchemes' ,
2024-11-04 18:00:35 +01:00
// reader
'exitMode' ,
'shouldSkipDupChapters' ,
2025-07-01 00:37:29 +02:00
'shouldSkipFilteredChapters' ,
2024-11-03 03:09:00 +01:00
'hotkeys' ,
2025-05-05 01:26:27 +02:00
'shouldShowTransitionPage' ,
2025-04-20 21:40:28 +02:00
// manga
// chapter list options
'reverse' ,
'bookmarked' ,
'downloaded' ,
'unread' ,
'showChapterNumber' ,
2025-06-23 02:20:40 +02:00
'excludedScanlators' ,
2024-10-05 23:17:20 +02:00
];
2024-10-03 04:03:04 +02:00
2024-10-05 23:17:20 +02:00
/**
* Once all changes have been done in the current branch, a new migration for all changes should be
* created.
*
* In case a value should be migrated for a specific key, and in the same migration the key is
* getting migrated, the key in the value migration is the "old" key (before the migration to the
* new key).
*
2026-01-25 17:31:02 +01:00
* For new metadata migrations, the value migrations have already been applied while the key migrations are still outstanding (same as for the value migrations)
*
2024-10-05 23:17:20 +02:00
* Migration order (function "applyMetadataMigrations"):
* 1. app metadata key prefix
* 2. app metadata values
2026-01-25 17:31:02 +01:00
* 3. create new app metadata with values from existing metadata
* 4. app metadata keys
* 5. app metadata keys deletion
2024-10-05 23:17:20 +02:00
*
* @example
* // changes:
* // commit: change key "X" to "Z"
* // commit: change key "Y" to "A"
* // commit: fix typo in reader setting "someTipo"
* // commit: add metadata migration
*
* // result:
* // old migrations:
* // const migrations = [
* // {
* // keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
* // }
* // ];
*
* // updated migrations
* const migrations = [
* {
* keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
* },
* {
* keys: [
* { oldKey: 'X', newKey: 'Z' },
* { oldKey: 'Y', newKey: 'A' },
* ],
* // all stored values in the metadata (of this app) will get migrated
* values: [
* {
* oldValue: 'someTipo',
* newValue: 'someTypo'
* },
* ],
* // to migrate only the value of a specific key (in this case of "someKey"):
* // values: [
* // {
* // key: 'someKey',
* // oldValue: 'someTipo',
* // newValue: 'someTypo',
* // },
* // ],
* },
* ];
*/
export const METADATA_MIGRATIONS : IMetadataMigration [] = [
{
keys : [{ oldKey : 'loadNextonEnding' , newKey : 'loadNextOnEnding' }],
},
{
keys : [{ oldKey : 'deleteChaptersAutoMarkedRead' , newKey : 'deleteChaptersWhileReading' }],
},
2024-10-03 04:03:04 +02:00
{
keys : [
{
oldKey : 'readerType' ,
newKey : 'readingMode' ,
},
2024-10-03 04:02:59 +02:00
{
oldKey : 'offsetFirstPage' ,
newKey : 'shouldOffsetDoubleSpreads' ,
},
{
oldKey : 'fitPageToWindow' ,
newKey : 'pageScaleMode' ,
},
{
oldKey : 'scalePage' ,
2024-11-04 18:00:35 +01:00
newKey : 'shouldStretchPage' ,
},
{
oldKey : 'skipDupChapters' ,
newKey : 'shouldSkipDupChapters' ,
},
{
oldKey : 'showPageNumber' ,
newKey : 'shouldShowPageNumber' ,
},
{
oldKey : 'staticNav' ,
newKey : 'isStaticNav' ,
2024-10-03 04:02:59 +02:00
},
2024-10-03 04:03:04 +02:00
],
values : [
// START: readerType
{
key : 'readerType' ,
oldValue : 'ContinuesVertical' ,
newValue : ` ${ ReadingMode . CONTINUOUS_VERTICAL } ` ,
},
{
key : 'readerType' ,
oldValue : 'Webtoon' ,
2025-01-06 12:51:26 +01:00
newValue : ` ${ ReadingMode . WEBTOON } ` ,
2024-10-03 04:03:04 +02:00
},
{
key : 'readerType' ,
oldValue : 'SingleRTL' ,
newValue : ` ${ ReadingMode . SINGLE_PAGE } ` ,
},
{
key : 'readerType' ,
oldValue : 'SingleLTR' ,
newValue : ` ${ ReadingMode . SINGLE_PAGE } ` ,
},
{
key : 'readerType' ,
oldValue : 'DoubleRTL' ,
newValue : ` ${ ReadingMode . DOUBLE_PAGE } ` ,
},
{
key : 'readerType' ,
oldValue : 'DoubleLTR' ,
newValue : ` ${ ReadingMode . DOUBLE_PAGE } ` ,
},
{
key : 'readerType' ,
oldValue : 'ContinuesHorizontalLTR' ,
newValue : ` ${ ReadingMode . CONTINUOUS_HORIZONTAL } ` ,
},
{
key : 'readerType' ,
oldValue : 'ContinuesHorizontalRTL' ,
newValue : ` ${ ReadingMode . CONTINUOUS_HORIZONTAL } ` ,
},
// END: readerType
2024-10-03 04:02:59 +02:00
// START: fitPageToWindow
{
key : 'fitPageToWindow' ,
oldValue : 'false' ,
newValue : ` ${ ReaderPageScaleMode . ORIGINAL } ` ,
},
{
key : 'fitPageToWindow' ,
oldValue : 'true' ,
newValue : ` ${ ReaderPageScaleMode . SCREEN } ` ,
},
// END: fitPageToWindow
2024-12-10 21:14:56 +01:00
{
key : 'readerWidth' ,
2024-12-11 11:28:56 +01:00
oldValue : /^[0-9]+$/g ,
2024-12-10 21:14:56 +01:00
newValue : ( oldValue ) => JSON . stringify ({ value : Number ( oldValue ), enabled : true }),
},
2024-10-03 04:03:04 +02:00
],
},
2025-01-05 14:29:47 +01:00
{
deleteKeys : [ 'pageScaleMode' , 'shouldStretchPage' , 'readerWidth' ],
},
2025-01-11 02:02:48 +01:00
{
values : [
{
key : 'progressBarPositionAutoVertical' ,
oldValue : '-1' ,
newValue : ` ${ DEFAULT_READER_SETTINGS . progressBarPositionAutoVertical } ` ,
},
{
key : 'progressBarPosition' ,
oldValue : '0' ,
newValue : ` ${ ProgressBarPosition . BOTTOM } ` ,
},
{
key : 'progressBarPosition' ,
oldValue : '1' ,
newValue : ` ${ ProgressBarPosition . LEFT } ` ,
},
{
key : 'progressBarPosition' ,
oldValue : '2' ,
newValue : ` ${ ProgressBarPosition . RIGHT } ` ,
},
],
},
2026-01-25 17:31:02 +01:00
{
values : [
{
key : 'sourceLanguages' ,
oldValue : /^\[.*]$/g ,
newValue : ( sourceLanguagesValue , key , oldMetadata ) => {
const extensionLanguagesKey = key . replace ( 'sourceLanguages' , 'extensionLanguages' );
const extensionLanguagesValue = oldMetadata [ extensionLanguagesKey ];
const convertedSourceLanguages = convertToObject < string [] >( sourceLanguagesValue , []);
const convertedExtensionLanguages = convertToObject < string [] >( extensionLanguagesValue , []);
return JSON . stringify (
toUniqueLanguageCodes ([... convertedSourceLanguages , ... convertedExtensionLanguages ]),
);
},
},
],
keys : [{ oldKey : 'sourceLanguages' , newKey : 'browseLanguages' }],
deleteKeys : [ 'sourceLanguages' , 'extensionLanguages' ],
},
2026-05-09 13:00:36 +02:00
{
values : [
...(() =>
[ 'pageScaleMode' , 'readingDirection' , 'readingMode' , 'tapZoneLayout' ]. map (( key ) => ({
key ,
oldValue : MATCH_ARRAY_NUMBERS ,
newValue : ( value ) => {
const array = jsonSaveParse < number [] >( value );
assertIsDefined ( array );
return JSON . stringify ( array [ 0 ]);
},
})) satisfies IMetadataMigration [ 'values' ])(),
],
},
2026-07-06 23:57:04 +02:00
{
values : [
{
key : 'browseLanguages' ,
oldValue : /^\[.*]$/g ,
newValue : ( browseLanguagesValue ) => {
const convertedBrowseLanguages = convertToObject < string [] >( browseLanguagesValue , []);
const convertedToValidIsoCodes = convertedBrowseLanguages . map (
( language ) => getISOLanguage ( language ) ? . isoCode ?? language ,
);
return JSON . stringify ( convertedToValidIsoCodes );
},
},
],
},
2024-10-05 23:17:20 +02:00
];
2026-03-07 03:07:40 +01:00
export const ALL_APP_METADATA_KEY_PREFIXES : string [] = [
... METADATA_MIGRATIONS . reduce < Set < string >>(( acc , migration ) => {
if ( migration . appKeyPrefix ? . oldPrefix ) {
acc . add ( migration . appKeyPrefix . oldPrefix );
}
return acc ;
}, new Set ()),
APP_METADATA_KEY_PREFIX ,
];