diff --git a/src/components/navbar/ReaderNavBar.tsx b/src/components/navbar/ReaderNavBar.tsx index 71e59d4b..b07a98c9 100644 --- a/src/components/navbar/ReaderNavBar.tsx +++ b/src/components/navbar/ReaderNavBar.tsx @@ -256,7 +256,7 @@ export default function ReaderNavBar(props: IProps) { setSettingValue={updateSettingValue} staticNav={settings.staticNav} showPageNumber={settings.showPageNumber} - loadNextonEnding={settings.loadNextonEnding} + loadNextOnEnding={settings.loadNextOnEnding} readerType={settings.readerType} /> diff --git a/src/components/reader/ReaderSettingsOptions.tsx b/src/components/reader/ReaderSettingsOptions.tsx index 12197b83..8ebdc95b 100644 --- a/src/components/reader/ReaderSettingsOptions.tsx +++ b/src/components/reader/ReaderSettingsOptions.tsx @@ -19,7 +19,7 @@ interface IProps extends IReaderSettings { } export default function ReaderSettingsOptions({ - staticNav, loadNextonEnding, readerType, showPageNumber, setSettingValue, + staticNav, loadNextOnEnding, readerType, showPageNumber, setSettingValue, }: IProps) { return ( <> @@ -49,8 +49,8 @@ export default function ReaderSettingsOptions({ setSettingValue('loadNextonEnding', e.target.checked)} + checked={loadNextOnEnding} + onChange={(e) => setSettingValue('loadNextOnEnding', e.target.checked)} /> diff --git a/src/components/reader/pager/DoublePagedPager.tsx b/src/components/reader/pager/DoublePagedPager.tsx index 8dab412f..ade3fd6a 100644 --- a/src/components/reader/pager/DoublePagedPager.tsx +++ b/src/components/reader/pager/DoublePagedPager.tsx @@ -102,7 +102,7 @@ export default function DoublePagedPager(props: IReaderProps) { if (curPage < pages.length - 1) { const nextCurPage = curPage + pagesDisplayed.current; setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage); - } else if (settings.loadNextonEnding) { + } else if (settings.loadNextOnEnding) { nextChapter(); } } diff --git a/src/components/reader/pager/HorizontalPager.tsx b/src/components/reader/pager/HorizontalPager.tsx index e6fe387b..8fa78004 100644 --- a/src/components/reader/pager/HorizontalPager.tsx +++ b/src/components/reader/pager/HorizontalPager.tsx @@ -42,7 +42,7 @@ export default function HorizontalPager(props: IReaderProps) { if (curPage < pages.length - 1) { pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' }); setCurPage((page) => page + 1); - } else if (settings.loadNextonEnding) { + } else if (settings.loadNextOnEnding) { nextChapter(); } } @@ -126,7 +126,7 @@ export default function HorizontalPager(props: IReaderProps) { }, [selfRef]); useEffect(() => { - if (settings.loadNextonEnding) { + if (settings.loadNextOnEnding) { document.addEventListener('scroll', handleLoadNextonEnding); } selfRef.current?.addEventListener('mousedown', clickControl); diff --git a/src/components/reader/pager/PagedPager.tsx b/src/components/reader/pager/PagedPager.tsx index 212ccf8e..74401ab7 100644 --- a/src/components/reader/pager/PagedPager.tsx +++ b/src/components/reader/pager/PagedPager.tsx @@ -24,7 +24,7 @@ export default function PagedReader(props: IReaderProps) { function nextPage() { if (curPage < pages.length - 1) { changePage(curPage + 1); - } else if (settings.loadNextonEnding) { + } else if (settings.loadNextOnEnding) { nextChapter(); } } diff --git a/src/components/reader/pager/VerticalPager.tsx b/src/components/reader/pager/VerticalPager.tsx index b4945c54..7f2e01b9 100644 --- a/src/components/reader/pager/VerticalPager.tsx +++ b/src/components/reader/pager/VerticalPager.tsx @@ -55,7 +55,7 @@ export default function VerticalPager(props: IReaderProps) { setCurPage(currentPageRef.current); // Go to next chapter if configured to and at bottom - if (settings.loadNextonEnding) { + if (settings.loadNextOnEnding) { nextChapter(); } } else { @@ -72,7 +72,7 @@ export default function VerticalPager(props: IReaderProps) { return () => { window.removeEventListener('scroll', handleScroll); }; - }, [settings.loadNextonEnding]); + }, [settings.loadNextOnEnding]); const go = useCallback((direction: 'up' | 'down') => { if (direction === 'down' && isAtBottom()) { diff --git a/src/screens/settings/DefaultReaderSettings.tsx b/src/screens/settings/DefaultReaderSettings.tsx index d8110a70..4664c41d 100644 --- a/src/screens/settings/DefaultReaderSettings.tsx +++ b/src/screens/settings/DefaultReaderSettings.tsx @@ -53,7 +53,7 @@ export default function DefaultReaderSettings() { setSettingValue={setSettingValue} staticNav={settings.staticNav} showPageNumber={settings.showPageNumber} - loadNextonEnding={settings.loadNextonEnding} + loadNextOnEnding={settings.loadNextOnEnding} readerType={settings.readerType} /> ); diff --git a/src/typings.d.ts b/src/typings.d.ts index 3206cce4..ed1871eb 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -54,6 +54,11 @@ interface IState { index: number } +interface IMetadataMigration { + appKeyPrefix?: { oldPrefix: string, newPrefix: string } + keys?: { oldKey: string, newKey: string }[] +} + interface IMetadata { [key: string]: VALUES; } @@ -168,7 +173,7 @@ type ReaderType = interface IReaderSettings{ staticNav: boolean showPageNumber: boolean - loadNextonEnding: boolean + loadNextOnEnding: boolean readerType: ReaderType } diff --git a/src/util/metadata.ts b/src/util/metadata.ts index 04996030..b102c917 100644 --- a/src/util/metadata.ts +++ b/src/util/metadata.ts @@ -11,7 +11,21 @@ import client from './client'; const APP_METADATA_KEY_PREFIX = 'webUI_'; -const getMetadataKey = (key: string) => `${APP_METADATA_KEY_PREFIX}${key}`; +const migrations: IMetadataMigration[] = [ + { + keys: [ + { oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }, + ], + }, +]; + +const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => `${appPrefix}${key}`; + +const doesMetadataKeyExistIn = ( + meta: IMetadata | undefined, + key: string, + appPrefix?: string, +): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix)); const convertValueFromMetadata = < T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes, @@ -33,31 +47,121 @@ const convertValueFromMetadata = < return value as T; }; +const getAppMetadataFrom = ( + meta: IMetadata, + appPrefix: string = APP_METADATA_KEY_PREFIX, +): IMetadata => { + const appMetadata: IMetadata = {}; + + Object.entries(meta).forEach(([key, value]) => { + if (key.startsWith(appPrefix)) { + appMetadata[key] = value; + } + }); + + return appMetadata; +}; + +const applyAppKeyPrefixMigration = (meta: IMetadata, migration: IMetadataMigration): IMetadata => { + const migratedMetadata: IMetadata = { ...meta }; + + if (!migration.appKeyPrefix) { + return migratedMetadata; + } + + const { oldPrefix, newPrefix } = migration.appKeyPrefix; + + const oldAppMetadata = getAppMetadataFrom(meta, oldPrefix); + const newAppMetadata = getAppMetadataFrom(meta, newPrefix); + + const missingMetadataKeys = Object.keys(oldAppMetadata) + .filter((key) => !Object.keys(newAppMetadata).includes(key)); + + const isMissingOldMetadata = missingMetadataKeys.length; + if (isMissingOldMetadata) { + missingMetadataKeys.forEach((oldKey) => { + const keyWithNewPrefix = oldKey.replace(oldPrefix, newPrefix); + migratedMetadata[keyWithNewPrefix] = oldAppMetadata[oldKey]; + }); + } + + return migratedMetadata; +}; + +const applyMetadataKeyMigration = (meta: IMetadata, migration: IMetadataMigration): IMetadata => { + const migratedMetadata: IMetadata = { ...meta }; + + if (!migration.keys) { + return migratedMetadata; + } + + const metadataKeyChanges = migration.keys; + + metadataKeyChanges.forEach(({ oldKey, newKey }) => { + if (!doesMetadataKeyExistIn(meta, oldKey)) { + return; + } + + if (doesMetadataKeyExistIn(meta, newKey)) { + return; + } + + migratedMetadata[getMetadataKey(newKey)] = meta[getMetadataKey(oldKey)]; + }); + + return migratedMetadata; +}; + +const applyMetadataMigrations = (meta?: IMetadata): IMetadata | undefined => { + if (!meta) { + return undefined; + } + + const migrationToMetadata: [number, IMetadata][] = [[0, meta]]; + + migrations.forEach((migration, index) => { + const migrationId = index + 1; + const metadataToMigrate = migrationToMetadata[migrationId - 1][1]; + const appKeyPrefixMigrated = applyAppKeyPrefixMigration(metadataToMigrate, migration); + const metadataKeysMigrated = applyMetadataKeyMigration(appKeyPrefixMigrated, migration); + + migrationToMetadata.push([migrationId, metadataKeysMigrated]); + }); + + const appliedMigration = migrationToMetadata.length > 1; + if (!appliedMigration) { + return { ...meta }; + } + + return migrationToMetadata.pop()![1]; +}; + export const getMetadataValueFrom = < T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes, >( { meta }: IMetadataHolder, key: AppMetadataKeys, defaultValue?: T, + applyMigrations: boolean = true, ): T | undefined => { - const metadataKey = getMetadataKey(key); + const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta; - const isMissingKey = !Object.prototype.hasOwnProperty.call(meta ?? {}, metadataKey); - if (meta === undefined || isMissingKey) { + if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) { return defaultValue; } - return convertValueFromMetadata(meta[metadataKey]); + return convertValueFromMetadata(metadata[getMetadataKey(key)]); }; export const getMetadataFrom = ( { meta }: IMetadataHolder, keysToDefaultValues: MetadataKeyValuePair[], + applyMigrations?: boolean, ): IMetadata => { const appMetadata: IMetadata = {}; keysToDefaultValues.forEach(([key, defaultValue]) => { - appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue); + appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue, applyMigrations); }); return appMetadata; diff --git a/src/util/readerSettings.ts b/src/util/readerSettings.ts index 37004b9d..db796498 100644 --- a/src/util/readerSettings.ts +++ b/src/util/readerSettings.ts @@ -13,31 +13,35 @@ export const getDefaultSettings = (forceUndefined: boolean = false) => ({ staticNav: forceUndefined ? undefined : false, showPageNumber: forceUndefined ? undefined : true, continuesPageGap: forceUndefined ? undefined : false, - loadNextonEnding: forceUndefined ? undefined : false, + loadNextOnEnding: forceUndefined ? undefined : false, readerType: forceUndefined ? undefined : 'ContinuesVertical', } as IReaderSettings); const getReaderSettingsWithDefaultValueFallback = ( meta?: IMetadata, defaultSettings?: IReaderSettings, + applyMetadataMigration: boolean = true, ): IReaderSettings => ({ ...getMetadataFrom( { meta }, Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[], + applyMetadataMigration, ) as unknown as IReaderSettings, }); export const getReaderSettingsFromMetadata = ( meta?: IMetadata, defaultSettings?: IReaderSettings, + applyMetadataMigration?: boolean, ): IReaderSettings => ({ - ...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings), + ...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration), }); export const getReaderSettingsFor = ( { meta }: IMetadataHolder, defaultSettings?: IReaderSettings, -): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings); + applyMetadataMigration?: boolean, +): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration); export const useDefaultReaderSettings = (): { metadata?: IMetadata, @@ -63,7 +67,7 @@ export const checkAndHandleMissingStoredReaderSettings = async ( defaultSettings: IReaderSettings, ): Promise => { const meta = metadataHolder.meta ?? metadataHolder as IMetadata; - const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true)); + const settingsToCheck = getReaderSettingsFor({ meta }, getDefaultSettings(true), false); const newSettings = getReaderSettingsFor({ meta }, defaultSettings); const undefinedSettings = Object.entries(settingsToCheck)