* feat: add Source filter to library filtering system Add a new Source filter section to the library options panel that allows users to filter their library manga by source (e.g. MangaDex, MangaSee). - Add hasSource to LibraryOptions type and category metadata - Add source filtering logic in useGetVisibleLibraryMangas - Add Source section with three-state checkboxes in filter UI - Show loading spinner while source list loads - Include source filter in active filter indicator Closes #1057 * refactor: address review feedback for source filter - Remove skip option, let query always run - Use uniqBy from lodash/fp for deduplication - Remove loading spinner (data available quickly) - Wrap source section in length check * docs: add source filter to changelog --------- Co-authored-by: Copilot <noreply@github.com>
767 lines
23 KiB
TypeScript
767 lines
23 KiB
TypeScript
/*
|
|
* 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/.
|
|
*/
|
|
|
|
import deepmerge from '@mui/utils/deepmerge';
|
|
import type { AppMetadataKeys, IMetadataMigration } from '@/features/metadata/Metadata.types.ts';
|
|
import type { IReaderSettings, ReaderCustomFilter, SafeAreaInset } from '@/features/reader/Reader.types.ts';
|
|
import { ProgressBarPosition, ReaderPageScaleMode, ReadingMode } from '@/features/reader/Reader.types.ts';
|
|
import {
|
|
AUTO_SCROLL_SPEED,
|
|
CUSTOM_FILTER,
|
|
DEFAULT_READER_SETTINGS,
|
|
IMAGE_PRE_LOAD_AMOUNT,
|
|
PAGE_GAP,
|
|
PROGRESS_BAR_SIZE,
|
|
SCROLL_AMOUNT,
|
|
} from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
|
import { coerceIn, jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
|
import { DOWNLOAD_AHEAD } from '@/features/downloads/Downloads.constants.ts';
|
|
import { MANGA_GRID_WIDTH } from '@/features/settings/Settings.constants.ts';
|
|
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';
|
|
import { detectLocale, getISOLanguage } from '@/lib/ISOLanguageUtil.ts';
|
|
import type { I18nResourceCode } from '@/i18n';
|
|
import { i18nResources } from '@/i18n';
|
|
import { toUniqueLanguageCodes } from '@/base/utils/Languages.ts';
|
|
import { assertIsDefined } from '@/base/Asserts.ts';
|
|
|
|
const MATCH_ARRAY_NUMBERS = /^\[\d+(?:,\d+)*]$/g;
|
|
|
|
export const APP_METADATA_KEY_PREFIX = 'webUI';
|
|
|
|
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, defaultValue: number): number => {
|
|
if (Number.isNaN(+value)) {
|
|
return defaultValue;
|
|
}
|
|
|
|
return +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<T>(value) ?? defaultValue;
|
|
|
|
if (Array.isArray(convertedValue)) {
|
|
return convertedValue;
|
|
}
|
|
|
|
if (defaultValue == null) {
|
|
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,
|
|
},
|
|
migrateMetadata: {
|
|
convert: convertToBoolean,
|
|
},
|
|
migrateSortSettings: {
|
|
convert: convertToObject<SortSettings>,
|
|
},
|
|
hideLibraryEntries: {
|
|
convert: convertToBoolean,
|
|
},
|
|
updateProgressAfterReading: {
|
|
convert: convertToBoolean,
|
|
},
|
|
updateProgressManualMarkRead: {
|
|
convert: convertToBoolean,
|
|
},
|
|
webUIInformAvailableUpdate: {
|
|
convert: convertToBoolean,
|
|
},
|
|
webUIInformVersionUpdated: {
|
|
convert: convertToBoolean,
|
|
},
|
|
serverInformAvailableUpdate: {
|
|
convert: convertToBoolean,
|
|
},
|
|
serverInformVersionUpdated: {
|
|
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']>,
|
|
},
|
|
hasSource: {
|
|
convert: convertToObject<LibraryOptions['hasSource']>,
|
|
},
|
|
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,
|
|
},
|
|
shouldSkipFilteredChapters: {
|
|
convert: convertToBoolean,
|
|
},
|
|
isStaticNav: {
|
|
convert: convertToBoolean,
|
|
},
|
|
overlayMode: {
|
|
convert: convertToNumber, // ReaderOverlayMode (enum)
|
|
},
|
|
shouldStretchPage: {
|
|
convert: convertToBoolean,
|
|
},
|
|
shouldShowPageNumber: {
|
|
convert: convertToBoolean,
|
|
},
|
|
backgroundColor: {
|
|
convert: convertToNumber, // ReaderBackgroundColor (enum)
|
|
},
|
|
useAutoBackgroundColorContinuousMode: {
|
|
convert: convertToBoolean,
|
|
},
|
|
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,
|
|
},
|
|
shouldWakeLockScreen: {
|
|
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,
|
|
},
|
|
browseLanguages: {
|
|
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[]>,
|
|
},
|
|
locale: {
|
|
convert: convertToString,
|
|
toConstrainedValue: (value: string) => {
|
|
const locale = getISOLanguage(value)?.isoCode;
|
|
|
|
if (!locale || !i18nResources.includes(locale as I18nResourceCode)) {
|
|
return detectLocale();
|
|
}
|
|
|
|
return locale;
|
|
},
|
|
},
|
|
safeAreaInset: {
|
|
convert: convertToObject<SafeAreaInset>,
|
|
},
|
|
} as const;
|
|
|
|
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA);
|
|
|
|
export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
|
// metadata applied migration id
|
|
'migration',
|
|
|
|
// downloads
|
|
'deleteChaptersManuallyMarkedRead',
|
|
'deleteChaptersWhileReading',
|
|
'deleteChaptersWithBookmark',
|
|
|
|
// library
|
|
'showAddToLibraryCategorySelectDialog',
|
|
'ignoreFilters',
|
|
'removeMangaFromCategories',
|
|
'showTabSize',
|
|
|
|
// library category options
|
|
// filter
|
|
'hasDownloadedChapters',
|
|
'hasBookmarkedChapters',
|
|
'hasUnreadChapters',
|
|
'hasReadChapters',
|
|
'hasDuplicateChapters',
|
|
'hasTrackerBinding',
|
|
'hasStatus',
|
|
'hasSource',
|
|
// sort
|
|
'sortBy',
|
|
'sortDesc',
|
|
// display
|
|
'showDownloadBadge',
|
|
'showUnreadBadge',
|
|
'showTabSize',
|
|
'showContinueReadingButton',
|
|
|
|
// client
|
|
'devices',
|
|
|
|
// migration
|
|
'migrateChapters',
|
|
'migrateCategories',
|
|
'migrateTracking',
|
|
'deleteChapters',
|
|
'migrateMetadata',
|
|
'migrateSortSettings',
|
|
|
|
// browse
|
|
'hideLibraryEntries',
|
|
'browseLanguages',
|
|
'showNsfw',
|
|
'lastUsedSourceId',
|
|
'shouldShowOnlySourcesWithResults',
|
|
|
|
// history
|
|
'hideHistory',
|
|
|
|
// tracking
|
|
'updateProgressAfterReading',
|
|
'updateProgressManualMarkRead',
|
|
|
|
// updates
|
|
'webUIInformAvailableUpdate',
|
|
'webUIInformVersionUpdated',
|
|
'serverInformAvailableUpdate',
|
|
'serverInformVersionUpdated',
|
|
|
|
// sources
|
|
'savedSearches',
|
|
'isPinned',
|
|
'isEnabled',
|
|
|
|
// themes
|
|
'locale',
|
|
'customThemes',
|
|
'mangaThumbnailBackdrop',
|
|
'mangaDynamicColorSchemes',
|
|
|
|
// reader
|
|
'exitMode',
|
|
'shouldSkipDupChapters',
|
|
'shouldSkipFilteredChapters',
|
|
'hotkeys',
|
|
'shouldShowTransitionPage',
|
|
|
|
// manga
|
|
// chapter list options
|
|
'reverse',
|
|
'bookmarked',
|
|
'downloaded',
|
|
'unread',
|
|
'showChapterNumber',
|
|
'excludedScanlators',
|
|
];
|
|
|
|
/**
|
|
* 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).
|
|
*
|
|
* For new metadata migrations, the value migrations have already been applied while the key migrations are still outstanding (same as for the value migrations)
|
|
*
|
|
* Migration order (function "applyMetadataMigrations"):
|
|
* 1. app metadata key prefix
|
|
* 2. app metadata values
|
|
* 3. create new app metadata with values from existing metadata
|
|
* 4. app metadata keys
|
|
* 5. app metadata keys deletion
|
|
*
|
|
* @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' }],
|
|
},
|
|
{
|
|
keys: [
|
|
{
|
|
oldKey: 'readerType',
|
|
newKey: 'readingMode',
|
|
},
|
|
{
|
|
oldKey: 'offsetFirstPage',
|
|
newKey: 'shouldOffsetDoubleSpreads',
|
|
},
|
|
{
|
|
oldKey: 'fitPageToWindow',
|
|
newKey: 'pageScaleMode',
|
|
},
|
|
{
|
|
oldKey: 'scalePage',
|
|
newKey: 'shouldStretchPage',
|
|
},
|
|
{
|
|
oldKey: 'skipDupChapters',
|
|
newKey: 'shouldSkipDupChapters',
|
|
},
|
|
{
|
|
oldKey: 'showPageNumber',
|
|
newKey: 'shouldShowPageNumber',
|
|
},
|
|
{
|
|
oldKey: 'staticNav',
|
|
newKey: 'isStaticNav',
|
|
},
|
|
],
|
|
values: [
|
|
// START: readerType
|
|
{
|
|
key: 'readerType',
|
|
oldValue: 'ContinuesVertical',
|
|
newValue: `${ReadingMode.CONTINUOUS_VERTICAL}`,
|
|
},
|
|
{
|
|
key: 'readerType',
|
|
oldValue: 'Webtoon',
|
|
newValue: `${ReadingMode.WEBTOON}`,
|
|
},
|
|
{
|
|
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
|
|
// START: fitPageToWindow
|
|
{
|
|
key: 'fitPageToWindow',
|
|
oldValue: 'false',
|
|
newValue: `${ReaderPageScaleMode.ORIGINAL}`,
|
|
},
|
|
{
|
|
key: 'fitPageToWindow',
|
|
oldValue: 'true',
|
|
newValue: `${ReaderPageScaleMode.SCREEN}`,
|
|
},
|
|
// END: fitPageToWindow
|
|
{
|
|
key: 'readerWidth',
|
|
oldValue: /^[0-9]+$/g,
|
|
newValue: (oldValue) => JSON.stringify({ value: Number(oldValue), enabled: true }),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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}`,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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'],
|
|
},
|
|
{
|
|
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'])(),
|
|
],
|
|
},
|
|
{
|
|
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);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
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,
|
|
];
|