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
This commit is contained in:
@@ -256,7 +256,7 @@ export default function ReaderNavBar(props: IProps) {
|
|||||||
setSettingValue={updateSettingValue}
|
setSettingValue={updateSettingValue}
|
||||||
staticNav={settings.staticNav}
|
staticNav={settings.staticNav}
|
||||||
showPageNumber={settings.showPageNumber}
|
showPageNumber={settings.showPageNumber}
|
||||||
loadNextonEnding={settings.loadNextonEnding}
|
loadNextOnEnding={settings.loadNextOnEnding}
|
||||||
readerType={settings.readerType}
|
readerType={settings.readerType}
|
||||||
/>
|
/>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface IProps extends IReaderSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ReaderSettingsOptions({
|
export default function ReaderSettingsOptions({
|
||||||
staticNav, loadNextonEnding, readerType, showPageNumber, setSettingValue,
|
staticNav, loadNextOnEnding, readerType, showPageNumber, setSettingValue,
|
||||||
}: IProps) {
|
}: IProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -49,8 +49,8 @@ export default function ReaderSettingsOptions({
|
|||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
edge="end"
|
edge="end"
|
||||||
checked={loadNextonEnding}
|
checked={loadNextOnEnding}
|
||||||
onChange={(e) => setSettingValue('loadNextonEnding', e.target.checked)}
|
onChange={(e) => setSettingValue('loadNextOnEnding', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function DoublePagedPager(props: IReaderProps) {
|
|||||||
if (curPage < pages.length - 1) {
|
if (curPage < pages.length - 1) {
|
||||||
const nextCurPage = curPage + pagesDisplayed.current;
|
const nextCurPage = curPage + pagesDisplayed.current;
|
||||||
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
|
setCurPage((nextCurPage >= pages.length) ? pages.length - 1 : nextCurPage);
|
||||||
} else if (settings.loadNextonEnding) {
|
} else if (settings.loadNextOnEnding) {
|
||||||
nextChapter();
|
nextChapter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function HorizontalPager(props: IReaderProps) {
|
|||||||
if (curPage < pages.length - 1) {
|
if (curPage < pages.length - 1) {
|
||||||
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
|
pagesRef.current[curPage + 1]?.scrollIntoView({ inline: 'center' });
|
||||||
setCurPage((page) => page + 1);
|
setCurPage((page) => page + 1);
|
||||||
} else if (settings.loadNextonEnding) {
|
} else if (settings.loadNextOnEnding) {
|
||||||
nextChapter();
|
nextChapter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ export default function HorizontalPager(props: IReaderProps) {
|
|||||||
}, [selfRef]);
|
}, [selfRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (settings.loadNextonEnding) {
|
if (settings.loadNextOnEnding) {
|
||||||
document.addEventListener('scroll', handleLoadNextonEnding);
|
document.addEventListener('scroll', handleLoadNextonEnding);
|
||||||
}
|
}
|
||||||
selfRef.current?.addEventListener('mousedown', clickControl);
|
selfRef.current?.addEventListener('mousedown', clickControl);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function PagedReader(props: IReaderProps) {
|
|||||||
function nextPage() {
|
function nextPage() {
|
||||||
if (curPage < pages.length - 1) {
|
if (curPage < pages.length - 1) {
|
||||||
changePage(curPage + 1);
|
changePage(curPage + 1);
|
||||||
} else if (settings.loadNextonEnding) {
|
} else if (settings.loadNextOnEnding) {
|
||||||
nextChapter();
|
nextChapter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function VerticalPager(props: IReaderProps) {
|
|||||||
setCurPage(currentPageRef.current);
|
setCurPage(currentPageRef.current);
|
||||||
|
|
||||||
// Go to next chapter if configured to and at bottom
|
// Go to next chapter if configured to and at bottom
|
||||||
if (settings.loadNextonEnding) {
|
if (settings.loadNextOnEnding) {
|
||||||
nextChapter();
|
nextChapter();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -72,7 +72,7 @@ export default function VerticalPager(props: IReaderProps) {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, [settings.loadNextonEnding]);
|
}, [settings.loadNextOnEnding]);
|
||||||
|
|
||||||
const go = useCallback((direction: 'up' | 'down') => {
|
const go = useCallback((direction: 'up' | 'down') => {
|
||||||
if (direction === 'down' && isAtBottom()) {
|
if (direction === 'down' && isAtBottom()) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function DefaultReaderSettings() {
|
|||||||
setSettingValue={setSettingValue}
|
setSettingValue={setSettingValue}
|
||||||
staticNav={settings.staticNav}
|
staticNav={settings.staticNav}
|
||||||
showPageNumber={settings.showPageNumber}
|
showPageNumber={settings.showPageNumber}
|
||||||
loadNextonEnding={settings.loadNextonEnding}
|
loadNextOnEnding={settings.loadNextOnEnding}
|
||||||
readerType={settings.readerType}
|
readerType={settings.readerType}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
7
src/typings.d.ts
vendored
7
src/typings.d.ts
vendored
@@ -54,6 +54,11 @@ interface IState {
|
|||||||
index: number
|
index: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IMetadataMigration {
|
||||||
|
appKeyPrefix?: { oldPrefix: string, newPrefix: string }
|
||||||
|
keys?: { oldKey: string, newKey: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
|
interface IMetadata<VALUES extends AllowedMetadataValueTypes = string> {
|
||||||
[key: string]: VALUES;
|
[key: string]: VALUES;
|
||||||
}
|
}
|
||||||
@@ -168,7 +173,7 @@ type ReaderType =
|
|||||||
interface IReaderSettings{
|
interface IReaderSettings{
|
||||||
staticNav: boolean
|
staticNav: boolean
|
||||||
showPageNumber: boolean
|
showPageNumber: boolean
|
||||||
loadNextonEnding: boolean
|
loadNextOnEnding: boolean
|
||||||
readerType: ReaderType
|
readerType: ReaderType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,21 @@ import client from './client';
|
|||||||
|
|
||||||
const APP_METADATA_KEY_PREFIX = 'webUI_';
|
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 = <
|
const convertValueFromMetadata = <
|
||||||
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
|
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
|
||||||
@@ -33,31 +47,121 @@ const convertValueFromMetadata = <
|
|||||||
return value as T;
|
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 = <
|
export const getMetadataValueFrom = <
|
||||||
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
|
T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes,
|
||||||
>(
|
>(
|
||||||
{ meta }: IMetadataHolder,
|
{ meta }: IMetadataHolder,
|
||||||
key: AppMetadataKeys,
|
key: AppMetadataKeys,
|
||||||
defaultValue?: T,
|
defaultValue?: T,
|
||||||
|
applyMigrations: boolean = true,
|
||||||
): T | undefined => {
|
): T | undefined => {
|
||||||
const metadataKey = getMetadataKey(key);
|
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
|
||||||
|
|
||||||
const isMissingKey = !Object.prototype.hasOwnProperty.call(meta ?? {}, metadataKey);
|
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
|
||||||
if (meta === undefined || isMissingKey) {
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertValueFromMetadata(meta[metadataKey]);
|
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMetadataFrom = (
|
export const getMetadataFrom = (
|
||||||
{ meta }: IMetadataHolder,
|
{ meta }: IMetadataHolder,
|
||||||
keysToDefaultValues: MetadataKeyValuePair[],
|
keysToDefaultValues: MetadataKeyValuePair[],
|
||||||
|
applyMigrations?: boolean,
|
||||||
): IMetadata<AllowedMetadataValueTypes> => {
|
): IMetadata<AllowedMetadataValueTypes> => {
|
||||||
const appMetadata: IMetadata<AllowedMetadataValueTypes> = {};
|
const appMetadata: IMetadata<AllowedMetadataValueTypes> = {};
|
||||||
|
|
||||||
keysToDefaultValues.forEach(([key, defaultValue]) => {
|
keysToDefaultValues.forEach(([key, defaultValue]) => {
|
||||||
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue);
|
appMetadata[key] = getMetadataValueFrom({ meta }, key, defaultValue, applyMigrations);
|
||||||
});
|
});
|
||||||
|
|
||||||
return appMetadata;
|
return appMetadata;
|
||||||
|
|||||||
@@ -13,31 +13,35 @@ export const getDefaultSettings = (forceUndefined: boolean = false) => ({
|
|||||||
staticNav: forceUndefined ? undefined : false,
|
staticNav: forceUndefined ? undefined : false,
|
||||||
showPageNumber: forceUndefined ? undefined : true,
|
showPageNumber: forceUndefined ? undefined : true,
|
||||||
continuesPageGap: forceUndefined ? undefined : false,
|
continuesPageGap: forceUndefined ? undefined : false,
|
||||||
loadNextonEnding: forceUndefined ? undefined : false,
|
loadNextOnEnding: forceUndefined ? undefined : false,
|
||||||
readerType: forceUndefined ? undefined : 'ContinuesVertical',
|
readerType: forceUndefined ? undefined : 'ContinuesVertical',
|
||||||
} as IReaderSettings);
|
} as IReaderSettings);
|
||||||
|
|
||||||
const getReaderSettingsWithDefaultValueFallback = (
|
const getReaderSettingsWithDefaultValueFallback = (
|
||||||
meta?: IMetadata,
|
meta?: IMetadata,
|
||||||
defaultSettings?: IReaderSettings,
|
defaultSettings?: IReaderSettings,
|
||||||
|
applyMetadataMigration: boolean = true,
|
||||||
): IReaderSettings => ({
|
): IReaderSettings => ({
|
||||||
...getMetadataFrom(
|
...getMetadataFrom(
|
||||||
{ meta },
|
{ meta },
|
||||||
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
|
Object.entries(defaultSettings ?? getDefaultSettings()) as MetadataKeyValuePair[],
|
||||||
|
applyMetadataMigration,
|
||||||
) as unknown as IReaderSettings,
|
) as unknown as IReaderSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getReaderSettingsFromMetadata = (
|
export const getReaderSettingsFromMetadata = (
|
||||||
meta?: IMetadata,
|
meta?: IMetadata,
|
||||||
defaultSettings?: IReaderSettings,
|
defaultSettings?: IReaderSettings,
|
||||||
|
applyMetadataMigration?: boolean,
|
||||||
): IReaderSettings => ({
|
): IReaderSettings => ({
|
||||||
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings),
|
...getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getReaderSettingsFor = (
|
export const getReaderSettingsFor = (
|
||||||
{ meta }: IMetadataHolder,
|
{ meta }: IMetadataHolder,
|
||||||
defaultSettings?: IReaderSettings,
|
defaultSettings?: IReaderSettings,
|
||||||
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings);
|
applyMetadataMigration?: boolean,
|
||||||
|
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);
|
||||||
|
|
||||||
export const useDefaultReaderSettings = (): {
|
export const useDefaultReaderSettings = (): {
|
||||||
metadata?: IMetadata,
|
metadata?: IMetadata,
|
||||||
@@ -63,7 +67,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
|
|||||||
defaultSettings: IReaderSettings,
|
defaultSettings: IReaderSettings,
|
||||||
): Promise<void | void[]> => {
|
): Promise<void | void[]> => {
|
||||||
const meta = metadataHolder.meta ?? metadataHolder as IMetadata;
|
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 newSettings = getReaderSettingsFor({ meta }, defaultSettings);
|
||||||
|
|
||||||
const undefinedSettings = Object.entries(settingsToCheck)
|
const undefinedSettings = Object.entries(settingsToCheck)
|
||||||
|
|||||||
Reference in New Issue
Block a user