Move metadata files into new folder
This commit is contained in:
@@ -1,447 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {
|
|
||||||
AllowedMetadataValueTypes,
|
|
||||||
AppMetadataKeys,
|
|
||||||
GqlMetaHolder,
|
|
||||||
IMetadataMigration,
|
|
||||||
Metadata,
|
|
||||||
MetadataHolder,
|
|
||||||
MetadataKeyValuePair,
|
|
||||||
} from '@/typings.ts';
|
|
||||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
|
||||||
import { MetaType, SourceType } from '@/lib/graphql/generated/graphql.ts';
|
|
||||||
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
|
||||||
import { MangaIdInfo } from '@/modules/manga/services/Mangas.ts';
|
|
||||||
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
|
||||||
import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts';
|
|
||||||
|
|
||||||
const APP_METADATA_KEY_PREFIX = 'webUI_';
|
|
||||||
|
|
||||||
const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
|
||||||
// downloads
|
|
||||||
'deleteChaptersManuallyMarkedRead',
|
|
||||||
'deleteChaptersWhileReading',
|
|
||||||
'deleteChaptersWithBookmark',
|
|
||||||
|
|
||||||
// library
|
|
||||||
'showAddToLibraryCategorySelectDialog',
|
|
||||||
'ignoreFilters',
|
|
||||||
'removeMangaFromCategories',
|
|
||||||
'showTabSize',
|
|
||||||
|
|
||||||
// library category options
|
|
||||||
// filter
|
|
||||||
'hasDownloadedChapters',
|
|
||||||
'hasBookmarkedChapters',
|
|
||||||
'hasUnreadChapters',
|
|
||||||
'hasDuplicateChapters',
|
|
||||||
'hasTrackerBinding',
|
|
||||||
'hasStatus',
|
|
||||||
// sort
|
|
||||||
'sortBy',
|
|
||||||
'sortDesc',
|
|
||||||
// display
|
|
||||||
'showDownloadBadge',
|
|
||||||
'showUnreadBadge',
|
|
||||||
'showTabSize',
|
|
||||||
'showContinueReadingButton',
|
|
||||||
|
|
||||||
// client
|
|
||||||
'devices',
|
|
||||||
|
|
||||||
// migration
|
|
||||||
'migrateChapters',
|
|
||||||
'migrateCategories',
|
|
||||||
'migrateTracking',
|
|
||||||
'deleteChapters',
|
|
||||||
'migrateSortSettings',
|
|
||||||
|
|
||||||
// browse
|
|
||||||
'hideLibraryEntries',
|
|
||||||
|
|
||||||
// tracking
|
|
||||||
'updateProgressAfterReading',
|
|
||||||
'updateProgressManualMarkRead',
|
|
||||||
|
|
||||||
// updates
|
|
||||||
'webUIInformAvailableUpdate',
|
|
||||||
'serverInformAvailableUpdate',
|
|
||||||
|
|
||||||
// sources
|
|
||||||
'savedSearches',
|
|
||||||
|
|
||||||
// themes
|
|
||||||
'customThemes',
|
|
||||||
'mangaThumbnailBackdrop',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*
|
|
||||||
* @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',
|
|
||||||
* // },
|
|
||||||
* // ],
|
|
||||||
* },
|
|
||||||
* ];
|
|
||||||
*/
|
|
||||||
const migrations: IMetadataMigration[] = [
|
|
||||||
{
|
|
||||||
keys: [{ oldKey: 'loadNextonEnding', newKey: 'loadNextOnEnding' }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
keys: [{ oldKey: 'deleteChaptersAutoMarkedRead', newKey: 'deleteChaptersWhileReading' }],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const getAppKeyPrefixForMigration = (migrationId: number): string => {
|
|
||||||
const appKeyPrefix = migrations
|
|
||||||
.slice(0, migrationId)
|
|
||||||
.reverse()
|
|
||||||
.find((migration) => !!migration.appKeyPrefix);
|
|
||||||
|
|
||||||
return appKeyPrefix?.appKeyPrefix?.newPrefix ?? APP_METADATA_KEY_PREFIX;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => {
|
|
||||||
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys);
|
|
||||||
const addActiveDevicePrefix = !isGlobalMetadataKey && getActiveDevice() !== DEFAULT_DEVICE;
|
|
||||||
|
|
||||||
return `${appPrefix}${addActiveDevicePrefix ? `${getActiveDevice()}_` : ''}${key}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string, appPrefix?: string): boolean =>
|
|
||||||
Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
|
|
||||||
|
|
||||||
const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
|
|
||||||
value: string,
|
|
||||||
): T => {
|
|
||||||
if (!Number.isNaN(+value)) {
|
|
||||||
return +value as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 'true' || value === 'false') {
|
|
||||||
return (value === 'true') as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 'undefined') {
|
|
||||||
return undefined as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 'null') {
|
|
||||||
return null as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value as T;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
|
|
||||||
if (!gqlMetadata) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata: Metadata = {};
|
|
||||||
gqlMetadata.forEach(({ key, value }) => {
|
|
||||||
metadata[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const convertToGqlMeta = (metadata?: Metadata): MetaType[] | undefined => {
|
|
||||||
if (!metadata) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.entries(metadata).map(([key, value]) => ({ key, value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
|
|
||||||
const appMetadata: Metadata = {};
|
|
||||||
|
|
||||||
Object.entries(meta).forEach(([key, value]) => {
|
|
||||||
if (key.startsWith(appPrefix)) {
|
|
||||||
appMetadata[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return appMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyAppKeyPrefixMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
|
|
||||||
const migratedMetadata: Metadata = { ...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 applyMetadataValueMigration = (meta: Metadata, migration: IMetadataMigration, appKeyPrefix: string): Metadata => {
|
|
||||||
const migratedMetadata: Metadata = { ...meta };
|
|
||||||
|
|
||||||
if (!migration.values) {
|
|
||||||
return migratedMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appMetadata = getAppMetadataFrom(meta, appKeyPrefix);
|
|
||||||
const metadataValueChanges = migration.values;
|
|
||||||
|
|
||||||
metadataValueChanges.forEach(({ key, oldValue, newValue }) => {
|
|
||||||
const migrateValue = (metaKey: string) => {
|
|
||||||
if (meta[metaKey] === oldValue) {
|
|
||||||
migratedMetadata[metaKey] = newValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const migrateValueOfAllAppKeys = key === undefined;
|
|
||||||
if (migrateValueOfAllAppKeys) {
|
|
||||||
Object.keys(appMetadata).forEach((metaKey) => {
|
|
||||||
migrateValue(metaKey);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doesMetadataKeyExistIn(meta, key, appKeyPrefix)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
migrateValue(getMetadataKey(key, appKeyPrefix));
|
|
||||||
});
|
|
||||||
|
|
||||||
return migratedMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyMetadataKeyMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
|
|
||||||
const migratedMetadata: Metadata = { ...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?: Metadata): Metadata | undefined => {
|
|
||||||
if (!meta) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const migrationToMetadata: [number, Metadata][] = [[0, meta]];
|
|
||||||
|
|
||||||
migrations.forEach((migration, index) => {
|
|
||||||
const migrationId = index + 1;
|
|
||||||
const metadataToMigrate = migrationToMetadata[migrationId - 1][1];
|
|
||||||
const appKeyPrefixMigrated = applyAppKeyPrefixMigration(metadataToMigrate, migration);
|
|
||||||
const metadataValuesMigrated = applyMetadataValueMigration(
|
|
||||||
appKeyPrefixMigrated,
|
|
||||||
migration,
|
|
||||||
getAppKeyPrefixForMigration(migrationId),
|
|
||||||
);
|
|
||||||
const metadataKeysMigrated = applyMetadataKeyMigration(metadataValuesMigrated, migration);
|
|
||||||
|
|
||||||
migrationToMetadata.push([migrationId, metadataKeysMigrated]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const appliedMigration = migrationToMetadata.length > 1;
|
|
||||||
if (!appliedMigration) {
|
|
||||||
return { ...meta };
|
|
||||||
}
|
|
||||||
|
|
||||||
return migrationToMetadata.pop()![1];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
|
|
||||||
{ meta }: MetadataHolder,
|
|
||||||
key: Key,
|
|
||||||
defaultValue?: Value,
|
|
||||||
applyMigrations: boolean = true,
|
|
||||||
): Value | undefined => {
|
|
||||||
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
|
|
||||||
|
|
||||||
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
|
||||||
{ meta }: MetadataHolder,
|
|
||||||
metadataWithDefaultValues: METADATA,
|
|
||||||
applyMigrations?: boolean,
|
|
||||||
): METADATA => {
|
|
||||||
const appMetadata = {} as METADATA;
|
|
||||||
|
|
||||||
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
|
|
||||||
appMetadata[key as AppMetadataKeys] = getMetadataValueFrom(
|
|
||||||
{ meta },
|
|
||||||
key as AppMetadataKeys,
|
|
||||||
defaultValue,
|
|
||||||
applyMigrations,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return appMetadata;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHolder => {
|
|
||||||
if (wrap) {
|
|
||||||
return {
|
|
||||||
meta: {
|
|
||||||
...metadata,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...metadata,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global' | 'source';
|
|
||||||
|
|
||||||
export const requestUpdateMetadataValue = async (
|
|
||||||
metadataHolder: GqlMetaHolder,
|
|
||||||
holderType: MetadataHolderType,
|
|
||||||
key: AppMetadataKeys,
|
|
||||||
value: AllowedMetadataValueTypes,
|
|
||||||
): Promise<void> => {
|
|
||||||
const metadataKey = getMetadataKey(key);
|
|
||||||
|
|
||||||
switch (holderType) {
|
|
||||||
case 'category':
|
|
||||||
await requestManager.setCategoryMeta((metadataHolder as CategoryIdInfo).id, metadataKey, value).response;
|
|
||||||
break;
|
|
||||||
case 'chapter':
|
|
||||||
await requestManager.setChapterMeta((metadataHolder as ChapterIdInfo).id, metadataKey, value).response;
|
|
||||||
break;
|
|
||||||
case 'global':
|
|
||||||
await requestManager.setGlobalMetadata(metadataKey, value).response;
|
|
||||||
break;
|
|
||||||
case 'manga':
|
|
||||||
await requestManager.setMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey, value).response;
|
|
||||||
break;
|
|
||||||
case 'source':
|
|
||||||
await requestManager.setSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey, value)
|
|
||||||
.response;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requestUpdateMetadata = async (
|
|
||||||
metadataHolder: GqlMetaHolder,
|
|
||||||
holderType: MetadataHolderType,
|
|
||||||
keysToValues: MetadataKeyValuePair[],
|
|
||||||
): Promise<void[]> =>
|
|
||||||
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
|
|
||||||
|
|
||||||
export const requestUpdateServerMetadata = async (keysToValues: MetadataKeyValuePair[]): Promise<void[]> =>
|
|
||||||
requestUpdateMetadata({}, 'global', keysToValues);
|
|
||||||
|
|
||||||
export const requestUpdateMangaMetadata = async (
|
|
||||||
manga: MangaIdInfo & GqlMetaHolder,
|
|
||||||
keysToValues: MetadataKeyValuePair[],
|
|
||||||
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues);
|
|
||||||
|
|
||||||
export const requestUpdateChapterMetadata = async (
|
|
||||||
chapter: ChapterIdInfo & GqlMetaHolder,
|
|
||||||
keysToValues: MetadataKeyValuePair[],
|
|
||||||
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues);
|
|
||||||
|
|
||||||
export const requestUpdateCategoryMetadata = async (
|
|
||||||
category: CategoryIdInfo & GqlMetaHolder,
|
|
||||||
keysToValues: MetadataKeyValuePair[],
|
|
||||||
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues);
|
|
||||||
|
|
||||||
export const requestUpdateSourceMetadata = async (
|
|
||||||
source: Pick<SourceType, 'id'> & GqlMetaHolder,
|
|
||||||
keysToValue: MetadataKeyValuePair[],
|
|
||||||
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue);
|
|
||||||
@@ -6,13 +6,20 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AllowedMetadataValueTypes, AppMetadataKeys, GqlMetaHolder, Metadata } from '@/typings.ts';
|
|
||||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||||
import { convertFromGqlMeta, getMetadataFrom, requestUpdateCategoryMetadata } from '@/lib/metadata/metadata.ts';
|
import { requestUpdateCategoryMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||||
import { CategoryIdInfo, CategoryMetadataKeys, ICategoryMetadata } from '@/modules/category/Category.types.ts';
|
import { CategoryIdInfo, CategoryMetadataKeys, ICategoryMetadata } from '@/modules/category/Category.types.ts';
|
||||||
|
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||||
|
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
|
||||||
|
import {
|
||||||
|
AllowedMetadataValueTypes,
|
||||||
|
AppMetadataKeys,
|
||||||
|
GqlMetaHolder,
|
||||||
|
Metadata,
|
||||||
|
} from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
export const getDefaultCategoryMetadata = (): ICategoryMetadata => ({
|
export const getDefaultCategoryMetadata = (): ICategoryMetadata => ({
|
||||||
// display options
|
// display options
|
||||||
|
|||||||
133
src/modules/metadata/Metadata.constants.ts
Normal file
133
src/modules/metadata/Metadata.constants.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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 { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
|
export const APP_METADATA_KEY_PREFIX = 'webUI_';
|
||||||
|
|
||||||
|
export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
||||||
|
// downloads
|
||||||
|
'deleteChaptersManuallyMarkedRead',
|
||||||
|
'deleteChaptersWhileReading',
|
||||||
|
'deleteChaptersWithBookmark',
|
||||||
|
|
||||||
|
// library
|
||||||
|
'showAddToLibraryCategorySelectDialog',
|
||||||
|
'ignoreFilters',
|
||||||
|
'removeMangaFromCategories',
|
||||||
|
'showTabSize',
|
||||||
|
|
||||||
|
// library category options
|
||||||
|
// filter
|
||||||
|
'hasDownloadedChapters',
|
||||||
|
'hasBookmarkedChapters',
|
||||||
|
'hasUnreadChapters',
|
||||||
|
'hasDuplicateChapters',
|
||||||
|
'hasTrackerBinding',
|
||||||
|
'hasStatus',
|
||||||
|
// sort
|
||||||
|
'sortBy',
|
||||||
|
'sortDesc',
|
||||||
|
// display
|
||||||
|
'showDownloadBadge',
|
||||||
|
'showUnreadBadge',
|
||||||
|
'showTabSize',
|
||||||
|
'showContinueReadingButton',
|
||||||
|
|
||||||
|
// client
|
||||||
|
'devices',
|
||||||
|
|
||||||
|
// migration
|
||||||
|
'migrateChapters',
|
||||||
|
'migrateCategories',
|
||||||
|
'migrateTracking',
|
||||||
|
'deleteChapters',
|
||||||
|
'migrateSortSettings',
|
||||||
|
|
||||||
|
// browse
|
||||||
|
'hideLibraryEntries',
|
||||||
|
|
||||||
|
// tracking
|
||||||
|
'updateProgressAfterReading',
|
||||||
|
'updateProgressManualMarkRead',
|
||||||
|
|
||||||
|
// updates
|
||||||
|
'webUIInformAvailableUpdate',
|
||||||
|
'serverInformAvailableUpdate',
|
||||||
|
|
||||||
|
// sources
|
||||||
|
'savedSearches',
|
||||||
|
|
||||||
|
// themes
|
||||||
|
'customThemes',
|
||||||
|
'mangaThumbnailBackdrop',
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @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' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
import { MetadataServerSettingKeys, SearchMetadataKeys } from '@/modules/settings/Settings.types.ts';
|
||||||
import { MangaMetadataKeys } from '@/modules/manga/MangaCard.types.tsx';
|
import { MangaMetadataKeys } from '@/modules/manga/MangaCard.types.tsx';
|
||||||
import { SourceMetadataKeys } from '@/modules/source/Source.types.ts';
|
import { SourceMetadataKeys } from '@/modules/source/Source.types.ts';
|
||||||
import { CategoryMetadataKeys } from '@/modules/category/Category.types.ts';
|
import { CategoryMetadataKeys } from '@/modules/category/Category.types.ts';
|
||||||
import { MetadataServerSettingKeys, SearchMetadataKeys } from '@/modules/settings/Settings.types.ts';
|
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
|
||||||
export interface IMetadataMigration {
|
export interface IMetadataMigration {
|
||||||
appKeyPrefix?: { oldPrefix: string; newPrefix: string };
|
appKeyPrefix?: { oldPrefix: string; newPrefix: string };
|
||||||
53
src/modules/metadata/services/MetadataConverter.ts
Normal file
53
src/modules/metadata/services/MetadataConverter.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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 { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { AllowedMetadataValueTypes, Metadata } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
|
export const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedMetadataValueTypes>(
|
||||||
|
value: string,
|
||||||
|
): T => {
|
||||||
|
if (!Number.isNaN(+value)) {
|
||||||
|
return +value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 'true' || value === 'false') {
|
||||||
|
return (value === 'true') as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 'undefined') {
|
||||||
|
return undefined as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === 'null') {
|
||||||
|
return null as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
|
||||||
|
if (!gqlMetadata) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata: Metadata = {};
|
||||||
|
gqlMetadata.forEach(({ key, value }) => {
|
||||||
|
metadata[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToGqlMeta = (metadata?: Metadata): MetaType[] | undefined => {
|
||||||
|
if (!metadata) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(metadata).map(([key, value]) => ({ key, value }));
|
||||||
|
};
|
||||||
136
src/modules/metadata/services/MetadataMigrations.ts
Normal file
136
src/modules/metadata/services/MetadataMigrations.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/*
|
||||||
|
* 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 { APP_METADATA_KEY_PREFIX, METADATA_MIGRATIONS } from '@/modules/metadata/Metadata.constants.ts';
|
||||||
|
import {
|
||||||
|
doesMetadataKeyExistIn,
|
||||||
|
getAppMetadataFrom,
|
||||||
|
getMetadataKey,
|
||||||
|
} from '@/modules/metadata/services/MetadataReader.ts';
|
||||||
|
import { IMetadataMigration, Metadata } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
|
export const getAppKeyPrefixForMigration = (migrationId: number): string => {
|
||||||
|
const appKeyPrefix = METADATA_MIGRATIONS.slice(0, migrationId)
|
||||||
|
.reverse()
|
||||||
|
.find((migration) => !!migration.appKeyPrefix);
|
||||||
|
|
||||||
|
return appKeyPrefix?.appKeyPrefix?.newPrefix ?? APP_METADATA_KEY_PREFIX;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyAppKeyPrefixMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
|
||||||
|
const migratedMetadata: Metadata = { ...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 applyMetadataValueMigration = (meta: Metadata, migration: IMetadataMigration, appKeyPrefix: string): Metadata => {
|
||||||
|
const migratedMetadata: Metadata = { ...meta };
|
||||||
|
|
||||||
|
if (!migration.values) {
|
||||||
|
return migratedMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appMetadata = getAppMetadataFrom(meta, appKeyPrefix);
|
||||||
|
const metadataValueChanges = migration.values;
|
||||||
|
|
||||||
|
metadataValueChanges.forEach(({ key, oldValue, newValue }) => {
|
||||||
|
const migrateValue = (metaKey: string) => {
|
||||||
|
if (meta[metaKey] === oldValue) {
|
||||||
|
migratedMetadata[metaKey] = newValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const migrateValueOfAllAppKeys = key === undefined;
|
||||||
|
if (migrateValueOfAllAppKeys) {
|
||||||
|
Object.keys(appMetadata).forEach((metaKey) => {
|
||||||
|
migrateValue(metaKey);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doesMetadataKeyExistIn(meta, key, appKeyPrefix)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateValue(getMetadataKey(key, appKeyPrefix));
|
||||||
|
});
|
||||||
|
|
||||||
|
return migratedMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyMetadataKeyMigration = (meta: Metadata, migration: IMetadataMigration): Metadata => {
|
||||||
|
const migratedMetadata: Metadata = { ...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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const applyMetadataMigrations = (meta?: Metadata): Metadata | undefined => {
|
||||||
|
if (!meta) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationToMetadata: [number, Metadata][] = [[0, meta]];
|
||||||
|
|
||||||
|
METADATA_MIGRATIONS.forEach((migration, index) => {
|
||||||
|
const migrationId = index + 1;
|
||||||
|
const metadataToMigrate = migrationToMetadata[migrationId - 1][1];
|
||||||
|
const appKeyPrefixMigrated = applyAppKeyPrefixMigration(metadataToMigrate, migration);
|
||||||
|
const metadataValuesMigrated = applyMetadataValueMigration(
|
||||||
|
appKeyPrefixMigrated,
|
||||||
|
migration,
|
||||||
|
getAppKeyPrefixForMigration(migrationId),
|
||||||
|
);
|
||||||
|
const metadataKeysMigrated = applyMetadataKeyMigration(metadataValuesMigrated, migration);
|
||||||
|
|
||||||
|
migrationToMetadata.push([migrationId, metadataKeysMigrated]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const appliedMigration = migrationToMetadata.length > 1;
|
||||||
|
if (!appliedMigration) {
|
||||||
|
return { ...meta };
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrationToMetadata.pop()![1];
|
||||||
|
};
|
||||||
74
src/modules/metadata/services/MetadataReader.ts
Normal file
74
src/modules/metadata/services/MetadataReader.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* 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 { APP_METADATA_KEY_PREFIX, GLOBAL_METADATA_KEYS } from '@/modules/metadata/Metadata.constants.ts';
|
||||||
|
import { DEFAULT_DEVICE, getActiveDevice } from '@/modules/device/services/Device.ts';
|
||||||
|
import { applyMetadataMigrations } from '@/modules/metadata/services/MetadataMigrations.ts';
|
||||||
|
import { convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||||
|
import {
|
||||||
|
AllowedMetadataValueTypes,
|
||||||
|
AppMetadataKeys,
|
||||||
|
Metadata,
|
||||||
|
MetadataHolder,
|
||||||
|
} from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
|
export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => {
|
||||||
|
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys);
|
||||||
|
const addActiveDevicePrefix = !isGlobalMetadataKey && getActiveDevice() !== DEFAULT_DEVICE;
|
||||||
|
|
||||||
|
return `${appPrefix}${addActiveDevicePrefix ? `${getActiveDevice()}_` : ''}${key}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string, appPrefix?: string): boolean =>
|
||||||
|
Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, appPrefix));
|
||||||
|
|
||||||
|
export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
|
||||||
|
{ meta }: MetadataHolder,
|
||||||
|
key: Key,
|
||||||
|
defaultValue?: Value,
|
||||||
|
applyMigrations: boolean = true,
|
||||||
|
): Value | undefined => {
|
||||||
|
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
|
||||||
|
|
||||||
|
if (metadata === undefined || !doesMetadataKeyExistIn(metadata, key)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||||
|
{ meta }: MetadataHolder,
|
||||||
|
metadataWithDefaultValues: METADATA,
|
||||||
|
applyMigrations?: boolean,
|
||||||
|
): METADATA => {
|
||||||
|
const appMetadata = {} as METADATA;
|
||||||
|
|
||||||
|
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
|
||||||
|
appMetadata[key as AppMetadataKeys] = getMetadataValueFrom(
|
||||||
|
{ meta },
|
||||||
|
key as AppMetadataKeys,
|
||||||
|
defaultValue,
|
||||||
|
applyMigrations,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return appMetadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
|
||||||
|
const appMetadata: Metadata = {};
|
||||||
|
|
||||||
|
Object.entries(meta).forEach(([key, value]) => {
|
||||||
|
if (key.startsWith(appPrefix)) {
|
||||||
|
appMetadata[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return appMetadata;
|
||||||
|
};
|
||||||
82
src/modules/metadata/services/MetadataUpdater.ts
Normal file
82
src/modules/metadata/services/MetadataUpdater.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* 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 { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||||
|
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
||||||
|
import { MangaIdInfo } from '@/modules/manga/services/Mangas.ts';
|
||||||
|
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||||
|
import { getMetadataKey } from '@/modules/metadata/services/MetadataReader.ts';
|
||||||
|
import {
|
||||||
|
AllowedMetadataValueTypes,
|
||||||
|
AppMetadataKeys,
|
||||||
|
GqlMetaHolder,
|
||||||
|
MetadataKeyValuePair,
|
||||||
|
} from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
|
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global' | 'source';
|
||||||
|
|
||||||
|
export const requestUpdateMetadataValue = async (
|
||||||
|
metadataHolder: GqlMetaHolder,
|
||||||
|
holderType: MetadataHolderType,
|
||||||
|
key: AppMetadataKeys,
|
||||||
|
value: AllowedMetadataValueTypes,
|
||||||
|
): Promise<void> => {
|
||||||
|
const metadataKey = getMetadataKey(key);
|
||||||
|
|
||||||
|
switch (holderType) {
|
||||||
|
case 'category':
|
||||||
|
await requestManager.setCategoryMeta((metadataHolder as CategoryIdInfo).id, metadataKey, value).response;
|
||||||
|
break;
|
||||||
|
case 'chapter':
|
||||||
|
await requestManager.setChapterMeta((metadataHolder as ChapterIdInfo).id, metadataKey, value).response;
|
||||||
|
break;
|
||||||
|
case 'global':
|
||||||
|
await requestManager.setGlobalMetadata(metadataKey, value).response;
|
||||||
|
break;
|
||||||
|
case 'manga':
|
||||||
|
await requestManager.setMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey, value).response;
|
||||||
|
break;
|
||||||
|
case 'source':
|
||||||
|
await requestManager.setSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey, value)
|
||||||
|
.response;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestUpdateMetadata = async (
|
||||||
|
metadataHolder: GqlMetaHolder,
|
||||||
|
holderType: MetadataHolderType,
|
||||||
|
keysToValues: MetadataKeyValuePair[],
|
||||||
|
): Promise<void[]> =>
|
||||||
|
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
|
||||||
|
|
||||||
|
export const requestUpdateServerMetadata = async (keysToValues: MetadataKeyValuePair[]): Promise<void[]> =>
|
||||||
|
requestUpdateMetadata({}, 'global', keysToValues);
|
||||||
|
|
||||||
|
export const requestUpdateMangaMetadata = async (
|
||||||
|
manga: MangaIdInfo & GqlMetaHolder,
|
||||||
|
keysToValues: MetadataKeyValuePair[],
|
||||||
|
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues);
|
||||||
|
|
||||||
|
export const requestUpdateChapterMetadata = async (
|
||||||
|
chapter: ChapterIdInfo & GqlMetaHolder,
|
||||||
|
keysToValues: MetadataKeyValuePair[],
|
||||||
|
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues);
|
||||||
|
|
||||||
|
export const requestUpdateCategoryMetadata = async (
|
||||||
|
category: CategoryIdInfo & GqlMetaHolder,
|
||||||
|
keysToValues: MetadataKeyValuePair[],
|
||||||
|
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues);
|
||||||
|
|
||||||
|
export const requestUpdateSourceMetadata = async (
|
||||||
|
source: Pick<SourceType, 'id'> & GqlMetaHolder,
|
||||||
|
keysToValue: MetadataKeyValuePair[],
|
||||||
|
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue);
|
||||||
@@ -27,7 +27,6 @@ import ListItem from '@mui/material/ListItem';
|
|||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AllowedMetadataValueTypes } from '@/typings.ts';
|
|
||||||
import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx';
|
import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx';
|
||||||
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
|
||||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
||||||
@@ -39,6 +38,7 @@ import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
|||||||
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
import { CustomIconButton } from '@/modules/core/components/buttons/CustomIconButton.tsx';
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
||||||
import { DirectionOffset } from '@/Base.types.ts';
|
import { DirectionOffset } from '@/Base.types.ts';
|
||||||
|
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
const Root = styled('div')({
|
const Root = styled('div')({
|
||||||
zIndex: 10,
|
zIndex: 10,
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import ListItemText from '@mui/material/ListItemText';
|
|||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import MenuItem from '@mui/material/MenuItem';
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AllowedMetadataValueTypes } from '@/typings.ts';
|
|
||||||
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx';
|
||||||
import { isHorizontalReaderType } from '@/modules/reader/components/page/Page.tsx';
|
import { isHorizontalReaderType } from '@/modules/reader/components/page/Page.tsx';
|
||||||
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
import { Select } from '@/modules/core/components/inputs/Select.tsx';
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
||||||
|
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
interface IProps extends IReaderSettings {
|
interface IProps extends IReaderSettings {
|
||||||
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
|
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
|
|
||||||
import { useContext, useLayoutEffect } from 'react';
|
import { useContext, useLayoutEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AllowedMetadataValueTypes } from '@/typings.ts';
|
import { requestUpdateServerMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||||
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/lib/metadata/metadata.ts';
|
|
||||||
import {
|
import {
|
||||||
checkAndHandleMissingStoredReaderSettings,
|
checkAndHandleMissingStoredReaderSettings,
|
||||||
useDefaultReaderSettings,
|
useDefaultReaderSettings,
|
||||||
@@ -22,6 +21,8 @@ import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder
|
|||||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.ts';
|
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.ts';
|
||||||
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
import { IReaderSettings } from '@/modules/reader/Reader.types.ts';
|
||||||
|
import { convertToGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||||
|
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
export function DefaultReaderSettings() {
|
export function DefaultReaderSettings() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ import { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, u
|
|||||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AllowedMetadataValueTypes } from '@/typings.ts';
|
|
||||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||||
import {
|
import {
|
||||||
checkAndHandleMissingStoredReaderSettings,
|
checkAndHandleMissingStoredReaderSettings,
|
||||||
getReaderSettingsFor,
|
getReaderSettingsFor,
|
||||||
useDefaultReaderSettings,
|
useDefaultReaderSettings,
|
||||||
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||||
import { requestUpdateMangaMetadata } from '@/lib/metadata/metadata.ts';
|
import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||||
import { HorizontalPager } from '@/modules/reader/components/pager/HorizontalPager.tsx';
|
import { HorizontalPager } from '@/modules/reader/components/pager/HorizontalPager.tsx';
|
||||||
import { PageNumber } from '@/modules/reader/components/page/PageNumber.tsx';
|
import { PageNumber } from '@/modules/reader/components/page/PageNumber.tsx';
|
||||||
import { PagedPager } from '@/modules/reader/components/pager/PagedPager.tsx';
|
import { PagedPager } from '@/modules/reader/components/pager/PagedPager.tsx';
|
||||||
@@ -45,6 +44,7 @@ import { CHAPTER_READER_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.
|
|||||||
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
|
||||||
import { IReaderSettings, ReaderType } from '@/modules/reader/Reader.types.ts';
|
import { IReaderSettings, ReaderType } from '@/modules/reader/Reader.types.ts';
|
||||||
import { DirectionOffset } from '@/Base.types.ts';
|
import { DirectionOffset } from '@/Base.types.ts';
|
||||||
|
import { AllowedMetadataValueTypes } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
type TChapter = GetChaptersReaderQuery['chapters']['nodes'][number];
|
type TChapter = GetChaptersReaderQuery['chapters']['nodes'][number];
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,18 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { GqlMetaHolder, Metadata, MetadataKeyValuePair } from '@/typings.ts';
|
|
||||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||||
import {
|
import {
|
||||||
convertFromGqlMeta,
|
|
||||||
getMetadataFrom,
|
|
||||||
requestUpdateMangaMetadata,
|
requestUpdateMangaMetadata,
|
||||||
requestUpdateServerMetadata,
|
requestUpdateServerMetadata,
|
||||||
} from '@/lib/metadata/metadata.ts';
|
} from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||||
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { MangaIdInfo } from '@/modules/manga/services/Mangas.ts';
|
import { MangaIdInfo } from '@/modules/manga/services/Mangas.ts';
|
||||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.ts';
|
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.ts';
|
||||||
import { IReaderSettings, UndefinedReaderSettings } from '@/modules/reader/Reader.types.ts';
|
import { IReaderSettings, UndefinedReaderSettings } from '@/modules/reader/Reader.types.ts';
|
||||||
|
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||||
|
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
|
||||||
|
import { GqlMetaHolder, Metadata, MetadataKeyValuePair } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
const getReaderSettingsWithDefaultValueFallback = <DefaultSettings extends IReaderSettings | UndefinedReaderSettings>(
|
const getReaderSettingsWithDefaultValueFallback = <DefaultSettings extends IReaderSettings | UndefinedReaderSettings>(
|
||||||
meta?: Metadata,
|
meta?: Metadata,
|
||||||
|
|||||||
@@ -6,15 +6,17 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AllowedMetadataValueTypes, AppMetadataKeys, Metadata } from '@/typings.ts';
|
|
||||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||||
import { convertFromGqlMeta, getMetadataFrom, requestUpdateServerMetadata } from '@/lib/metadata/metadata.ts';
|
import { requestUpdateServerMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { MetadataMigrationSettings } from '@/modules/migration/Migration.types.ts';
|
import { MetadataMigrationSettings } from '@/modules/migration/Migration.types.ts';
|
||||||
import { MetadataThemeSettings } from '@/modules/theme/AppTheme.types.ts';
|
import { MetadataThemeSettings } from '@/modules/theme/AppTheme.types.ts';
|
||||||
import { SERVER_SETTINGS_METADATA_DEFAULT } from '@/modules/settings/Settings.constants.ts';
|
import { SERVER_SETTINGS_METADATA_DEFAULT } from '@/modules/settings/Settings.constants.ts';
|
||||||
import { MetadataServerSettingKeys, MetadataServerSettings } from '@/modules/settings/Settings.types.ts';
|
import { MetadataServerSettingKeys, MetadataServerSettings } from '@/modules/settings/Settings.types.ts';
|
||||||
|
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||||
|
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
|
||||||
|
import { AllowedMetadataValueTypes, AppMetadataKeys, Metadata } from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
export const convertSettingsToMetadata = (
|
export const convertSettingsToMetadata = (
|
||||||
settings: Partial<MetadataServerSettings>,
|
settings: Partial<MetadataServerSettings>,
|
||||||
|
|||||||
@@ -6,12 +6,19 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AllowedMetadataValueTypes, AppMetadataKeys, GqlMetaHolder, Metadata } from '@/typings.ts';
|
|
||||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||||
import { convertFromGqlMeta, getMetadataFrom, requestUpdateSourceMetadata } from '@/lib/metadata/metadata.ts';
|
import { requestUpdateSourceMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||||
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { ISourceMetadata, SourceMetadataKeys } from '@/modules/source/Source.types.ts';
|
import { ISourceMetadata, SourceMetadataKeys } from '@/modules/source/Source.types.ts';
|
||||||
|
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||||
|
import { getMetadataFrom } from '@/modules/metadata/services/MetadataReader.ts';
|
||||||
|
import {
|
||||||
|
AllowedMetadataValueTypes,
|
||||||
|
AppMetadataKeys,
|
||||||
|
GqlMetaHolder,
|
||||||
|
Metadata,
|
||||||
|
} from '@/modules/metadata/Metadata.types.ts';
|
||||||
|
|
||||||
const convertAppMetadataToGqlMetadata = (
|
const convertAppMetadataToGqlMetadata = (
|
||||||
metadata: Partial<ISourceMetadata>,
|
metadata: Partial<ISourceMetadata>,
|
||||||
|
|||||||
Reference in New Issue
Block a user