@@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- (**Browse**) Merge languages filter of the sources and extensions into one
|
||||
|
||||
### Fixed
|
||||
- (**General**) Fix saving large client data on the server (e.g., custom source filters)
|
||||
- (**Library**) Fix total library size chip color in light mode
|
||||
- (**Browse**) Fix missing pinned sources in the source language filter
|
||||
- (**Browse**) Fix incorrectly showing "local source" source in the source language filter (the local source can't be disabled)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { GqlMetaHolder } from '@/features/metadata/Metadata.types.ts';
|
||||
|
||||
export const FALLBACK_MANGA: MangaIdInfo & GqlMetaHolder = { id: -1 };
|
||||
|
||||
export const GLOBAL_READER_SETTINGS_MANGA: MangaIdInfo = { id: -2 };
|
||||
export const GLOBAL_READER_SETTINGS_MANGA: MangaIdInfo & GqlMetaHolder = { id: -2 };
|
||||
|
||||
export const MANGA_COVER_ASPECT_RATIO = '1 / 1.5';
|
||||
|
||||
|
||||
@@ -37,9 +37,12 @@ export const getMetadataKey = (key: string, prefixes: string[] = [], appPrefix:
|
||||
return `${finalPrefix.join('_')}_${key}`;
|
||||
};
|
||||
|
||||
export const doesMetadataKeyExistIn = (
|
||||
export const doesAppMetadataKeyExistIn = (
|
||||
meta: Metadata | undefined,
|
||||
key: string,
|
||||
prefixes?: string[],
|
||||
appPrefix?: string,
|
||||
): boolean => Object.prototype.hasOwnProperty.call(meta ?? {}, getMetadataKey(key, prefixes, appPrefix));
|
||||
|
||||
export const doesMetadataKeyExistIn = (meta: Metadata | undefined, key: string): boolean =>
|
||||
Object.prototype.hasOwnProperty.call(meta ?? {}, key);
|
||||
|
||||
213
src/features/metadata/services/MetadataChunker.ts
Normal file
213
src/features/metadata/services/MetadataChunker.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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 { GqlMetaHolder, Metadata, MetadataHolderType } from '@/features/metadata/Metadata.types.ts';
|
||||
import { convertFromGqlMeta } from '@/features/metadata/services/MetadataConverter.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { GET_GLOBAL_METADATAS } from '@/lib/graphql/metadata/GlobalMetadataQuery.ts';
|
||||
import {
|
||||
GetGlobalMetadatasQuery,
|
||||
GetGlobalMetadatasQueryVariables,
|
||||
MetaInput,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
import { doesMetadataKeyExistIn } from '@/features/metadata/Metadata.utils.ts';
|
||||
|
||||
export class MetadataChunker {
|
||||
static readonly MAX_METADATA_VALUE_LENGTH = 4000;
|
||||
|
||||
static getChunkLengthKey(fullKey: string): string {
|
||||
return `${fullKey}_length`;
|
||||
}
|
||||
|
||||
static getChunkIndexKey(fullKey: string, index: number): string {
|
||||
return `${fullKey}_${index}`;
|
||||
}
|
||||
|
||||
static isChunkedInMetadata(metadata: Metadata | undefined, fullKey: string): boolean {
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return doesMetadataKeyExistIn(metadata, this.getChunkLengthKey(fullKey));
|
||||
}
|
||||
|
||||
static getExistingChunkCount(metadata: Metadata | undefined, fullKey: string): number {
|
||||
if (!metadata) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const lengthKey = this.getChunkLengthKey(fullKey);
|
||||
if (!doesMetadataKeyExistIn(metadata, lengthKey)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const count = Number(metadata[lengthKey]);
|
||||
|
||||
return Number.isNaN(count) ? 0 : count;
|
||||
}
|
||||
|
||||
static getStaleChunkKeyCount(metadata: Metadata | undefined, fullKey: string): number {
|
||||
if (!metadata) return 0;
|
||||
|
||||
const scanFrom = (index: number): number =>
|
||||
doesMetadataKeyExistIn(metadata, this.getChunkIndexKey(fullKey, index)) ? scanFrom(index + 1) : index;
|
||||
|
||||
return scanFrom(this.getExistingChunkCount(metadata, fullKey));
|
||||
}
|
||||
|
||||
static reassembleChunkedValue(metadata: Metadata, fullKey: string): string | undefined {
|
||||
const chunkCount = this.getExistingChunkCount(metadata, fullKey);
|
||||
|
||||
if (chunkCount <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Array(chunkCount)
|
||||
.fill(1)
|
||||
.reduce((acc, _, index) => {
|
||||
if (acc === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chunkKey = this.getChunkIndexKey(fullKey, index);
|
||||
|
||||
if (!doesMetadataKeyExistIn(metadata, chunkKey)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${acc}${metadata[chunkKey]}`;
|
||||
}, '');
|
||||
}
|
||||
|
||||
static reassembleAllChunkedValues(metadata: Metadata | undefined): Metadata | undefined {
|
||||
if (metadata === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chunkedKeys = new Set<string>();
|
||||
|
||||
const reassembledEntries = Object.entries(metadata)
|
||||
.map(([key]) => {
|
||||
if (!key.endsWith('_length')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseKey = key.slice(0, -'_length'.length);
|
||||
const count = Number(metadata[key]);
|
||||
|
||||
if (Number.isNaN(count) || count <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reassembledValue = this.reassembleChunkedValue(metadata, baseKey);
|
||||
|
||||
if (reassembledValue === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
chunkedKeys.add(key);
|
||||
for (let i = 0; i < count; i++) {
|
||||
chunkedKeys.add(this.getChunkIndexKey(baseKey, i));
|
||||
}
|
||||
|
||||
return [baseKey, reassembledValue];
|
||||
})
|
||||
.filter((entry): entry is [string, string] => entry !== null);
|
||||
|
||||
const nonChunkedEntries = Object.entries(metadata)
|
||||
.map(([key, value]) => {
|
||||
if (chunkedKeys.has(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
})
|
||||
.filter((entry): entry is [string, string] => entry !== null);
|
||||
|
||||
const reassembledMetadata = Object.fromEntries([...nonChunkedEntries, ...reassembledEntries]);
|
||||
|
||||
return reassembledMetadata;
|
||||
}
|
||||
|
||||
static chunkValue(fullKey: string, value: string): MetaInput[] {
|
||||
if (value.length <= this.MAX_METADATA_VALUE_LENGTH) {
|
||||
return [{ key: fullKey, value }];
|
||||
}
|
||||
|
||||
const entries: MetaInput[] = [];
|
||||
let offset = 0;
|
||||
let index = 0;
|
||||
|
||||
while (offset < value.length) {
|
||||
const chunk = value.substring(offset, offset + this.MAX_METADATA_VALUE_LENGTH);
|
||||
|
||||
entries.push({ key: this.getChunkIndexKey(fullKey, index), value: chunk });
|
||||
|
||||
offset += this.MAX_METADATA_VALUE_LENGTH;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
entries.push({ key: this.getChunkLengthKey(fullKey), value: `${index}` });
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
static computeChunkDeletions(
|
||||
existingMetadata: Metadata | undefined,
|
||||
fullKey: string,
|
||||
newChunkCount: number,
|
||||
): string[] {
|
||||
if (!existingMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isNowChunked = newChunkCount > 0;
|
||||
const hasDirectKey = doesMetadataKeyExistIn(existingMetadata, fullKey);
|
||||
const hasLengthKey = doesMetadataKeyExistIn(existingMetadata, this.getChunkLengthKey(fullKey));
|
||||
const staleChunkKeyCount = this.getStaleChunkKeyCount(existingMetadata, fullKey);
|
||||
|
||||
const keysToDelete: string[] = [];
|
||||
|
||||
const changedToChunkedFromUnchunked = hasDirectKey && isNowChunked;
|
||||
if (changedToChunkedFromUnchunked) {
|
||||
keysToDelete.push(fullKey);
|
||||
}
|
||||
|
||||
const changedToUnchunkedFromChunked = hasLengthKey && !isNowChunked;
|
||||
if (changedToUnchunkedFromChunked) {
|
||||
keysToDelete.push(this.getChunkLengthKey(fullKey));
|
||||
}
|
||||
|
||||
// Delete stale chunk index keys
|
||||
const startIndex = isNowChunked ? newChunkCount : 0;
|
||||
for (let i = startIndex; i < staleChunkKeyCount; i++) {
|
||||
keysToDelete.push(this.getChunkIndexKey(fullKey, i));
|
||||
}
|
||||
|
||||
return keysToDelete;
|
||||
}
|
||||
|
||||
static getExistingMetadata(metadataHolder: GqlMetaHolder, holderType: MetadataHolderType): Metadata | undefined {
|
||||
if (holderType === 'global' && metadataHolder.meta === undefined) {
|
||||
const cached = requestManager.graphQLClient.client.readQuery<
|
||||
GetGlobalMetadatasQuery,
|
||||
GetGlobalMetadatasQueryVariables
|
||||
>({
|
||||
query: GET_GLOBAL_METADATAS,
|
||||
});
|
||||
|
||||
if (!cached) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return convertFromGqlMeta(cached.metas.nodes);
|
||||
}
|
||||
|
||||
return convertFromGqlMeta(metadataHolder.meta);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||
import { CategoryIdInfo } from '@/features/category/Category.types.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { getMetadataUpdateFunction } from '@/features/metadata/services/MetadataUpdater.ts';
|
||||
import { MetadataChunker } from '@/features/metadata/services/MetadataChunker.ts';
|
||||
import { SourceIdInfo } from '@/features/source/Source.types.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
|
||||
@@ -278,7 +279,8 @@ const commitMigratedMetadata = (
|
||||
migratedMetadata: Metadata,
|
||||
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
|
||||
): void => {
|
||||
const metadata = metadataHolder?.meta;
|
||||
const rawMetadata = metadataHolder?.meta;
|
||||
const metadata = MetadataChunker.reassembleAllChunkedValues(rawMetadata);
|
||||
|
||||
const migrationId = Number(metadata?.[getMetadataKey('migration')] ?? 1);
|
||||
|
||||
@@ -294,6 +296,16 @@ const commitMigratedMetadata = (
|
||||
migratedMetadata[key],
|
||||
]) as MetadataKeyValuePair[];
|
||||
|
||||
// Expand app keys to chunk keys for deletion (e.g., some_key -> some_key_0, some_key_1, some_key_2, ..., some_key_n, some_key_length
|
||||
const keysToDelete = metadataKeysToDelete.flatMap((fullKey) => {
|
||||
if (rawMetadata && MetadataChunker.isChunkedInMetadata(rawMetadata, fullKey)) {
|
||||
const count = MetadataChunker.getExistingChunkCount(rawMetadata, fullKey);
|
||||
const chunkKeys = Array.from({ length: count }, (_, i) => MetadataChunker.getChunkIndexKey(fullKey, i));
|
||||
return [MetadataChunker.getChunkLengthKey(fullKey), ...chunkKeys];
|
||||
}
|
||||
return [fullKey];
|
||||
}) as AppMetadataKeys[];
|
||||
|
||||
const updateMetadata = getMetadataUpdateFunction(type, metadataHolder ?? { id: -1, meta: {} });
|
||||
|
||||
useEffectFn(() => {
|
||||
@@ -318,7 +330,7 @@ const commitMigratedMetadata = (
|
||||
try {
|
||||
await updateMetadata({
|
||||
update: metadataToUpdate,
|
||||
delete: metadataKeysToDelete,
|
||||
delete: keysToDelete,
|
||||
migrate: [['migration', METADATA_MIGRATIONS.length]],
|
||||
isMetadataKey: true,
|
||||
});
|
||||
@@ -343,7 +355,8 @@ export const applyMetadataMigrations = (
|
||||
| (SourceIdInfo & MetadataHolder),
|
||||
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
|
||||
): Metadata | undefined => {
|
||||
const meta = { ...(metadataHolder?.meta ?? {}) };
|
||||
const rawMeta = metadataHolder?.meta ?? {};
|
||||
const meta = MetadataChunker.reassembleAllChunkedValues(rawMeta) ?? {};
|
||||
|
||||
const migrationIdKey = getMetadataKey('migration');
|
||||
const appliedMigrationId = Number.isNaN(Number(meta[migrationIdKey]))
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||
|
||||
import { CategoryIdInfo } from '@/features/category/Category.types.ts';
|
||||
import { doesMetadataKeyExistIn, getMetadataKey } from '@/features/metadata/Metadata.utils.ts';
|
||||
import { MetadataChunker } from '@/features/metadata/services/MetadataChunker.ts';
|
||||
import { applyMetadataMigrations } from '@/features/metadata/services/MetadataMigrations.ts';
|
||||
import { SourceIdInfo } from '@/features/source/Source.types.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
@@ -55,15 +56,21 @@ const getRawMetadataValueFrom = (
|
||||
key: string,
|
||||
prefixes?: string[],
|
||||
): string | undefined => {
|
||||
if (
|
||||
metadata === undefined ||
|
||||
!doesMetadataKeyExistIn(metadata, key, prefixes) ||
|
||||
metadata[getMetadataKey(key, prefixes)] === undefined
|
||||
) {
|
||||
if (metadata === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return metadata[getMetadataKey(key, prefixes)];
|
||||
const fullKey = getMetadataKey(key, prefixes);
|
||||
|
||||
if (doesMetadataKeyExistIn(metadata, fullKey) && metadata[fullKey] !== undefined) {
|
||||
return metadata[fullKey];
|
||||
}
|
||||
|
||||
if (MetadataChunker.isChunkedInMetadata(metadata, fullKey)) {
|
||||
return MetadataChunker.reassembleChunkedValue(metadata, fullKey);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends AllowedMetadataValueTypes>(
|
||||
|
||||
@@ -18,8 +18,10 @@ import {
|
||||
import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||
import { getMetadataKey } from '@/features/metadata/Metadata.utils.ts';
|
||||
import { convertToGqlMeta } from '@/features/metadata/services/MetadataConverter.ts';
|
||||
import { MetadataChunker } from '@/features/metadata/services/MetadataChunker.ts';
|
||||
import { SourceIdInfo } from '@/features/source/Source.types.ts';
|
||||
import { ChapterIdInfo } from '@/features/chapter/Chapter.types.ts';
|
||||
import { MetaInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
type MetadataUpdateOptions = {
|
||||
update?: MetadataKeyValuePair[];
|
||||
@@ -52,19 +54,47 @@ const requestMetadataUpdate = async (
|
||||
);
|
||||
}
|
||||
|
||||
const updateMetas = keysToValues.map(([key, value]) => ({
|
||||
key: isMetadataKey ? key : getMetadataKey(key, keyPrefixes),
|
||||
value: `${value}`,
|
||||
}));
|
||||
const deleteKeys = keysToDelete.map((key) => (isMetadataKey ? key : getMetadataKey(key, keyPrefixes)));
|
||||
const existingMetadata = MetadataChunker.getExistingMetadata(metadataHolder, holderType);
|
||||
|
||||
const allUpdateMetas: MetaInput[] = [];
|
||||
const allDeleteKeys: string[] = [];
|
||||
|
||||
keysToValues.forEach(([key, value]) => {
|
||||
const fullKey = isMetadataKey ? key : getMetadataKey(key, keyPrefixes);
|
||||
const stringValue = `${value}`;
|
||||
|
||||
const chunkEntries = MetadataChunker.chunkValue(fullKey, stringValue);
|
||||
allUpdateMetas.push(...chunkEntries);
|
||||
|
||||
const doFullCleanup = !!existingMetadata;
|
||||
if (doFullCleanup) {
|
||||
const newChunkCount = chunkEntries.length - 1;
|
||||
allDeleteKeys.push(...MetadataChunker.computeChunkDeletions(existingMetadata, fullKey, newChunkCount));
|
||||
} else {
|
||||
const isNowChunked = chunkEntries.length > 1;
|
||||
if (!isNowChunked) {
|
||||
allDeleteKeys.push(MetadataChunker.getChunkLengthKey(fullKey));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
keysToDelete.forEach((key) => {
|
||||
const fullKey = isMetadataKey ? key : getMetadataKey(key, keyPrefixes);
|
||||
allDeleteKeys.push(fullKey);
|
||||
|
||||
allDeleteKeys.push(...MetadataChunker.computeChunkDeletions(existingMetadata, fullKey, 0));
|
||||
});
|
||||
|
||||
const uniqueDeleteKeys = [...new Set(allDeleteKeys)];
|
||||
|
||||
const migrateMetas = keysToMigrate.map(([key, value]) => ({
|
||||
key: getMetadataKey(key, keyPrefixes),
|
||||
value: `${value}`,
|
||||
}));
|
||||
|
||||
const updateMetaInput = {
|
||||
updateInput: { metas: updateMetas },
|
||||
deleteInput: { keys: deleteKeys },
|
||||
updateInput: { metas: allUpdateMetas },
|
||||
deleteInput: { keys: uniqueDeleteKeys },
|
||||
migrateInput: { metas: migrateMetas },
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,14 @@ export class MetadataValueCache {
|
||||
return `${type}::${holderId ?? ''}::${key}`;
|
||||
}
|
||||
|
||||
static getCachedValue<T>(
|
||||
type: MetadataHolderType,
|
||||
holderId: string | number | undefined,
|
||||
key: string,
|
||||
): T | undefined {
|
||||
return this.convertedValueByKey.get(this.getCacheKey(type, holderId, key)) as T | undefined;
|
||||
}
|
||||
|
||||
static getStableValue<T>(
|
||||
type: MetadataHolderType,
|
||||
holderId: string | number | undefined,
|
||||
@@ -26,7 +34,7 @@ export class MetadataValueCache {
|
||||
): T {
|
||||
const cacheKey = this.getCacheKey(type, holderId, key);
|
||||
const cachedRawValue = this.rawValueByKey.get(cacheKey);
|
||||
const cachedConvertedValue = this.convertedValueByKey.get(cacheKey);
|
||||
const cachedConvertedValue = this.getCachedValue<T>(type, holderId, key);
|
||||
|
||||
if (cachedRawValue !== undefined && rawValue === cachedRawValue) {
|
||||
return cachedConvertedValue as T;
|
||||
|
||||
@@ -20,13 +20,10 @@ import {
|
||||
ReadingDirection,
|
||||
ReadingMode,
|
||||
} from '@/features/reader/Reader.types.ts';
|
||||
import { updateReaderSettings } from '@/features/reader/settings/ReaderSettingsMetadata.ts';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { MANGA_META_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts';
|
||||
import { makeToast } from '@/base/utils/Toast.ts';
|
||||
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
|
||||
import { GLOBAL_METADATA } from '@/lib/graphql/common/Fragments.ts';
|
||||
import { updateMetadataList } from '@/features/metadata/services/MetadataApolloCacheHandler.ts';
|
||||
import { useBackButton } from '@/base/hooks/useBackButton.ts';
|
||||
import { GLOBAL_READER_SETTING_KEYS } from '@/features/reader/settings/ReaderSettings.constants.tsx';
|
||||
import { UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
@@ -54,6 +51,13 @@ import {
|
||||
} from '@/features/reader/stores/ReaderStore.ts';
|
||||
import { ReactRouter } from '@/lib/react-router/ReactRouter.ts';
|
||||
import { getPage } from '@/features/reader/overlay/progress-bar/ReaderProgressBar.utils.tsx';
|
||||
import { getMetadataUpdateFunction } from '@/features/metadata/services/MetadataUpdater.ts';
|
||||
import { MetadataChunker } from '@/features/metadata/services/MetadataChunker.ts';
|
||||
import { MetadataValueCache } from '@/features/metadata/services/MetadataValueCache.ts';
|
||||
import { convertFromGqlMeta, convertToGqlMeta } from '@/features/metadata/services/MetadataConverter.ts';
|
||||
import { GLOBAL_METADATA } from '@/lib/graphql/common/Fragments.ts';
|
||||
import { AppMetadataKeys, GqlMetaHolder, Metadata, MetadataHolderType } from '@/features/metadata/Metadata.types.ts';
|
||||
import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
||||
|
||||
const DIRECTION_TO_INVERTED: Record<Direction, Direction> = {
|
||||
ltr: 'rtl',
|
||||
@@ -272,6 +276,37 @@ export class ReaderService {
|
||||
ReaderService.updateSetting('shouldOffsetDoubleSpreads', shouldOffset);
|
||||
}
|
||||
|
||||
private static getMetadataWithUnmodifiedKey(
|
||||
metaHolder: (MangaIdInfo & GqlMetaHolder) | GqlMetaHolder,
|
||||
metaHolderType: MetadataHolderType,
|
||||
key: string,
|
||||
isGlobalSetting: boolean,
|
||||
): Metadata | undefined {
|
||||
// Since we update the value in the apollo cache before sending the mutation, we need the actual unmodified value
|
||||
// to be able to handle updating chunked metadata
|
||||
const unmodifiedOriginalValue =
|
||||
MetadataValueCache.getCachedValue(
|
||||
metaHolderType,
|
||||
isGlobalSetting ? undefined : (metaHolder as MangaIdInfo).id,
|
||||
key,
|
||||
) ?? '';
|
||||
|
||||
const currentMetadata = isGlobalSetting
|
||||
? MetadataChunker.getExistingMetadata(metaHolder, metaHolderType)
|
||||
: convertFromGqlMeta(metaHolder.meta);
|
||||
|
||||
const settingMetaKeysToDelete = MetadataChunker.computeChunkDeletions(currentMetadata, key, 0);
|
||||
const currentMetadataWithoutKey = convertToGqlMeta(currentMetadata)?.filter(
|
||||
(meta) => !settingMetaKeysToDelete.includes(meta.key),
|
||||
);
|
||||
const existingMeta = {
|
||||
...convertFromGqlMeta(currentMetadataWithoutKey),
|
||||
...convertFromGqlMeta(MetadataChunker.chunkValue(key, unmodifiedOriginalValue.toString())),
|
||||
};
|
||||
|
||||
return existingMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the change immediately to the cache and sends a mutation in case "commit" is true.
|
||||
*/
|
||||
@@ -283,60 +318,66 @@ export class ReaderService {
|
||||
profile?: ReadingMode,
|
||||
): void {
|
||||
const { manga: currentManga } = getReaderStore();
|
||||
const manga = currentManga ?? (isGlobal ? GLOBAL_READER_SETTINGS_MANGA : currentManga);
|
||||
const manga = isGlobal ? GLOBAL_READER_SETTINGS_MANGA : (currentManga ?? FALLBACK_MANGA);
|
||||
|
||||
if (!manga || manga.id === FALLBACK_MANGA.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
const metaHolderType = isGlobalSetting ? 'global' : 'manga';
|
||||
|
||||
const key = getMetadataKey(setting, profile !== undefined ? [profile?.toString()] : undefined);
|
||||
const metaValue = JSON.stringify(value);
|
||||
const chunkedMeta = MetadataChunker.chunkValue(key, metaValue);
|
||||
|
||||
const existingMeta = this.getMetadataWithUnmodifiedKey(manga, metaHolderType, key, isGlobalSetting);
|
||||
|
||||
const keysToDelete = MetadataChunker.computeChunkDeletions(existingMeta, key, chunkedMeta.length - 1);
|
||||
|
||||
const updatedMetadata = convertToGqlMeta({
|
||||
...existingMeta,
|
||||
...convertFromGqlMeta(chunkedMeta),
|
||||
})?.filter((meta) => !keysToDelete.includes(meta.key));
|
||||
|
||||
const { cache } = requestManager.graphQLClient.client;
|
||||
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
if (isGlobalSetting) {
|
||||
const reference = cache.writeFragment({
|
||||
fragment: GLOBAL_METADATA,
|
||||
data: {
|
||||
__typename: 'GlobalMetaType',
|
||||
key,
|
||||
value: metaValue,
|
||||
},
|
||||
});
|
||||
cache.modify({
|
||||
fields: {
|
||||
metas(existingMetas, { readField }) {
|
||||
metas(existingMetas) {
|
||||
return {
|
||||
...existingMetas,
|
||||
nodes: updateMetadataList(
|
||||
[{ key, value: metaValue }],
|
||||
existingMetas?.nodes,
|
||||
readField,
|
||||
() => reference,
|
||||
nodes: updatedMetadata?.map((meta) =>
|
||||
cache.writeFragment({
|
||||
fragment: GLOBAL_METADATA,
|
||||
data: {
|
||||
__typename: 'GlobalMetaType',
|
||||
mangaId: manga.id,
|
||||
key: meta.key,
|
||||
value: meta.value,
|
||||
},
|
||||
}),
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const reference = cache.writeFragment({
|
||||
fragment: MANGA_META_FIELDS,
|
||||
data: {
|
||||
__typename: 'MangaMetaType',
|
||||
mangaId: manga.id,
|
||||
key,
|
||||
value: metaValue,
|
||||
},
|
||||
});
|
||||
cache.modify({
|
||||
id: cache.identify({ __typename: 'MangaType', id: manga.id }),
|
||||
fields: {
|
||||
meta(existingMetas, { readField }) {
|
||||
return updateMetadataList(
|
||||
[{ key, value: metaValue }],
|
||||
existingMetas,
|
||||
readField,
|
||||
() => reference,
|
||||
meta() {
|
||||
return updatedMetadata?.map((meta) =>
|
||||
cache.writeFragment({
|
||||
fragment: MANGA_META_FIELDS,
|
||||
data: {
|
||||
__typename: 'MangaMetaType',
|
||||
mangaId: manga.id,
|
||||
key: meta.key,
|
||||
value: meta.value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -344,41 +385,42 @@ export class ReaderService {
|
||||
}
|
||||
|
||||
if (commit) {
|
||||
updateReaderSettings(manga, setting, value, isGlobal, profile).catch((e) =>
|
||||
makeToast(t`Could not save the reader settings to the server`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
getMetadataUpdateFunction(metaHolderType, { id: manga.id, meta: convertFromGqlMeta(updatedMetadata) })({
|
||||
update: [[setting, metaValue]],
|
||||
delete: keysToDelete as AppMetadataKeys[],
|
||||
keyPrefixes: profile !== undefined ? [profile.toString()] : undefined,
|
||||
}).catch((e) => {
|
||||
makeToast(t`Could not save the reader settings to the server`, 'error', getErrorMessage(e));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static deleteSetting<Setting extends keyof IReaderSettings>(
|
||||
setting: Setting,
|
||||
isGlobal: boolean = false,
|
||||
profile?: string,
|
||||
profile?: ReadingMode,
|
||||
): void {
|
||||
const { manga: currentManga } = getReaderStore();
|
||||
const manga = currentManga ?? (isGlobal ? GLOBAL_READER_SETTINGS_MANGA : currentManga);
|
||||
const manga = isGlobal ? GLOBAL_READER_SETTINGS_MANGA : (currentManga ?? FALLBACK_MANGA);
|
||||
|
||||
if (!manga || manga.id === FALLBACK_MANGA.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = getMetadataKey(setting, profile !== undefined ? [profile] : undefined);
|
||||
|
||||
const { cache } = requestManager.graphQLClient.client;
|
||||
|
||||
const key = getMetadataKey(setting, profile !== undefined ? [profile?.toString()] : undefined);
|
||||
const isGlobalSetting = isGlobal || GLOBAL_READER_SETTING_KEYS.includes(setting);
|
||||
if (isGlobalSetting) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'GlobalMetaType', key }) });
|
||||
} else {
|
||||
cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', mangaId: manga.id, key }) });
|
||||
}
|
||||
const metaHolderType = isGlobalSetting ? 'global' : 'manga';
|
||||
|
||||
const deleteSetting = isGlobalSetting
|
||||
? () => requestManager.deleteGlobalMeta({ keys: [key] }).response
|
||||
: () => requestManager.deleteMangaMeta({ items: [{ mangaIds: [manga.id], keys: [key] }] }).response;
|
||||
deleteSetting().catch((e) =>
|
||||
makeToast(t`Could not save the reader settings to the server`, 'error', getErrorMessage(e)),
|
||||
);
|
||||
const existingMeta = this.getMetadataWithUnmodifiedKey(manga, metaHolderType, key, isGlobalSetting);
|
||||
|
||||
const keysToDelete = MetadataChunker.computeChunkDeletions(existingMeta, key, 0);
|
||||
|
||||
const updatedMetadata = convertToGqlMeta(existingMeta)?.filter((meta) => !keysToDelete.includes(meta.key));
|
||||
|
||||
getMetadataUpdateFunction(metaHolderType, { id: manga.id, meta: convertFromGqlMeta(updatedMetadata) })({
|
||||
delete: [setting],
|
||||
keyPrefixes: profile !== undefined ? [`${profile}`] : undefined,
|
||||
}).catch((e) => makeToast(t`Could not save the reader settings to the server`, 'error', getErrorMessage(e)));
|
||||
}
|
||||
|
||||
static useOverlayMode(): { mode: ReaderOverlayMode; isDesktop: boolean; isMobile: boolean } {
|
||||
|
||||
Reference in New Issue
Block a user