Cleanup deprecated metadata after migration

- delete deprecated metadata
- save applied migration id per metadata holder
This commit is contained in:
schroda
2024-10-26 17:25:04 +02:00
parent b22cec12b4
commit e71c4b7ef1
34 changed files with 687 additions and 183 deletions

View File

@@ -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;
}>;

View File

@@ -38,12 +38,6 @@ export const DELETE_CATEGORY_METADATA = gql`
meta {
...CATEGORY_META_FIELDS
}
category {
id
meta {
...CATEGORY_META_FIELDS
}
}
}
}
`;

View File

@@ -19,12 +19,6 @@ export const DELETE_CHAPTER_METADATA = gql`
meta {
...CHAPTER_META_FIELDS
}
chapter {
id
meta {
...CHAPTER_META_FIELDS
}
}
}
}
`;

View File

@@ -17,12 +17,6 @@ export const DELETE_MANGA_METADATA = gql`
meta {
...MANGA_META_FIELDS
}
manga {
id
meta {
...MANGA_META_FIELDS
}
}
}
}
`;

View File

@@ -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
}
}
}
`;

View File

@@ -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>,

View File

@@ -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,

View File

@@ -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({

View File

@@ -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'>>,

View File

@@ -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')),
);

View File

@@ -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: () => {},
});

View File

@@ -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],
);

View File

@@ -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,

View File

@@ -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} />

View File

@@ -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();

View File

@@ -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';

View File

@@ -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}>

View File

@@ -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,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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';

View 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()];
};

View File

@@ -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;

View File

@@ -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 = {};

View File

@@ -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);

View File

@@ -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();

View File

@@ -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;

View File

@@ -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}

View File

@@ -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]);

View File

@@ -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);
};

View File

@@ -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]);
};

View File

@@ -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);

View File

@@ -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'),
);

View File

@@ -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,