Files
suwayomi-material-you-webui/src/modules/metadata/Metadata.constants.ts

649 lines
19 KiB
TypeScript
Raw Normal View History

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/.
*/
// eslint-disable-next-line import/no-extraneous-dependencies,no-restricted-imports
import deepmerge from '@mui/utils/deepmerge';
2024-10-05 23:17:20 +02:00
import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata.types.ts';
import {
IReaderSettings,
ProgressBarPosition,
ReaderCustomFilter,
ReaderPageScaleMode,
ReadingMode,
} from '@/modules/reader/types/Reader.types.ts';
import {
AUTO_SCROLL_SPEED,
CUSTOM_FILTER,
DEFAULT_READER_SETTINGS,
IMAGE_PRE_LOAD_AMOUNT,
PAGE_GAP,
PROGRESS_BAR_SIZE,
SCROLL_AMOUNT,
} from '@/modules/reader/constants/ReaderSettings.constants.tsx';
import { coerceIn, jsonSaveParse } from '@/lib/HelperFunctions.ts';
import { DOWNLOAD_AHEAD } from '@/modules/downloads/Downloads.constants.ts';
import { MANGA_GRID_WIDTH } from '@/modules/settings/Settings.constants.ts';
import { NullAndUndefined } from '@/Base.types.ts';
import { SortSettings } from '@/modules/migration/Migration.types.ts';
import { ISourceMetadata } from '@/modules/source/Source.types.ts';
import { LibraryOptions } from '@/modules/library/Library.types.ts';
import { MetadataThemeSettings } from '@/modules/theme/AppTheme.types.ts';
import { TapZoneInvertMode } from '@/modules/reader/types/TapZoneLayout.types.ts';
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
const convertToTypeNullAndUndefined = <T>(value: string, convertToType: (value: string) => T): NullAndUndefined<T> => {
if (value === 'null') return null;
if (value === 'undefined') return undefined;
return convertToType(value);
};
const convertToString = (value: string): string => value;
const convertToNumber = (value: string): number => +value;
const convertToBoolean = (value: string): boolean => value === 'true';
const convertToStringNullAndUndefined = (value: string) => convertToTypeNullAndUndefined(value, convertToString);
const convertToBooleanNullAndUndefined = (value: string) => convertToTypeNullAndUndefined(value, convertToBoolean);
const convertToObject = <T>(value: string, defaultValue: T): T => {
const convertedValue = jsonSaveParse(value) ?? defaultValue;
if (Array.isArray(defaultValue)) {
return convertedValue;
}
return deepmerge(defaultValue, convertedValue);
};
export const APP_METADATA: Record<
AppMetadataKeys,
{
convert: (value: string, defaultValue: any) => any;
toConstrainedValue?: (value: any) => any;
}
> = {
migration: {
convert: convertToNumber,
},
deleteChaptersManuallyMarkedRead: {
convert: convertToBoolean,
},
deleteChaptersWhileReading: {
convert: convertToNumber,
},
deleteChaptersWithBookmark: {
convert: convertToBoolean,
},
downloadAheadLimit: {
convert: convertToNumber,
toConstrainedValue: (value: number) => {
const isDisabled = value === 0;
if (isDisabled) {
return value;
}
return coerceIn(value, DOWNLOAD_AHEAD.min, DOWNLOAD_AHEAD.max);
},
},
showAddToLibraryCategorySelectDialog: {
convert: convertToBoolean,
},
ignoreFilters: {
convert: convertToBoolean,
},
removeMangaFromCategories: {
convert: convertToBoolean,
},
showTabSize: {
convert: convertToBoolean,
},
devices: {
convert: convertToObject<string[]>,
},
migrateChapters: {
convert: convertToBoolean,
},
migrateCategories: {
convert: convertToBoolean,
},
migrateTracking: {
convert: convertToBoolean,
},
deleteChapters: {
convert: convertToBoolean,
},
migrateSortSettings: {
convert: convertToObject<SortSettings>,
},
hideLibraryEntries: {
convert: convertToBoolean,
},
updateProgressAfterReading: {
convert: convertToBoolean,
},
updateProgressManualMarkRead: {
convert: convertToBoolean,
},
webUIInformAvailableUpdate: {
convert: convertToBoolean,
},
serverInformAvailableUpdate: {
convert: convertToBoolean,
},
readerWidth: {
convert: convertToObject<IReaderSettings['readerWidth']>,
},
savedSearches: {
convert: convertToObject<ISourceMetadata['savedSearches']>,
},
showContinueReadingButton: {
convert: convertToBoolean,
},
showDownloadBadge: {
convert: convertToBoolean,
},
showUnreadBadge: {
convert: convertToBoolean,
},
gridLayout: {
convert: convertToNumber, // GridLayout (enum)
},
sortBy: {
convert: convertToStringNullAndUndefined, // LibrarySortMode
},
sortDesc: {
convert: convertToBooleanNullAndUndefined,
},
hasDownloadedChapters: {
convert: convertToBooleanNullAndUndefined,
},
hasBookmarkedChapters: {
convert: convertToBooleanNullAndUndefined,
},
hasUnreadChapters: {
convert: convertToBooleanNullAndUndefined,
},
hasReadChapters: {
convert: convertToBooleanNullAndUndefined,
},
hasDuplicateChapters: {
convert: convertToBooleanNullAndUndefined,
},
hasTrackerBinding: {
convert: convertToObject<LibraryOptions['hasTrackerBinding']>,
},
hasStatus: {
convert: convertToObject<LibraryOptions['hasStatus']>,
},
customThemes: {
convert: convertToObject<MetadataThemeSettings['customThemes']>,
},
mangaThumbnailBackdrop: {
convert: convertToBoolean,
},
mangaDynamicColorSchemes: {
convert: convertToBoolean,
},
tapZoneLayout: {
convert: convertToNumber, // TapZoneLayouts (enum)
},
tapZoneInvertMode: {
convert: convertToObject<TapZoneInvertMode>, // TapZoneInvertMode (object)
},
readingDirection: {
convert: convertToNumber, // ReadingDirection (enum)
},
progressBarType: {
convert: convertToNumber, // ProgressBarType (enum)
},
progressBarSize: {
convert: convertToNumber,
toConstrainedValue: (value: number) => coerceIn(value, PROGRESS_BAR_SIZE.min, PROGRESS_BAR_SIZE.max),
},
progressBarPosition: {
convert: convertToNumber, // ProgressBarPosition (enum)
},
progressBarPositionAutoVertical: {
convert: convertToNumber, // TProgressBarPositionAutoVertical (enum)
},
readingMode: {
convert: convertToNumber, // ReadingMode (enum)
},
pageScaleMode: {
convert: convertToNumber, // ReaderPageScaleMode (enum)
},
shouldOffsetDoubleSpreads: {
convert: convertToBoolean,
},
exitMode: {
convert: convertToNumber, // ReaderExitMode (enum)
},
customFilter: {
convert: convertToObject<ReaderCustomFilter>,
toConstrainedValue: (value: ReaderCustomFilter): ReaderCustomFilter => ({
...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: {
convert: convertToBoolean,
},
isStaticNav: {
convert: convertToBoolean,
},
overlayMode: {
convert: convertToNumber, // ReaderOverlayMode (enum)
},
shouldStretchPage: {
convert: convertToBoolean,
},
shouldShowPageNumber: {
convert: convertToBoolean,
},
backgroundColor: {
convert: convertToNumber, // ReaderBackgroundColor (enum)
},
pageGap: {
convert: convertToNumber,
toConstrainedValue: (value: number) => coerceIn(value, PAGE_GAP.min, PAGE_GAP.max),
},
hotkeys: {
convert: convertToObject<IReaderSettings['hotkeys']>,
},
imagePreLoadAmount: {
convert: convertToNumber,
toConstrainedValue: (value: number) => coerceIn(value, IMAGE_PRE_LOAD_AMOUNT.min, IMAGE_PRE_LOAD_AMOUNT.max),
},
shouldUseAutoWebtoonMode: {
convert: convertToBoolean,
},
autoScroll: {
convert: convertToObject<IReaderSettings['autoScroll']>,
toConstrainedValue: (value: IReaderSettings['autoScroll']): IReaderSettings['autoScroll'] => ({
...value,
value: coerceIn(value.value, AUTO_SCROLL_SPEED.min, AUTO_SCROLL_SPEED.max),
}),
},
shouldShowReadingModePreview: {
convert: convertToBoolean,
},
shouldShowTapZoneLayoutPreview: {
convert: convertToBoolean,
},
shouldInformAboutMissingChapter: {
convert: convertToBoolean,
},
shouldInformAboutScanlatorChange: {
convert: convertToBoolean,
},
hideHistory: {
convert: convertToBoolean,
},
scrollAmount: {
convert: convertToNumber, // ReaderScrollAmount (enum)
toConstrainedValue: (value: number) => coerceIn(value, SCROLL_AMOUNT.min, SCROLL_AMOUNT.max),
},
reverse: {
convert: convertToBoolean,
},
bookmarked: {
convert: convertToBooleanNullAndUndefined,
},
downloaded: {
convert: convertToBooleanNullAndUndefined,
},
unread: {
convert: convertToBooleanNullAndUndefined,
},
showChapterNumber: {
convert: convertToBoolean,
},
extensionLanguages: {
convert: convertToObject<string[]>,
},
sourceLanguages: {
convert: convertToObject<string[]>,
},
showNsfw: {
convert: convertToBoolean,
},
shouldUseInfiniteScroll: {
convert: convertToBoolean,
},
shouldShowTransitionPage: {
convert: convertToBoolean,
},
appTheme: {
convert: convertToString,
},
themeMode: {
convert: convertToString, // ThemeMode (enum)
},
shouldUsePureBlackMode: {
convert: convertToBoolean,
},
mangaGridItemWidth: {
convert: convertToNumber,
toConstrainedValue: (value: number) => coerceIn(value, MANGA_GRID_WIDTH.min, MANGA_GRID_WIDTH.max),
},
isPinned: {
convert: convertToBoolean,
},
isEnabled: {
convert: convertToBoolean,
},
lastUsedSourceId: {
convert: convertToString,
},
shouldShowOnlySourcesWithResults: {
convert: convertToBoolean,
},
excludedScanlators: {
convert: convertToObject<string[]>,
},
} as const;
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA);
2024-10-05 23:17:20 +02:00
export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
// 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',
// sort
'sortBy',
'sortDesc',
// display
'showDownloadBadge',
'showUnreadBadge',
'showTabSize',
'showContinueReadingButton',
// client
'devices',
// migration
'migrateChapters',
'migrateCategories',
'migrateTracking',
'deleteChapters',
'migrateSortSettings',
// browse
'hideLibraryEntries',
'extensionLanguages',
'sourceLanguages',
2025-04-21 19:18:42 +02:00
'showNsfw',
2025-05-17 23:37:00 +02:00
'lastUsedSourceId',
'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',
'serverInformAvailableUpdate',
// sources
'savedSearches',
2025-05-17 22:17:15 +02:00
'isPinned',
'isEnabled',
2024-10-05 23:17:20 +02:00
// themes
'customThemes',
'mangaThumbnailBackdrop',
'mangaDynamicColorSchemes',
2024-11-04 18:00:35 +01:00
// reader
'exitMode',
'customFilter',
'shouldSkipDupChapters',
2024-11-03 03:09:00 +01:00
'hotkeys',
'shouldShowTransitionPage',
2025-04-20 21:40:28 +02:00
// manga
// chapter list options
'reverse',
'bookmarked',
'downloaded',
'unread',
'showChapterNumber',
'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).
*
* Migration order (function "applyMetadataMigrations"):
* 1. app metadata key prefix
* 2. app metadata values
* 3. app metadata keys
* 4. 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',
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',
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'],
},
{
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}`,
},
],
},
2024-10-05 23:17:20 +02:00
];