Cleanup deprecated metadata after migration
- delete deprecated metadata - save applied migration id per metadata holder
This commit is contained in:
@@ -2826,7 +2826,7 @@ export type DeleteCategoryMetadataMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteCategoryMetadataMutation = { __typename?: 'Mutation', deleteCategoryMeta?: { __typename?: 'DeleteCategoryMetaPayload', meta?: { __typename?: 'CategoryMetaType', categoryId: number, key: string, value: string } | null, category: { __typename?: 'CategoryType', id: number, meta: Array<{ __typename?: 'CategoryMetaType', categoryId: number, key: string, value: string }> } } | null };
|
||||
export type DeleteCategoryMetadataMutation = { __typename?: 'Mutation', deleteCategoryMeta?: { __typename?: 'DeleteCategoryMetaPayload', meta?: { __typename?: 'CategoryMetaType', categoryId: number, key: string, value: string } | null } | null };
|
||||
|
||||
export type SetCategoryMetadataMutationVariables = Exact<{
|
||||
input: SetCategoryMetaInput;
|
||||
@@ -2869,7 +2869,7 @@ export type DeleteChapterMetadataMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteChapterMetadataMutation = { __typename?: 'Mutation', deleteChapterMeta?: { __typename?: 'DeleteChapterMetaPayload', meta?: { __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string } | null, chapter: { __typename?: 'ChapterType', id: number, meta: Array<{ __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string }> } } | null };
|
||||
export type DeleteChapterMetadataMutation = { __typename?: 'Mutation', deleteChapterMeta?: { __typename?: 'DeleteChapterMetaPayload', meta?: { __typename?: 'ChapterMetaType', chapterId: number, key: string, value: string } | null } | null };
|
||||
|
||||
export type GetChapterPagesFetchMutationVariables = Exact<{
|
||||
input: FetchChapterPagesInput;
|
||||
@@ -3044,7 +3044,7 @@ export type DeleteMangaMetadataMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteMangaMetadataMutation = { __typename?: 'Mutation', deleteMangaMeta?: { __typename?: 'DeleteMangaMetaPayload', meta?: { __typename?: 'MangaMetaType', mangaId: number, key: string, value: string } | null, manga: { __typename?: 'MangaType', id: number, meta: Array<{ __typename?: 'MangaMetaType', mangaId: number, key: string, value: string }> } } | null };
|
||||
export type DeleteMangaMetadataMutation = { __typename?: 'Mutation', deleteMangaMeta?: { __typename?: 'DeleteMangaMetaPayload', meta?: { __typename?: 'MangaMetaType', mangaId: number, key: string, value: string } | null } | null };
|
||||
|
||||
export type GetMangaFetchMutationVariables = Exact<{
|
||||
input: FetchMangaInput;
|
||||
@@ -3149,6 +3149,13 @@ export type SetSourceMetadataMutationVariables = Exact<{
|
||||
|
||||
export type SetSourceMetadataMutation = { __typename?: 'Mutation', setSourceMeta?: { __typename?: 'SetSourceMetaPayload', meta: { __typename?: 'SourceMetaType', sourceId: string, key: string, value: string } } | null };
|
||||
|
||||
export type DeleteSourceMetadataMutationVariables = Exact<{
|
||||
input: DeleteSourceMetaInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteSourceMetadataMutation = { __typename?: 'Mutation', deleteSourceMeta?: { __typename?: 'DeleteSourceMetaPayload', meta?: { __typename?: 'SourceMetaType', sourceId: string, key: string, value: string } | null } | null };
|
||||
|
||||
export type TrackerLoginOauthMutationVariables = Exact<{
|
||||
input: LoginTrackerOAuthInput;
|
||||
}>;
|
||||
|
||||
@@ -38,12 +38,6 @@ export const DELETE_CATEGORY_METADATA = gql`
|
||||
meta {
|
||||
...CATEGORY_META_FIELDS
|
||||
}
|
||||
category {
|
||||
id
|
||||
meta {
|
||||
...CATEGORY_META_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -19,12 +19,6 @@ export const DELETE_CHAPTER_METADATA = gql`
|
||||
meta {
|
||||
...CHAPTER_META_FIELDS
|
||||
}
|
||||
chapter {
|
||||
id
|
||||
meta {
|
||||
...CHAPTER_META_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -17,12 +17,6 @@ export const DELETE_MANGA_METADATA = gql`
|
||||
meta {
|
||||
...MANGA_META_FIELDS
|
||||
}
|
||||
manga {
|
||||
id
|
||||
meta {
|
||||
...MANGA_META_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -46,3 +46,15 @@ export const SET_SOURCE_METADATA = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_SOURCE_METADATA = gql`
|
||||
${SOURCE_META_FIELDS}
|
||||
|
||||
mutation DELETE_SOURCE_METADATA($input: DeleteSourceMetaInput!) {
|
||||
deleteSourceMeta(input: $input) {
|
||||
meta {
|
||||
...SOURCE_META_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -46,12 +46,22 @@ import {
|
||||
CreateCategoryInput,
|
||||
CreateCategoryMutation,
|
||||
CreateCategoryMutationVariables,
|
||||
DeleteCategoryMetadataMutation,
|
||||
DeleteCategoryMetadataMutationVariables,
|
||||
DeleteCategoryMutation,
|
||||
DeleteCategoryMutationVariables,
|
||||
DeleteChapterMetadataMutation,
|
||||
DeleteChapterMetadataMutationVariables,
|
||||
DeleteDownloadedChapterMutation,
|
||||
DeleteDownloadedChapterMutationVariables,
|
||||
DeleteDownloadedChaptersMutation,
|
||||
DeleteDownloadedChaptersMutationVariables,
|
||||
DeleteGlobalMetadataMutation,
|
||||
DeleteGlobalMetadataMutationVariables,
|
||||
DeleteMangaMetadataMutation,
|
||||
DeleteMangaMetadataMutationVariables,
|
||||
DeleteSourceMetadataMutation,
|
||||
DeleteSourceMetadataMutationVariables,
|
||||
DequeueChapterDownloadMutation,
|
||||
DequeueChapterDownloadMutationVariables,
|
||||
DequeueChapterDownloadsMutation,
|
||||
@@ -199,7 +209,7 @@ import {
|
||||
WebuiUpdateSubscription,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts';
|
||||
import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
|
||||
import { DELETE_GLOBAL_METADATA, SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
|
||||
import {
|
||||
CHECK_FOR_SERVER_UPDATES,
|
||||
CHECK_FOR_WEBUI_UPDATE,
|
||||
@@ -214,6 +224,7 @@ import {
|
||||
} from '@/lib/graphql/mutations/ExtensionMutation.ts';
|
||||
import { GET_MIGRATABLE_SOURCES, GET_SOURCES_LIST } from '@/lib/graphql/queries/SourceQuery.ts';
|
||||
import {
|
||||
DELETE_MANGA_METADATA,
|
||||
GET_MANGA_FETCH,
|
||||
GET_MANGA_TO_MIGRATE_TO_FETCH,
|
||||
SET_MANGA_METADATA,
|
||||
@@ -235,6 +246,7 @@ import {
|
||||
GET_CATEGORY_MANGAS,
|
||||
} from '@/lib/graphql/queries/CategoryQuery.ts';
|
||||
import {
|
||||
DELETE_SOURCE_METADATA,
|
||||
GET_SOURCE_MANGAS_FETCH,
|
||||
SET_SOURCE_METADATA,
|
||||
UPDATE_SOURCE_PREFERENCES,
|
||||
@@ -257,6 +269,7 @@ import {
|
||||
GET_MANGAS_CHAPTER_IDS_WITH_STATE,
|
||||
} from '@/lib/graphql/queries/ChapterQuery.ts';
|
||||
import {
|
||||
DELETE_CHAPTER_METADATA,
|
||||
GET_CHAPTER_PAGES_FETCH,
|
||||
GET_MANGA_CHAPTERS_FETCH,
|
||||
SET_CHAPTER_METADATA,
|
||||
@@ -266,6 +279,7 @@ import {
|
||||
import {
|
||||
CREATE_CATEGORY,
|
||||
DELETE_CATEGORY,
|
||||
DELETE_CATEGORY_METADATA,
|
||||
SET_CATEGORY_METADATA,
|
||||
UPDATE_CATEGORY,
|
||||
UPDATE_CATEGORY_ORDER,
|
||||
@@ -1152,6 +1166,34 @@ export class RequestManager {
|
||||
);
|
||||
}
|
||||
|
||||
public deleteGlobalMeta(
|
||||
key: string,
|
||||
options?: MutationOptions<DeleteGlobalMetadataMutation, DeleteGlobalMetadataMutationVariables>,
|
||||
): AbortableApolloMutationResponse<DeleteGlobalMetadataMutation> {
|
||||
return this.doRequest(
|
||||
GQLMethod.MUTATION,
|
||||
DELETE_GLOBAL_METADATA,
|
||||
{ input: { key } },
|
||||
{
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
deleteGlobalMeta: {
|
||||
__typename: 'DeleteGlobalMetaPayload',
|
||||
meta: {
|
||||
__typename: 'GlobalMetaType',
|
||||
key,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'GlobalMetaType', key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public useGetAbout(
|
||||
options?: QueryHookOptions<GetAboutQuery, GetAboutQueryVariables>,
|
||||
): AbortableApolloUseQueryResponse<GetAboutQuery, GetAboutQueryVariables> {
|
||||
@@ -1448,6 +1490,36 @@ export class RequestManager {
|
||||
);
|
||||
}
|
||||
|
||||
public deleteSourceMeta(
|
||||
sourceId: string,
|
||||
key: string,
|
||||
options?: MutationOptions<DeleteSourceMetadataMutation, DeleteSourceMetadataMutationVariables>,
|
||||
): AbortableApolloMutationResponse<DeleteSourceMetadataMutation> {
|
||||
return this.doRequest(
|
||||
GQLMethod.MUTATION,
|
||||
DELETE_SOURCE_METADATA,
|
||||
{ input: { sourceId, key } },
|
||||
{
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
deleteSourceMeta: {
|
||||
__typename: 'DeleteSourceMetaPayload',
|
||||
meta: {
|
||||
__typename: 'SourceMetaType',
|
||||
sourceId,
|
||||
key,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'SourceMetaType', id: sourceId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public useGetSourceMangas(
|
||||
input: FetchSourceMangaInput,
|
||||
initialPages: number = 1,
|
||||
@@ -1942,6 +2014,36 @@ export class RequestManager {
|
||||
);
|
||||
}
|
||||
|
||||
public deleteMangaMeta(
|
||||
mangaId: number,
|
||||
key: string,
|
||||
options?: MutationOptions<DeleteMangaMetadataMutation, DeleteMangaMetadataMutationVariables>,
|
||||
): AbortableApolloMutationResponse<DeleteMangaMetadataMutation> {
|
||||
return this.doRequest(
|
||||
GQLMethod.MUTATION,
|
||||
DELETE_MANGA_METADATA,
|
||||
{ input: { mangaId, key } },
|
||||
{
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
deleteMangaMeta: {
|
||||
__typename: 'DeleteMangaMetaPayload',
|
||||
meta: {
|
||||
__typename: 'MangaMetaType',
|
||||
mangaId,
|
||||
key,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'MangaMetaType', id: mangaId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public useGetChapters<Data, Variables extends OperationVariables>(
|
||||
document: DocumentNode | TypedDocumentNode<Data, Variables>,
|
||||
variables: Variables,
|
||||
@@ -2140,6 +2242,36 @@ export class RequestManager {
|
||||
);
|
||||
}
|
||||
|
||||
public deleteChapterMeta(
|
||||
chapterId: number,
|
||||
key: string,
|
||||
options?: MutationOptions<DeleteChapterMetadataMutation, DeleteChapterMetadataMutationVariables>,
|
||||
): AbortableApolloMutationResponse<DeleteChapterMetadataMutation> {
|
||||
return this.doRequest(
|
||||
GQLMethod.MUTATION,
|
||||
DELETE_CHAPTER_METADATA,
|
||||
{ input: { chapterId, key } },
|
||||
{
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
deleteChapterMeta: {
|
||||
__typename: 'DeleteChapterMetaPayload',
|
||||
meta: {
|
||||
__typename: 'ChapterMetaType',
|
||||
chapterId,
|
||||
key,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'ChapterMetaType', id: chapterId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getChapterPageUrl(mangaId: number | string, chapterIndex: number | string, page: number): string {
|
||||
return this.getValidImgUrlFor(
|
||||
`manga/${mangaId}/chapter/${chapterIndex}/page/${page}`,
|
||||
@@ -2389,6 +2521,36 @@ export class RequestManager {
|
||||
);
|
||||
}
|
||||
|
||||
public deleteCategoryMeta(
|
||||
categoryId: number,
|
||||
key: string,
|
||||
options?: MutationOptions<DeleteCategoryMetadataMutation, DeleteCategoryMetadataMutationVariables>,
|
||||
): AbortableApolloMutationResponse<DeleteCategoryMetadataMutation> {
|
||||
return this.doRequest(
|
||||
GQLMethod.MUTATION,
|
||||
DELETE_CATEGORY_METADATA,
|
||||
{ input: { categoryId, key } },
|
||||
{
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
deleteCategoryMeta: {
|
||||
__typename: 'DeleteCategoryMetaPayload',
|
||||
meta: {
|
||||
__typename: 'CategoryMetaType',
|
||||
categoryId,
|
||||
key,
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
update(cache) {
|
||||
cache.evict({ id: cache.identify({ __typename: 'CategoryMetaType', id: categoryId, key }) });
|
||||
},
|
||||
...options,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public restoreBackupFile(
|
||||
file: File,
|
||||
options?: MutationOptions<RestoreBackupMutation, RestoreBackupMutationVariables>,
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||
import { requestUpdateCategoryMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryIdInfo, CategoryMetadataKeys, ICategoryMetadata } from '@/modules/category/Category.types.ts';
|
||||
import { convertFromGqlMeta } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||
@@ -19,9 +19,11 @@ import {
|
||||
AppMetadataKeys,
|
||||
GqlMetaHolder,
|
||||
Metadata,
|
||||
MetadataHolder,
|
||||
} from '@/modules/metadata/Metadata.types.ts';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const getDefaultCategoryMetadata = (): ICategoryMetadata => ({
|
||||
export const DEFAULT_CATEGORY_METADATA: ICategoryMetadata = {
|
||||
// display options
|
||||
showContinueReadingButton: false,
|
||||
showDownloadBadge: false,
|
||||
@@ -39,7 +41,7 @@ export const getDefaultCategoryMetadata = (): ICategoryMetadata => ({
|
||||
hasDuplicateChapters: undefined,
|
||||
hasTrackerBinding: {},
|
||||
hasStatus: {} as LibraryOptions['hasStatus'],
|
||||
});
|
||||
};
|
||||
|
||||
const convertAppMetadataToGqlMetadata = (
|
||||
metadata: Partial<ICategoryMetadata>,
|
||||
@@ -60,20 +62,37 @@ const convertGqlMetadataToAppMetadata = (
|
||||
});
|
||||
|
||||
const getCategoryMetadataWithDefaultValueFallback = (
|
||||
meta?: Metadata,
|
||||
defaultMetadata: ICategoryMetadata = getDefaultCategoryMetadata(),
|
||||
applyMetadataMigration: boolean = true,
|
||||
meta: CategoryIdInfo & MetadataHolder,
|
||||
defaultMetadata: ICategoryMetadata = DEFAULT_CATEGORY_METADATA,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): ICategoryMetadata =>
|
||||
convertGqlMetadataToAppMetadata(
|
||||
getMetadataFrom({ meta }, convertAppMetadataToGqlMetadata(defaultMetadata), applyMetadataMigration),
|
||||
getMetadataFrom('category', meta, convertAppMetadataToGqlMetadata(defaultMetadata), true, useEffectFn),
|
||||
);
|
||||
|
||||
const getMetadata = (
|
||||
metaHolder: CategoryIdInfo & GqlMetaHolder,
|
||||
defaultMetadata?: ICategoryMetadata,
|
||||
useEffectFn?: typeof useEffect,
|
||||
) =>
|
||||
getCategoryMetadataWithDefaultValueFallback(
|
||||
{ ...metaHolder, meta: convertFromGqlMeta(metaHolder.meta) },
|
||||
defaultMetadata,
|
||||
useEffectFn,
|
||||
);
|
||||
|
||||
export const getCategoryMetadata = (
|
||||
{ meta }: GqlMetaHolder = {},
|
||||
metaHolder: CategoryIdInfo & GqlMetaHolder,
|
||||
defaultMetadata?: ICategoryMetadata,
|
||||
applyMetadataMigration?: boolean,
|
||||
): ICategoryMetadata =>
|
||||
getCategoryMetadataWithDefaultValueFallback(convertFromGqlMeta(meta), defaultMetadata, applyMetadataMigration);
|
||||
): ICategoryMetadata => getMetadata(metaHolder, defaultMetadata);
|
||||
|
||||
export const useGetCategoryMetadata = (
|
||||
metaHolder: CategoryIdInfo & GqlMetaHolder,
|
||||
defaultMetadata?: ICategoryMetadata,
|
||||
): ICategoryMetadata => {
|
||||
const metadata = getMetadata(metaHolder, defaultMetadata, useEffect);
|
||||
return useMemo(() => metadata, [metaHolder, defaultMetadata]);
|
||||
};
|
||||
|
||||
export const updateCategoryMetadata = async <
|
||||
MetadataKeys extends CategoryMetadataKeys = CategoryMetadataKeys,
|
||||
|
||||
@@ -15,7 +15,8 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import React from 'react';
|
||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
||||
export function GridLayouts({
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GridLayout, useLibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { useLibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { IMangaGridProps, MangaGrid } from '@/modules/manga/components/MangaGrid.tsx';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
interface LibraryMangaGridProps
|
||||
extends Required<Pick<IMangaGridProps, 'isSelectModeActive' | 'selectedMangaIds' | 'handleSelection' | 'mangas'>>,
|
||||
|
||||
@@ -8,19 +8,17 @@
|
||||
|
||||
import FormLabel from '@mui/material/FormLabel';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { RadioInput } from '@/modules/core/components/inputs/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/modules/core/components/inputs/SortRadioInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { OptionsTabs } from '@/modules/core/components/OptionsTabs.tsx';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { Trackers } from '@/modules/tracker/services/Trackers.ts';
|
||||
import { GetTrackersSettingsQuery, MangaStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import { createUpdateCategoryMetadata, getCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { createUpdateCategoryMetadata, useGetCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
@@ -30,6 +28,7 @@ import { TranslationKey } from '@/Base.types.ts';
|
||||
import { LibrarySortMode } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryMetadataInfo } from '@/modules/category/Category.types.ts';
|
||||
import { statusToTranslationKey } from '@/modules/manga/Manga.constants.ts';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
@@ -61,7 +60,7 @@ export const LibraryOptionsPanel = ({
|
||||
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
|
||||
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
|
||||
|
||||
const categoryLibraryOptions = useMemo(() => getCategoryMetadata(category), [category]);
|
||||
const categoryLibraryOptions = useGetCategoryMetadata(category);
|
||||
const updateCategoryLibraryOptions = createUpdateCategoryMetadata(category, () =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes', 'error')),
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { getDefaultCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { DEFAULT_CATEGORY_METADATA } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
|
||||
type ContextType = {
|
||||
@@ -15,14 +15,8 @@ type ContextType = {
|
||||
setOptions: React.Dispatch<React.SetStateAction<LibraryOptions>>;
|
||||
};
|
||||
|
||||
export enum GridLayout {
|
||||
Compact = 0,
|
||||
Comfortable = 1,
|
||||
List = 2,
|
||||
}
|
||||
|
||||
export const LibraryOptionsContext = React.createContext<ContextType>({
|
||||
options: getDefaultCategoryMetadata(),
|
||||
options: DEFAULT_CATEGORY_METADATA,
|
||||
setOptions: () => {},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { LibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { getDefaultCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { DEFAULT_CATEGORY_METADATA } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { LibraryOptions } from '@/modules/library/Library.types.ts';
|
||||
|
||||
interface IProps {
|
||||
@@ -17,10 +17,10 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
||||
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', getDefaultCategoryMetadata());
|
||||
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DEFAULT_CATEGORY_METADATA);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ options: { ...getDefaultCategoryMetadata(), ...options }, setOptions }),
|
||||
() => ({ options: { ...DEFAULT_CATEGORY_METADATA, ...options }, setOptions }),
|
||||
[options, setOptions],
|
||||
);
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import { useMemo } from 'react';
|
||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||
import { ChapterType, MangaType, SourceType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { enhancedCleanup } from '@/util/Strings.ts';
|
||||
import { getCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { useGetCategoryMetadata } from '@/modules/category/services/CategoryMetadata.ts';
|
||||
import { NullAndUndefined } from '@/Base.types.ts';
|
||||
import { LibraryOptions, LibrarySortMode } from '@/modules/library/Library.types.ts';
|
||||
import { CategoryMetadataInfo } from '@/modules/category/Category.types.ts';
|
||||
import { CategoryIdInfo, CategoryMetadataInfo } from '@/modules/category/Category.types.ts';
|
||||
import { MangaChapterCountInfo, MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
|
||||
const triStateFilter = (
|
||||
@@ -198,6 +198,7 @@ const sortManga = <Manga extends TMangaSort>(
|
||||
return result;
|
||||
};
|
||||
|
||||
const DEFAULT_CATEGORY: CategoryIdInfo = { id: -1 };
|
||||
export const useGetVisibleLibraryMangas = <Manga extends MangaIdInfo & TMangasFilter & TMangaSort>(
|
||||
mangas: Manga[],
|
||||
category?: CategoryMetadataInfo,
|
||||
@@ -206,7 +207,7 @@ export const useGetVisibleLibraryMangas = <Manga extends MangaIdInfo & TMangasFi
|
||||
showFilteredOutMessage: boolean;
|
||||
} => {
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
const options = getCategoryMetadata(category);
|
||||
const options = useGetCategoryMetadata(category ?? DEFAULT_CATEGORY);
|
||||
const {
|
||||
hasUnreadChapters,
|
||||
hasDownloadedChapters,
|
||||
|
||||
@@ -84,7 +84,7 @@ export function Library() {
|
||||
const activeTab: (typeof tabs)[number] | undefined = tabs.find((tab) => tab.order === tabSearchParam) ?? tabs[0];
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setOptions(getCategoryMetadata(activeTab));
|
||||
setOptions(getCategoryMetadata(activeTab ?? { id: -1 }));
|
||||
}, [activeTab]);
|
||||
|
||||
const {
|
||||
@@ -169,7 +169,7 @@ export function Library() {
|
||||
setTitle(navBarTitle, title);
|
||||
setAction(
|
||||
<>
|
||||
{!isSelectModeActive && (
|
||||
{!isSelectModeActive && activeTab && (
|
||||
<>
|
||||
<AppbarSearch />
|
||||
<LibraryToolbarMenu category={activeTab} />
|
||||
|
||||
@@ -17,7 +17,6 @@ import Box from '@mui/material/Box';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { GridLayouts } from '@/modules/core/components/GridLayouts.tsx';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
@@ -33,6 +32,7 @@ import { IMangaGridProps } from '@/modules/manga/components/MangaGrid.tsx';
|
||||
import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx';
|
||||
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
|
||||
import { LibraryDuplicatesWorkerInput, TMangaDuplicate, TMangaDuplicates } from '@/modules/library/Library.types.ts';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const LibraryDuplicates = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
|
||||
import { LongPressPointerHandlers, LongPressResult } from 'use-long-press/lib/use-long-press.types';
|
||||
import { PopupState } from 'material-ui-popup-state/hooks';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||
import { useManageMangaLibraryState } from '@/modules/manga/hooks/useManageMangaLibraryState.tsx';
|
||||
import { ChapterType, MangaReaderFieldsFragment, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx';
|
||||
import { IReaderSettings } from '@/modules/reader/Reader.types';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { MangaCard } from '@/modules/manga/components/cards/MangaCard.tsx';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { SelectableCollectionReturnType } from '@/modules/collection/hooks/useSelectableCollection.ts';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/modules/core/components/buttons/StyledFab.tsx';
|
||||
@@ -33,6 +32,7 @@ import { MangaCardProps } from '@/modules/manga/Manga.types.ts';
|
||||
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { useResizeObserver } from '@/modules/core/hooks/useResizeObserver.tsx';
|
||||
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
|
||||
<Grid {...props} ref={ref} container spacing={1}>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import PopupState, { bindMenu } from 'material-ui-popup-state';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLongPress } from 'use-long-press';
|
||||
import { GridLayout, useLibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { useLibraryOptionsContext } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { MangaActionMenuItems, SingleModeProps } from '@/modules/manga/components/MangaActionMenuItems.tsx';
|
||||
import { Menu } from '@/modules/core/components/menu/Menu.tsx';
|
||||
import { MigrateDialog } from '@/modules/migration/components/MigrateDialog.tsx';
|
||||
@@ -19,6 +19,7 @@ import { MangaListCard } from '@/modules/manga/components/cards/MangaListCard.ts
|
||||
import { MangaCardMode, MangaCardProps } from '@/modules/manga/Manga.types.ts';
|
||||
import { ContinueReadingButton } from '@/modules/manga/components/ContinueReadingButton.tsx';
|
||||
import { MangaBadges } from '@/modules/manga/components/MangaBadges.tsx';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
const getMangaLinkTo = (
|
||||
mode: MangaCardMode,
|
||||
|
||||
@@ -17,11 +17,11 @@ import { styled } from '@mui/material/styles';
|
||||
import { useRef } from 'react';
|
||||
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
|
||||
import { MangaOptionButton } from '@/modules/manga/components/MangaOptionButton.tsx';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { Mangas } from '@/modules/manga/services/Mangas.ts';
|
||||
import { SpecificMangaCardProps } from '@/modules/manga/Manga.types.ts';
|
||||
import { TypographyMaxLines } from '@/modules/core/components/TypographyMaxLines.tsx';
|
||||
import { MANGA_COVER_ASPECT_RATIO } from '@/modules/manga/Manga.constants.ts';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
const BottomGradient = styled('div')({
|
||||
position: 'absolute',
|
||||
|
||||
@@ -10,6 +10,55 @@ import { AppMetadataKeys, IMetadataMigration } from '@/modules/metadata/Metadata
|
||||
|
||||
export const APP_METADATA_KEY_PREFIX = 'webUI_';
|
||||
|
||||
const APP_METADATA_OBJECT: Record<AppMetadataKeys, undefined> = {
|
||||
staticNav: undefined,
|
||||
showPageNumber: undefined,
|
||||
loadNextOnEnding: undefined,
|
||||
skipDupChapters: undefined,
|
||||
fitPageToWindow: undefined,
|
||||
scalePage: undefined,
|
||||
readerType: undefined,
|
||||
offsetFirstPage: undefined,
|
||||
migration: undefined,
|
||||
deleteChaptersManuallyMarkedRead: undefined,
|
||||
deleteChaptersWhileReading: undefined,
|
||||
deleteChaptersWithBookmark: undefined,
|
||||
downloadAheadLimit: undefined,
|
||||
showAddToLibraryCategorySelectDialog: undefined,
|
||||
ignoreFilters: undefined,
|
||||
removeMangaFromCategories: undefined,
|
||||
showTabSize: undefined,
|
||||
devices: undefined,
|
||||
migrateChapters: undefined,
|
||||
migrateCategories: undefined,
|
||||
migrateTracking: undefined,
|
||||
deleteChapters: undefined,
|
||||
migrateSortSettings: undefined,
|
||||
hideLibraryEntries: undefined,
|
||||
updateProgressAfterReading: undefined,
|
||||
updateProgressManualMarkRead: undefined,
|
||||
webUIInformAvailableUpdate: undefined,
|
||||
serverInformAvailableUpdate: undefined,
|
||||
readerWidth: undefined,
|
||||
savedSearches: undefined,
|
||||
showContinueReadingButton: undefined,
|
||||
showDownloadBadge: undefined,
|
||||
showUnreadBadge: undefined,
|
||||
gridLayout: undefined,
|
||||
sortBy: undefined,
|
||||
sortDesc: undefined,
|
||||
hasDownloadedChapters: undefined,
|
||||
hasBookmarkedChapters: undefined,
|
||||
hasUnreadChapters: undefined,
|
||||
hasDuplicateChapters: undefined,
|
||||
hasTrackerBinding: undefined,
|
||||
hasStatus: undefined,
|
||||
customThemes: undefined,
|
||||
mangaThumbnailBackdrop: undefined,
|
||||
};
|
||||
|
||||
export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_OBJECT);
|
||||
|
||||
export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [
|
||||
// downloads
|
||||
'deleteChaptersManuallyMarkedRead',
|
||||
|
||||
@@ -38,7 +38,12 @@ export type MetadataHolder<Keys extends string = string, Values = string> = {
|
||||
|
||||
export type AllowedMetadataValueTypes = string | boolean | number | undefined | null;
|
||||
|
||||
interface MetadataAppliedMigration {
|
||||
migration: number;
|
||||
}
|
||||
|
||||
export type AppMetadataKeys =
|
||||
| keyof MetadataAppliedMigration
|
||||
| MetadataServerSettingKeys
|
||||
| MangaMetadataKeys
|
||||
| SearchMetadataKeys
|
||||
@@ -46,3 +51,5 @@ export type AppMetadataKeys =
|
||||
| CategoryMetadataKeys;
|
||||
|
||||
export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
|
||||
|
||||
export type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global' | 'source';
|
||||
|
||||
33
src/modules/metadata/services/MetadataApolloCacheHandler.ts
Normal file
33
src/modules/metadata/services/MetadataApolloCacheHandler.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 { ReadFieldFunction } from '@apollo/client/cache/core/types/common';
|
||||
import { Reference } from '@apollo/client/utilities';
|
||||
|
||||
export const updateMetadataList = (
|
||||
key: string,
|
||||
existingMetas: Reference[] | undefined,
|
||||
readField: ReadFieldFunction,
|
||||
createMetaRef: () => Reference | undefined,
|
||||
deleted: boolean = false,
|
||||
): (Reference | undefined)[] | undefined => {
|
||||
if (!existingMetas) {
|
||||
return existingMetas;
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
return existingMetas.filter((metaRef: Reference) => readField('key', metaRef) !== key);
|
||||
}
|
||||
|
||||
const exists = existingMetas.some((metaRef: Reference) => readField('key', metaRef) === key);
|
||||
if (exists) {
|
||||
return existingMetas;
|
||||
}
|
||||
|
||||
return [...existingMetas, createMetaRef()];
|
||||
};
|
||||
@@ -111,7 +111,8 @@ export const applyMetadataMigrations = (meta?: Metadata): Metadata | undefined =
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const migrationToMetadata: [number, Metadata][] = [[0, meta]];
|
||||
const appliedMigrationId = Number.isNaN(Number(meta.migration)) ? 0 : Math.max(0, Number(meta.migration) - 1);
|
||||
const migrationToMetadata: [number, Metadata][] = [[appliedMigrationId, meta]];
|
||||
|
||||
METADATA_MIGRATIONS.forEach((migration, index) => {
|
||||
const migrationId = index + 1;
|
||||
|
||||
@@ -6,16 +6,43 @@
|
||||
* 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 { useEffect } from 'react';
|
||||
import {
|
||||
APP_METADATA_KEY_PREFIX,
|
||||
GLOBAL_METADATA_KEYS,
|
||||
METADATA_MIGRATIONS,
|
||||
VALID_APP_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 { convertToGqlMeta, convertValueFromMetadata } from '@/modules/metadata/services/MetadataConverter.ts';
|
||||
import {
|
||||
AllowedMetadataValueTypes,
|
||||
AppMetadataKeys,
|
||||
Metadata,
|
||||
MetadataHolder,
|
||||
MetadataHolderType,
|
||||
MetadataKeyValuePair,
|
||||
} from '@/modules/metadata/Metadata.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import {
|
||||
requestDeleteCategoryMetadata,
|
||||
requestDeleteChapterMetadata,
|
||||
requestDeleteMangaMetadata,
|
||||
requestDeleteServerMetadata,
|
||||
requestDeleteSourceMetadata,
|
||||
requestUpdateCategoryMetadata,
|
||||
requestUpdateChapterMetadata,
|
||||
requestUpdateMangaMetadata,
|
||||
requestUpdateServerMetadata,
|
||||
requestUpdateSourceMetadata,
|
||||
} from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { ChapterIdInfo } from '@/modules/chapter/services/Chapters.ts';
|
||||
import { CategoryIdInfo } from '@/modules/category/Category.types.ts';
|
||||
import { SourceType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export const extractOriginalKey = (key: string): string => key.split('_').slice(-1)[0];
|
||||
|
||||
export const getMetadataKey = (key: string, appPrefix: string = APP_METADATA_KEY_PREFIX) => {
|
||||
const isGlobalMetadataKey = GLOBAL_METADATA_KEYS.includes(key as AppMetadataKeys);
|
||||
@@ -33,7 +60,9 @@ export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends
|
||||
defaultValue?: Value,
|
||||
applyMigrations: boolean = true,
|
||||
): Value | undefined => {
|
||||
const metadata = applyMigrations ? applyMetadataMigrations(meta) : meta;
|
||||
const requiresMigration = Number(meta?.migration) !== METADATA_MIGRATIONS.length;
|
||||
const doMigration = requiresMigration && applyMigrations;
|
||||
const metadata = doMigration ? applyMetadataMigrations(meta) : meta;
|
||||
|
||||
if (
|
||||
metadata === undefined ||
|
||||
@@ -46,24 +75,187 @@ export const getMetadataValueFrom = <Key extends AppMetadataKeys, Value extends
|
||||
return convertValueFromMetadata(metadata[getMetadataKey(key)]);
|
||||
};
|
||||
|
||||
export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
{ meta }: MetadataHolder,
|
||||
const getMetadataUpdateFunction = (
|
||||
type: MetadataHolderType,
|
||||
metadataHolder:
|
||||
| MetadataHolder
|
||||
| (MangaIdInfo & MetadataHolder)
|
||||
| (ChapterIdInfo & MetadataHolder)
|
||||
| (CategoryIdInfo & MetadataHolder)
|
||||
| (Pick<SourceType, 'id'> & MetadataHolder),
|
||||
): ((appliedMigration: [MetadataKeyValuePair]) => Promise<void[]>) => {
|
||||
switch (type) {
|
||||
case 'global':
|
||||
return (appliedMigration) => requestUpdateServerMetadata(appliedMigration);
|
||||
case 'manga':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateMangaMetadata(
|
||||
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'chapter':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateChapterMetadata(
|
||||
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'category':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateCategoryMetadata(
|
||||
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'source':
|
||||
return (appliedMigration) =>
|
||||
requestUpdateSourceMetadata(
|
||||
{
|
||||
id: (metadataHolder as Pick<SourceType, 'id'>).id,
|
||||
meta: convertToGqlMeta(metadataHolder.meta),
|
||||
},
|
||||
appliedMigration,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
const getMetadataDeleteFunction = (
|
||||
type: MetadataHolderType,
|
||||
metadataHolder:
|
||||
| MetadataHolder
|
||||
| (MangaIdInfo & MetadataHolder)
|
||||
| (ChapterIdInfo & MetadataHolder)
|
||||
| (CategoryIdInfo & MetadataHolder)
|
||||
| (Pick<SourceType, 'id'> & MetadataHolder),
|
||||
): ((metadataToDelete: AppMetadataKeys[]) => Promise<void[]>) => {
|
||||
switch (type) {
|
||||
case 'global':
|
||||
return (appliedMigration) => requestDeleteServerMetadata(appliedMigration);
|
||||
case 'manga':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteMangaMetadata(
|
||||
{ id: (metadataHolder as MangaIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'chapter':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteChapterMetadata(
|
||||
{ id: (metadataHolder as ChapterIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'category':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteCategoryMetadata(
|
||||
{ id: (metadataHolder as CategoryIdInfo).id, meta: convertToGqlMeta(metadataHolder.meta) },
|
||||
appliedMigration,
|
||||
);
|
||||
case 'source':
|
||||
return (appliedMigration) =>
|
||||
requestDeleteSourceMetadata(
|
||||
{
|
||||
id: (metadataHolder as Pick<SourceType, 'id'>).id,
|
||||
meta: convertToGqlMeta(metadataHolder.meta),
|
||||
},
|
||||
appliedMigration,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected "type" (${type})`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent spamming requests due to frequent metadata reads while the migration hasn't been commited to the server yet
|
||||
*/
|
||||
const commitedMigrations = new Set<string>();
|
||||
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'global',
|
||||
metadataHolder: MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
): METADATA => {
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'manga',
|
||||
metadataHolder: MangaIdInfo & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'chapter',
|
||||
metadataHolder: ChapterIdInfo & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'category',
|
||||
metadataHolder: CategoryIdInfo & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: 'source',
|
||||
metadataHolder: Pick<SourceType, 'id'> & MetadataHolder,
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): METADATA;
|
||||
export function getMetadataFrom<METADATA extends Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>>(
|
||||
type: MetadataHolderType,
|
||||
metadataHolder:
|
||||
| MetadataHolder
|
||||
| (MangaIdInfo & MetadataHolder)
|
||||
| (ChapterIdInfo & MetadataHolder)
|
||||
| (CategoryIdInfo & MetadataHolder)
|
||||
| (Pick<SourceType, 'id'> & MetadataHolder),
|
||||
metadataWithDefaultValues: METADATA,
|
||||
applyMigrations?: boolean,
|
||||
useEffectFn: typeof useEffect = (fn: () => void) => fn(),
|
||||
): METADATA {
|
||||
const wasMigrated =
|
||||
!!metadataHolder?.meta &&
|
||||
!!applyMigrations &&
|
||||
Number(getMetadataValueFrom(metadataHolder, 'migration')) !== METADATA_MIGRATIONS.length;
|
||||
const appMetadata = {} as METADATA;
|
||||
|
||||
Object.entries(metadataWithDefaultValues).forEach(([key, defaultValue]) => {
|
||||
appMetadata[key as AppMetadataKeys] = getMetadataValueFrom(
|
||||
{ meta },
|
||||
metadataHolder,
|
||||
key as AppMetadataKeys,
|
||||
defaultValue,
|
||||
applyMigrations,
|
||||
);
|
||||
});
|
||||
|
||||
useEffectFn(() => {
|
||||
const itemMigrationKey = `${type}_${type === 'global' ? '' : (metadataHolder as { id: any }).id}`;
|
||||
const commitMigration = !commitedMigrations.has(itemMigrationKey);
|
||||
if (wasMigrated && commitMigration) {
|
||||
const metadataToDelete = Object.keys(metadataHolder.meta ?? {})
|
||||
.map(extractOriginalKey)
|
||||
.filter((key) => !VALID_APP_METADATA_KEYS.includes(key)) as AppMetadataKeys[];
|
||||
|
||||
commitedMigrations.add(itemMigrationKey);
|
||||
|
||||
getMetadataDeleteFunction(
|
||||
type,
|
||||
metadataHolder,
|
||||
)(metadataToDelete)
|
||||
.then(() =>
|
||||
getMetadataUpdateFunction(type, metadataHolder)([['migration', METADATA_MIGRATIONS.length]]),
|
||||
)
|
||||
.catch((error) => {
|
||||
defaultPromiseErrorHandler(`MetadataReader#getMetadataFromServer`)(error);
|
||||
commitedMigrations.delete(itemMigrationKey);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return appMetadata;
|
||||
};
|
||||
}
|
||||
|
||||
export const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
|
||||
const appMetadata: Metadata = {};
|
||||
|
||||
@@ -15,12 +15,11 @@ import {
|
||||
AllowedMetadataValueTypes,
|
||||
AppMetadataKeys,
|
||||
GqlMetaHolder,
|
||||
MetadataHolderType,
|
||||
MetadataKeyValuePair,
|
||||
} from '@/modules/metadata/Metadata.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
|
||||
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global' | 'source';
|
||||
|
||||
export const requestUpdateMetadataValue = async (
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
@@ -80,3 +79,62 @@ export const requestUpdateSourceMetadata = async (
|
||||
source: Pick<SourceType, 'id'> & GqlMetaHolder,
|
||||
keysToValue: MetadataKeyValuePair[],
|
||||
): Promise<void[]> => requestUpdateMetadata(source, 'source', keysToValue);
|
||||
|
||||
export const requestDeleteMetadataValue = async (
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
key: AppMetadataKeys,
|
||||
): Promise<void> => {
|
||||
const metadataKey = getMetadataKey(key);
|
||||
|
||||
switch (holderType) {
|
||||
case 'category':
|
||||
await requestManager.deleteCategoryMeta((metadataHolder as CategoryIdInfo).id, metadataKey).response;
|
||||
break;
|
||||
case 'chapter':
|
||||
await requestManager.deleteChapterMeta((metadataHolder as ChapterIdInfo).id, metadataKey).response;
|
||||
break;
|
||||
case 'global':
|
||||
await requestManager.deleteGlobalMeta(metadataKey).response;
|
||||
break;
|
||||
case 'manga':
|
||||
await requestManager.deleteMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey).response;
|
||||
break;
|
||||
case 'source':
|
||||
await requestManager.deleteSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey).response;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`requestDeleteMetadataValue: unknown holderType "${holderType}"`);
|
||||
}
|
||||
};
|
||||
|
||||
export async function requestDeleteMetadata(
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> {
|
||||
return Promise.all(keys.map((key) => requestDeleteMetadataValue(metadataHolder, holderType, key)));
|
||||
}
|
||||
|
||||
export const requestDeleteServerMetadata = async (keys: AppMetadataKeys[]): Promise<void[]> =>
|
||||
requestDeleteMetadata({}, 'global', keys);
|
||||
|
||||
export const requestDeleteMangaMetadata = async (
|
||||
manga: MangaIdInfo & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(manga, 'manga', keys);
|
||||
|
||||
export const requestDeleteChapterMetadata = async (
|
||||
chapter: ChapterIdInfo & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(chapter, 'chapter', keys);
|
||||
|
||||
export const requestDeleteCategoryMetadata = async (
|
||||
category: CategoryIdInfo & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(category, 'category', keys);
|
||||
|
||||
export const requestDeleteSourceMetadata = async (
|
||||
source: Pick<SourceType, 'id'> & GqlMetaHolder,
|
||||
keys: AppMetadataKeys[],
|
||||
): Promise<void[]> => requestDeleteMetadata(source, 'source', keys);
|
||||
|
||||
@@ -16,12 +16,12 @@ import { LoadingPlaceholder } from '@/modules/core/components/placeholder/Loadin
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { GridLayouts } from '@/modules/core/components/GridLayouts.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { GetSourceMigratableQuery, GetSourceMigratableQueryVariables } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_SOURCE_MIGRATABLE } from '@/lib/graphql/queries/SourceQuery.ts';
|
||||
import { SOURCE_BASE_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts';
|
||||
import { BaseMangaGrid } from '@/modules/manga/components/BaseMangaGrid.tsx';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export const Migrate = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -32,10 +32,6 @@ export interface IReaderSettings {
|
||||
readerWidth: number;
|
||||
}
|
||||
|
||||
export type UndefinedReaderSettings = {
|
||||
[setting in keyof IReaderSettings]: IReaderSettings[setting] | undefined;
|
||||
};
|
||||
|
||||
export interface IReaderPage {
|
||||
index: number;
|
||||
src: string;
|
||||
|
||||
@@ -9,19 +9,14 @@
|
||||
import { useContext, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestUpdateServerMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import {
|
||||
checkAndHandleMissingStoredReaderSettings,
|
||||
useDefaultReaderSettings,
|
||||
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { ReaderSettingsOptions } from '@/modules/reader/components/ReaderSettingsOptions.tsx';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.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() {
|
||||
@@ -38,7 +33,6 @@ export function DefaultReaderSettings() {
|
||||
}, [t]);
|
||||
|
||||
const {
|
||||
metadata,
|
||||
settings,
|
||||
loading,
|
||||
request: { error, refetch },
|
||||
@@ -66,12 +60,6 @@ export function DefaultReaderSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
checkAndHandleMissingStoredReaderSettings(
|
||||
{ meta: convertToGqlMeta(metadata)! },
|
||||
'server',
|
||||
DEFAULT_READER_SETTINGS,
|
||||
).catch(defaultPromiseErrorHandler('DefaultReaderSettings::checkAndHandleMissingStoredReaderSettings'));
|
||||
|
||||
return (
|
||||
<ReaderSettingsOptions
|
||||
setSettingValue={setSettingValue}
|
||||
|
||||
@@ -12,11 +12,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import Box from '@mui/material/Box';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import {
|
||||
checkAndHandleMissingStoredReaderSettings,
|
||||
getReaderSettingsFor,
|
||||
useDefaultReaderSettings,
|
||||
} from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { getReaderSettingsFor, useDefaultReaderSettings } from '@/modules/reader/services/ReaderSettingsMetadata.ts';
|
||||
import { requestUpdateMangaMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { HorizontalPager } from '@/modules/reader/components/pager/HorizontalPager.tsx';
|
||||
import { PageNumber } from '@/modules/reader/components/page/PageNumber.tsx';
|
||||
@@ -219,7 +215,7 @@ export function Reader() {
|
||||
const error = mangaError ?? chapterError ?? chaptersError ?? pagesError;
|
||||
|
||||
const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings();
|
||||
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
|
||||
const [settings, setSettings] = useState(defaultSettings);
|
||||
|
||||
const { settings: metadataSettings } = useMetadataServerSettings();
|
||||
|
||||
@@ -419,9 +415,6 @@ export function Reader() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!areDefaultSettingsLoading && !isMangaLoading) {
|
||||
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(
|
||||
defaultPromiseErrorHandler('Reader::checkAndHandleMissingStoredReaderSettings'),
|
||||
);
|
||||
setSettings(getReaderSettingsFor(manga, defaultSettings));
|
||||
}
|
||||
}, [areDefaultSettingsLoading, isMangaLoading]);
|
||||
|
||||
@@ -6,37 +6,69 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import {
|
||||
requestUpdateMangaMetadata,
|
||||
requestUpdateServerMetadata,
|
||||
} from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { DEFAULT_READER_SETTINGS } from '@/modules/reader/Reader.constants.ts';
|
||||
import { IReaderSettings, UndefinedReaderSettings } from '@/modules/reader/Reader.types.ts';
|
||||
import { IReaderSettings } 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';
|
||||
import { GqlMetaHolder, Metadata, MetadataHolder, MetadataHolderType } from '@/modules/metadata/Metadata.types.ts';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
|
||||
const getReaderSettingsWithDefaultValueFallback = <DefaultSettings extends IReaderSettings | UndefinedReaderSettings>(
|
||||
meta?: Metadata,
|
||||
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
|
||||
type: 'global',
|
||||
metadataHolder: MetadataHolder,
|
||||
defaultSettings?: DefaultSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): DefaultSettings;
|
||||
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
|
||||
type: 'manga',
|
||||
metadataHolder: MangaIdInfo & MetadataHolder,
|
||||
defaultSettings?: DefaultSettings,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): DefaultSettings;
|
||||
function getReaderSettingsWithDefaultValueFallback<DefaultSettings extends IReaderSettings>(
|
||||
type: Extract<MetadataHolderType, 'global' | 'manga'>,
|
||||
metadataHolder: (MangaIdInfo & MetadataHolder) | MetadataHolder,
|
||||
defaultSettings: DefaultSettings = DEFAULT_READER_SETTINGS as DefaultSettings,
|
||||
applyMetadataMigration: boolean = true,
|
||||
): DefaultSettings => getMetadataFrom({ meta }, defaultSettings, applyMetadataMigration);
|
||||
useEffectFn?: typeof useEffect,
|
||||
): DefaultSettings {
|
||||
return getMetadataFrom(
|
||||
type as Parameters<typeof getMetadataFrom>[0],
|
||||
metadataHolder as Parameters<typeof getMetadataFrom>[1],
|
||||
defaultSettings,
|
||||
true,
|
||||
useEffectFn,
|
||||
);
|
||||
}
|
||||
|
||||
export const getReaderSettingsFromMetadata = (
|
||||
meta?: Metadata,
|
||||
const getSettings = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings?: IReaderSettings,
|
||||
applyMetadataMigration?: boolean,
|
||||
): IReaderSettings => getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration);
|
||||
useEffectFn?: typeof useEffect,
|
||||
) =>
|
||||
getReaderSettingsWithDefaultValueFallback(
|
||||
'manga',
|
||||
{
|
||||
...metaHolder,
|
||||
meta: convertFromGqlMeta(metaHolder.meta),
|
||||
},
|
||||
defaultSettings,
|
||||
useEffectFn,
|
||||
);
|
||||
|
||||
export const getReaderSettingsFor = (
|
||||
{ meta }: GqlMetaHolder = {},
|
||||
defaultSettings?: IReaderSettings,
|
||||
applyMetadataMigration?: boolean,
|
||||
): IReaderSettings => getReaderSettingsFromMetadata(convertFromGqlMeta(meta), defaultSettings, applyMetadataMigration);
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings: IReaderSettings,
|
||||
): IReaderSettings => getSettings(metaHolder, defaultSettings);
|
||||
|
||||
export const useGetReaderSettingsFor = (
|
||||
metaHolder: MangaIdInfo & GqlMetaHolder,
|
||||
defaultSettings: IReaderSettings,
|
||||
): IReaderSettings => {
|
||||
const settings = getSettings(metaHolder, defaultSettings, useEffect);
|
||||
return useMemo(() => settings, [metaHolder, defaultSettings]);
|
||||
};
|
||||
|
||||
export const useDefaultReaderSettings = (): {
|
||||
metadata?: Metadata;
|
||||
@@ -47,7 +79,9 @@ export const useDefaultReaderSettings = (): {
|
||||
const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true });
|
||||
const { data, loading } = request;
|
||||
const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]);
|
||||
const settings = useMemo(() => getReaderSettingsWithDefaultValueFallback<IReaderSettings>(metadata), [metadata]);
|
||||
const metaHolder: MetadataHolder = useMemo(() => ({ meta: metadata }), [metadata]);
|
||||
const tmpSettings = getReaderSettingsWithDefaultValueFallback('global', metaHolder, undefined, useEffect);
|
||||
const settings = useMemo(() => tmpSettings, [metaHolder]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
@@ -59,55 +93,3 @@ export const useDefaultReaderSettings = (): {
|
||||
[metadata, settings, loading, request],
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves all missing reader settings from the metadata to the server
|
||||
*
|
||||
* @param metadataHolder
|
||||
* @param metadataHolderType
|
||||
* @param defaultSettings
|
||||
*/
|
||||
export const checkAndHandleMissingStoredReaderSettings = async (
|
||||
metadataHolder: Required<GqlMetaHolder> | MetaType[],
|
||||
metadataHolderType: 'manga' | 'server',
|
||||
defaultSettings: IReaderSettings,
|
||||
): Promise<void | void[]> => {
|
||||
const getMeta = () => (Array.isArray(metadataHolder) ? metadataHolder : metadataHolder.meta);
|
||||
const meta = convertFromGqlMeta(getMeta())!;
|
||||
const settingsToCheck = getReaderSettingsWithDefaultValueFallback(
|
||||
meta,
|
||||
{
|
||||
staticNav: undefined,
|
||||
showPageNumber: undefined,
|
||||
loadNextOnEnding: undefined,
|
||||
skipDupChapters: undefined,
|
||||
fitPageToWindow: undefined,
|
||||
scalePage: undefined,
|
||||
readerType: undefined,
|
||||
offsetFirstPage: undefined,
|
||||
readerWidth: undefined,
|
||||
},
|
||||
false,
|
||||
);
|
||||
const newSettings = getReaderSettingsFor({ meta: getMeta() }, defaultSettings);
|
||||
|
||||
const undefinedSettings = Object.entries(settingsToCheck).filter((setting) => setting[1] === undefined);
|
||||
|
||||
const settingsToUpdate: MetadataKeyValuePair[] = [];
|
||||
undefinedSettings.forEach((setting) => {
|
||||
const key = setting[0] as keyof IReaderSettings;
|
||||
|
||||
settingsToUpdate.push([key, newSettings[key]]);
|
||||
});
|
||||
|
||||
if (!undefinedSettings.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadataHolderType === 'manga') {
|
||||
await requestUpdateMangaMetadata(metadataHolder as MangaIdInfo & GqlMetaHolder, settingsToUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
await requestUpdateServerMetadata(settingsToUpdate);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||
import { requestUpdateServerMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||
@@ -48,10 +48,10 @@ export const convertMetadataToSettings = (
|
||||
const getMetadataServerSettingsWithDefaultFallback = (
|
||||
meta?: Metadata,
|
||||
defaultSettings: MetadataServerSettings = SERVER_SETTINGS_METADATA_DEFAULT,
|
||||
applyMetadataMigration: boolean = true,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): MetadataServerSettings =>
|
||||
convertMetadataToSettings(
|
||||
getMetadataFrom({ meta }, convertSettingsToMetadata(defaultSettings), applyMetadataMigration),
|
||||
getMetadataFrom('global', { meta }, convertSettingsToMetadata(defaultSettings), true, useEffectFn),
|
||||
);
|
||||
export const useMetadataServerSettings = (): {
|
||||
metadata?: Metadata;
|
||||
@@ -62,7 +62,8 @@ export const useMetadataServerSettings = (): {
|
||||
const request = requestManager.useGetGlobalMeta({ notifyOnNetworkStatusChange: true });
|
||||
const { data, loading } = request;
|
||||
const metadata = useMemo(() => convertFromGqlMeta(data?.metas.nodes), [data?.metas.nodes]);
|
||||
const settings = useMemo(() => getMetadataServerSettingsWithDefaultFallback(metadata), [metadata]);
|
||||
const tmpSettings = getMetadataServerSettingsWithDefaultFallback(metadata, undefined, useEffect);
|
||||
const settings = useMemo(() => tmpSettings, [metadata]);
|
||||
|
||||
return useMemo(() => ({ metadata, settings, loading, request }), [metadata, settings, loading, request]);
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { GridLayouts } from '@/modules/core/components/GridLayouts.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
export function SourceGridLayout() {
|
||||
const [sourceGridLayout, setSourceGridLayout] = useLocalStorage('source-grid-layout', GridLayout.Compact);
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
AbortableApolloUseMutationPaginatedResponse,
|
||||
SPECIAL_ED_SOURCES,
|
||||
} from '@/lib/requests/RequestManager.ts';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { SourceGridLayout } from '@/modules/source/components/SourceGridLayout.tsx';
|
||||
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
|
||||
import { SourceOptions } from '@/modules/source/components/SourceOptions.tsx';
|
||||
@@ -35,13 +34,14 @@ import {
|
||||
GetSourceBrowseQueryVariables,
|
||||
GetSourceMangasFetchMutation,
|
||||
GetSourceMangasFetchMutationVariables,
|
||||
SourceType,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
import { NavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx';
|
||||
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts';
|
||||
import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { AppStorage } from '@/lib/storage/AppStorage.ts';
|
||||
import { getGridSnapshotKey } from '@/modules/manga/components/MangaGrid.tsx';
|
||||
import { createUpdateSourceMetadata, getSourceMetadata } from '@/modules/source/services/SourceMetadata.ts';
|
||||
import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/modules/source/services/SourceMetadata.ts';
|
||||
import { makeToast } from '@/modules/core/utils/Toast.ts';
|
||||
import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
@@ -50,6 +50,9 @@ import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'
|
||||
import { EmptyView } from '@/modules/core/components/placeholder/EmptyView.tsx';
|
||||
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
|
||||
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
|
||||
import { GridLayout } from '@/modules/core/Core.types.ts';
|
||||
|
||||
const DEFAULT_SOURCE: Pick<SourceType, 'id'> = { id: '-1' };
|
||||
|
||||
const ContentTypeMenu = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@@ -285,8 +288,8 @@ export function SourceMangas() {
|
||||
const source = sourceData?.source;
|
||||
|
||||
const filters = source?.filters ?? [];
|
||||
const { savedSearches = {} } = useMemo(() => getSourceMetadata(source), [source, source?.meta]);
|
||||
const updateSourceMetadata = createUpdateSourceMetadata<'savedSearches'>(source ?? { id: sourceId }, () =>
|
||||
const { savedSearches = {} } = useGetSourceMetadata(source ?? DEFAULT_SOURCE);
|
||||
const updateSourceMetadata = createUpdateSourceMetadata<'savedSearches'>(source ?? { id: '-1' }, () =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error'),
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { jsonSaveParse } from '@/lib/HelperFunctions.ts';
|
||||
import { requestUpdateSourceMetadata } from '@/modules/metadata/services/MetadataUpdater.ts';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
@@ -20,6 +21,10 @@ import {
|
||||
Metadata,
|
||||
} from '@/modules/metadata/Metadata.types.ts';
|
||||
|
||||
const DEFAULT_SOURCE_METADATA: ISourceMetadata = {
|
||||
savedSearches: undefined,
|
||||
};
|
||||
|
||||
const convertAppMetadataToGqlMetadata = (
|
||||
metadata: Partial<ISourceMetadata>,
|
||||
): Metadata<string, AllowedMetadataValueTypes> => ({
|
||||
@@ -27,18 +32,35 @@ const convertAppMetadataToGqlMetadata = (
|
||||
savedSearches: metadata.savedSearches ? JSON.stringify(metadata.savedSearches) : undefined,
|
||||
});
|
||||
|
||||
export const convertGqlMetadataToAppMetadata = (
|
||||
const convertGqlMetadataToAppMetadata = (
|
||||
metadata: Partial<Metadata<AppMetadataKeys, AllowedMetadataValueTypes>>,
|
||||
): ISourceMetadata => ({
|
||||
...(metadata as unknown as ISourceMetadata),
|
||||
savedSearches: jsonSaveParse<ISourceMetadata['savedSearches']>(metadata.savedSearches as string) ?? undefined,
|
||||
});
|
||||
|
||||
export const getSourceMetadata = ({ meta }: GqlMetaHolder = {}, applyMetadataMigration?: boolean): ISourceMetadata =>
|
||||
const getMetadata = (
|
||||
metaHolder: Pick<SourceType, 'id'> & GqlMetaHolder,
|
||||
useEffectFn?: typeof useEffect,
|
||||
): ISourceMetadata =>
|
||||
convertGqlMetadataToAppMetadata(
|
||||
getMetadataFrom({ meta: convertFromGqlMeta(meta) }, { savedSearches: undefined }, applyMetadataMigration),
|
||||
getMetadataFrom(
|
||||
'source',
|
||||
{ ...metaHolder, meta: convertFromGqlMeta(metaHolder.meta) },
|
||||
convertAppMetadataToGqlMetadata(DEFAULT_SOURCE_METADATA),
|
||||
true,
|
||||
useEffectFn,
|
||||
),
|
||||
);
|
||||
|
||||
export const getSourceMetadata = (metaHolder: Pick<SourceType, 'id'> & GqlMetaHolder): ISourceMetadata =>
|
||||
getMetadata(metaHolder);
|
||||
|
||||
export const useGetSourceMetadata = (metaHolder: Pick<SourceType, 'id'> & GqlMetaHolder): ISourceMetadata => {
|
||||
const metadata = getMetadata(metaHolder, useEffect);
|
||||
return useMemo(() => metadata, [metaHolder]);
|
||||
};
|
||||
|
||||
export const updateSourceMetadata = async <
|
||||
MetadataKeys extends SourceMetadataKeys = SourceMetadataKeys,
|
||||
MetadataKey extends MetadataKeys = MetadataKeys,
|
||||
|
||||
Reference in New Issue
Block a user