From 97027287715629b5017801550f2387797adefb24 Mon Sep 17 00:00:00 2001 From: Daniel <50052685+schroda@users.noreply.github.com> Date: Fri, 6 Jan 2023 18:19:45 +0100 Subject: [PATCH] add logic for metadata migration (#218) * [#215] Handle metadata migration Makes it possible to change "metadata keys" and the "app metadata key prefix" without losing data * [#215] Fix checking and handling missing reader settings in metadata With the new migration logic missing reader settings couldn't be detected anymore in case the metadata included outdated app metadata keys for the reader settings. * [#215] Fix name of "IReaderSettings" property --- src/components/navbar/ReaderNavBar.tsx | 2 +- .../reader/ReaderSettingsOptions.tsx | 6 +- .../reader/pager/DoublePagedPager.tsx | 2 +- .../reader/pager/HorizontalPager.tsx | 4 +- src/components/reader/pager/PagedPager.tsx | 2 +- src/components/reader/pager/VerticalPager.tsx | 4 +- .../settings/DefaultReaderSettings.tsx | 2 +- src/typings.d.ts | 7 +- src/util/metadata.ts | 116 +++++++++++++++++- src/util/readerSettings.ts | 12 +- 10 files changed, 135 insertions(+), 22 deletions(-) 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)