Use gql for "mangas" I - get manga

This commit is contained in:
schroda
2023-09-08 00:44:01 +02:00
parent a5b4b95bce
commit 89dc053f00
14 changed files with 170 additions and 126 deletions

View File

@@ -10,14 +10,15 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import PublicIcon from '@mui/icons-material/Public'; import PublicIcon from '@mui/icons-material/Public';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import React, { useEffect } from 'react'; import React, { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { mutate } from 'swr'; import { mutate } from 'swr';
import { t as translate } from 'i18next'; import { t as translate } from 'i18next';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { IManga, ISource } from '@/typings'; import { ISource } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
width: '100%', width: '100%',
@@ -125,11 +126,36 @@ const Genres = styled('div')(() => ({
}, },
})); }));
const OpenSourceButton = ({ url }: { url?: string | null }) => {
const { t } = useTranslation();
const button = useMemo(
() => (
<Button disabled={!!url} startIcon={<PublicIcon />} size="large">
{t('global.button.open_site')}
</Button>
),
[url],
);
if (!url) {
return button;
}
return (
<a href={url} target="_blank" rel="noreferrer">
<Button startIcon={<PublicIcon />} size="large">
{t('global.button.open_site')}
</Button>
</a>
);
};
interface IProps { interface IProps {
manga: IManga; manga: MangaType;
} }
function getSourceName(source: ISource) { function getSourceName(source?: ISource | null) {
if (!source) { if (!source) {
return translate('global.label.unknown'); return translate('global.label.unknown');
} }
@@ -137,7 +163,7 @@ function getSourceName(source: ISource) {
return source.displayName ?? source.id; return source.displayName ?? source.id;
} }
function getValueOrUnknown(val: string) { function getValueOrUnknown(val?: string | null) {
return val || 'UNKNOWN'; return val || 'UNKNOWN';
} }
@@ -169,7 +195,9 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
<TopContentWrapper> <TopContentWrapper>
<ThumbnailMetadataWrapper> <ThumbnailMetadataWrapper>
<Thumbnail> <Thumbnail>
<img src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} alt="Manga Thumbnail" /> {manga.thumbnailUrl && (
<img src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} alt="Manga Thumbnail" />
)}
</Thumbnail> </Thumbnail>
<Metadata> <Metadata>
<h1>{manga.title}</h1> <h1>{manga.title}</h1>
@@ -195,11 +223,7 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')} {manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
</Button> </Button>
</div> </div>
<a href={manga.realUrl} target="_blank" rel="noreferrer"> <OpenSourceButton url={manga.realUrl} />
<Button startIcon={<PublicIcon />} size="large">
{t('global.button.open_site')}
</Button>
</a>
</MangaButtonsContainer> </MangaButtonsContainer>
</TopContentWrapper> </TopContentWrapper>
<BottomContentWrapper> <BottomContentWrapper>

View File

@@ -21,11 +21,11 @@ import {
} from '@mui/material'; } from '@mui/material';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IManga } from '@/typings';
import CategorySelect from '@/components/navbar/action/CategorySelect'; import CategorySelect from '@/components/navbar/action/CategorySelect';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
interface IProps { interface IProps {
manga: IManga; manga: MangaType;
onRefresh: () => any; onRefresh: () => any;
refreshing: boolean; refreshing: boolean;
} }

View File

@@ -16,7 +16,7 @@ export const useRefreshManga = (mangaId: string) => {
const handleRefresh = useCallback(async () => { const handleRefresh = useCallback(async () => {
setFetchingOnline(true); setFetchingOnline(true);
await Promise.all([ await Promise.all([
requestManager.getManga(mangaId, true).response.then((res) => { requestManager.getMangaFetch(mangaId).response.then((res) => {
mutate(`${RequestManager.API_VERSION}manga/${mangaId}`, res, { revalidate: false }); mutate(`${RequestManager.API_VERSION}manga/${mangaId}`, res, { revalidate: false });
}), }),
requestManager.getMangaChapters(mangaId, true).response.then((res) => requestManager.getMangaChapters(mangaId, true).response.then((res) =>

View File

@@ -24,8 +24,9 @@ import ListItemText from '@mui/material/ListItemText';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction'; import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChapterOffset, IChapter, IManga, IMangaCard, IReaderSettings } from '@/typings'; import { ChapterOffset, IChapter, IReaderSettings } from '@/typings';
import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions'; import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
const Root = styled('div')(({ theme }) => ({ const Root = styled('div')(({ theme }) => ({
top: 0, top: 0,
@@ -114,7 +115,7 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
interface IProps { interface IProps {
settings: IReaderSettings; settings: IReaderSettings;
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void; setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
manga: IManga | IMangaCard; manga: Pick<MangaType, 'id'>;
chapter: IChapter; chapter: IChapter;
curPage: number; curPage: number;
scrollToPage: (page: number) => void; scrollToPage: (page: number) => void;

View File

@@ -22,7 +22,7 @@ function getRandomErrorFace() {
interface IProps { interface IProps {
message: string; message: string;
messageExtra?: JSX.Element; messageExtra?: JSX.Element | string;
} }
export default function EmptyView({ message, messageExtra }: IProps) { export default function EmptyView({ message, messageExtra }: IProps) {

View File

@@ -69,6 +69,10 @@ import {
GetExtensionsQueryVariables, GetExtensionsQueryVariables,
GetGlobalMetadatasQuery, GetGlobalMetadatasQuery,
GetGlobalMetadatasQueryVariables, GetGlobalMetadatasQueryVariables,
GetMangaFetchMutation,
GetMangaFetchMutationVariables,
GetMangaQuery,
GetMangaQueryVariables,
GetSourceQuery, GetSourceQuery,
GetSourceQueryVariables, GetSourceQueryVariables,
GetSourcesQuery, GetSourcesQuery,
@@ -125,7 +129,12 @@ import {
UPDATE_EXTENSION, UPDATE_EXTENSION,
} from '@/lib/graphql/mutations/ExtensionMutation.ts'; } from '@/lib/graphql/mutations/ExtensionMutation.ts';
import { GET_SOURCE, GET_SOURCES } from '@/lib/graphql/queries/SourceQuery.ts'; import { GET_SOURCE, GET_SOURCES } from '@/lib/graphql/queries/SourceQuery.ts';
import { SET_MANGA_METADATA, UPDATE_MANGA, UPDATE_MANGA_CATEGORIES } from '@/lib/graphql/mutations/MangaMutation.ts'; import {
GET_MANGA_FETCH,
SET_MANGA_METADATA,
UPDATE_MANGA,
UPDATE_MANGA_CATEGORIES,
} from '@/lib/graphql/mutations/MangaMutation.ts';
import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts'; import { GET_MANGA, GET_MANGAS } from '@/lib/graphql/queries/MangaQuery.ts';
import { GET_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_CATEGORIES, GET_CATEGORY, GET_CATEGORY_MANGAS } from '@/lib/graphql/queries/CategoryQuery.ts';
import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation.ts'; import { GET_SOURCE_MANGAS_FETCH } from '@/lib/graphql/mutations/SourceMutation.ts';
@@ -712,27 +721,28 @@ export class RequestManager {
public useGetManga( public useGetManga(
mangaId: number | string, mangaId: number | string,
{ doOnlineFetch, ...swrOptions }: SWROptions<IManga> & RequestOption = {}, options?: QueryHookOptions<GetMangaQuery, GetMangaQueryVariables>,
): AbortableSWRResponse<IManga> { ): AbortableApolloUseQueryResponse<GetMangaQuery, GetMangaQueryVariables> {
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : ''; return this.doRequestNew(GQLMethod.USE_QUERY, GET_MANGA, { id: Number(mangaId) }, options);
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}${onlineFetch}`, {
swrOptions,
});
} }
public getManga(mangaId: number | string, doOnlineFetch?: boolean): AbortableAxiosResponse<IManga> { public getMangaFetch(
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
return this.doRequest(HttpMethod.GET, `manga/${mangaId}${onlineFetch}`);
}
public useGetFullManga(
mangaId: number | string, mangaId: number | string,
{ doOnlineFetch, ...swrOptions }: SWROptions<IManga> & RequestOption = {}, options?: MutationOptions<GetMangaFetchMutation, GetMangaFetchMutationVariables>,
): AbortableSWRResponse<IManga> { ): AbortableApolloMutationResponse<GetMangaFetchMutation> {
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : ''; return this.doRequestNew<GetMangaFetchMutation, GetMangaFetchMutationVariables>(
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/full${onlineFetch}`, { GQLMethod.MUTATION,
swrOptions, GET_MANGA_FETCH,
}); {
input: {
id: Number(mangaId),
},
},
{
refetchQueries: [GET_MANGA, GET_MANGAS],
...options,
},
);
} }
public getMangaThumbnailUrl(mangaId: number): string { public getMangaThumbnailUrl(mangaId: number): string {

View File

@@ -11,6 +11,7 @@ import { CircularProgress, IconButton, Stack, Tooltip, Box } from '@mui/material
import React, { useContext, useEffect, useRef } from 'react'; import React, { useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext'; import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
import ChapterList from '@/components/manga/ChapterList'; import ChapterList from '@/components/manga/ChapterList';
@@ -19,8 +20,9 @@ import MangaDetails from '@/components/manga/MangaDetails';
import MangaToolbarMenu from '@/components/manga/MangaToolbarMenu'; import MangaToolbarMenu from '@/components/manga/MangaToolbarMenu';
import EmptyView from '@/components/util/EmptyView'; import EmptyView from '@/components/util/EmptyView';
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder'; import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
const AUTOFETCH_AGE = 60 * 60 * 24; // 24 hours const AUTOFETCH_AGE = 1000 * 60 * 60 * 24; // 24 hours
const Manga: React.FC = () => { const Manga: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -29,21 +31,26 @@ const Manga: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const autofetchedRef = useRef(false); const autofetchedRef = useRef(false);
const { data: manga, error, isLoading, isValidating, mutate } = requestManager.useGetManga(id); const { data, error, loading: isLoading, networkStatus, refetch } = requestManager.useGetManga(id);
const isValidating = isNetworkRequestInFlight(networkStatus);
const manga = data?.manga as MangaType | undefined;
const [refresh, { loading: refreshing }] = useRefreshManga(id); const [refresh, { loading: refreshing }] = useRefreshManga(id);
useSetDefaultBackTo('library'); useSetDefaultBackTo('library');
useEffect(() => { useEffect(() => {
// Automatically fetch manga from source if data is older then 24 hours // Automatically fetch manga from source if data is older then 24 hours OR manga is not initialized yet
// Automatic fetch is done only once, to prevent issues when server does // Automatic fetch is done only once, to prevent issues when server does
// not update age for some reason (ie. error on source side) // not update age for some reason (ie. error on source side)
if (manga == null) return; if (manga == null) return;
if (
manga.inLibrary && const isOutdated =
(manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE) && Date.now() - Number(manga.lastFetchedAt) * 1000 > AUTOFETCH_AGE ||
autofetchedRef.current === false Date.now() - Number(manga.chaptersLastFetchedAt) * 1000 > AUTOFETCH_AGE;
) { const refetchBecauseOutdated = manga.inLibrary && isOutdated;
const doFetch = !autofetchedRef.current && (refetchBecauseOutdated || !manga.initialized);
if (doFetch) {
autofetchedRef.current = true; autofetchedRef.current = true;
refresh(); refresh();
} }
@@ -67,7 +74,7 @@ const Manga: React.FC = () => {
</> </>
} }
> >
<IconButton onClick={() => mutate()}> <IconButton onClick={() => refetch()}>
<Warning color="error" /> <Warning color="error" />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -80,7 +87,7 @@ const Manga: React.FC = () => {
{manga && <MangaToolbarMenu manga={manga} onRefresh={refresh} refreshing={refreshing} />} {manga && <MangaToolbarMenu manga={manga} onRefresh={refresh} refreshing={refreshing} />}
</Stack>, </Stack>,
); );
}, [t, error, isValidating, refreshing, mutate, manga, refresh]); }, [t, error, isValidating, refreshing, manga, refresh]);
if (error && !manga) { if (error && !manga) {
return <EmptyView message={t('manga.error.label.request_failure')} messageExtra={error.message ?? error} />; return <EmptyView message={t('manga.error.label.request_failure')} messageExtra={error.message ?? error} />;

View File

@@ -7,11 +7,11 @@
*/ */
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { useCallback, useContext, useEffect, useState } from 'react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ChapterOffset, IChapter, IManga, IMangaCard, IReaderSettings, ReaderType, TranslationKey } from '@/typings'; import { ChapterOffset, IChapter, IReaderSettings, ReaderType, TranslationKey } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { import {
checkAndHandleMissingStoredReaderSettings, checkAndHandleMissingStoredReaderSettings,
@@ -27,6 +27,7 @@ import VerticalPager from '@/components/reader/pager/VerticalPager';
import ReaderNavBar from '@/components/navbar/ReaderNavBar'; import ReaderNavBar from '@/components/navbar/ReaderNavBar';
import NavbarContext from '@/components/context/NavbarContext'; import NavbarContext from '@/components/context/NavbarContext';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => { const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => {
const nextChapter = await requestManager.getChapter(currentChapter.mangaId, chapterIndex).response; const nextChapter = await requestManager.getChapter(currentChapter.mangaId, chapterIndex).response;
@@ -93,17 +94,22 @@ export default function Reader() {
const location = useLocation(); const location = useLocation();
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>(); const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
const {
data: manga = { const initialManga = useMemo(
id: +mangaId, () =>
title: '', ({
thumbnailUrl: '', id: +mangaId,
genre: [], title: '',
inLibraryAt: 0, thumbnailUrl: '',
lastReadAt: 0, genre: [],
} as IMangaCard | IManga, inLibraryAt: 0,
isLoading: isMangaLoading, lastReadAt: 0,
} = requestManager.useGetManga(mangaId); }) as unknown as MangaType,
[mangaId],
);
const { data, loading: isMangaLoading } = requestManager.useGetManga(mangaId);
const manga = (data?.manga as MangaType) ?? initialManga;
const { data: chapter = initialChapter, isLoading: isChapterLoading } = requestManager.useGetChapter( const { data: chapter = initialChapter, isLoading: isChapterLoading } = requestManager.useGetChapter(
mangaId, mangaId,
chapterIndex, chapterIndex,

View File

@@ -11,7 +11,7 @@ import { Box } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { IReaderSettings } from '@/typings'; import { IReaderSettings } from '@/typings';
import { requestUpdateServerMetadata } from '@/util/metadata'; import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
import { import {
checkAndHandleMissingStoredReaderSettings, checkAndHandleMissingStoredReaderSettings,
getDefaultSettings, getDefaultSettings,
@@ -34,7 +34,7 @@ export default function DefaultReaderSettings() {
useSetDefaultBackTo('settings'); useSetDefaultBackTo('settings');
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => { const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [[key, value]]).catch(() =>
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'), makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
); );
}; };
@@ -54,7 +54,11 @@ export default function DefaultReaderSettings() {
); );
} }
checkAndHandleMissingStoredReaderSettings({ meta: metadata }, 'server', getDefaultSettings()).catch(() => {}); checkAndHandleMissingStoredReaderSettings(
{ meta: convertToGqlMeta(metadata)! },
'server',
getDefaultSettings(),
).catch(() => {});
return ( return (
<ReaderSettingsOptions <ReaderSettingsOptions

View File

@@ -12,7 +12,7 @@ import ListItemIcon from '@mui/material/ListItemIcon';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SearchMetadataKeys } from '@/typings'; import { SearchMetadataKeys } from '@/typings';
import { requestUpdateServerMetadata } from '@/util/metadata'; import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
import { useSearchSettings } from '@/util/searchSettings'; import { useSearchSettings } from '@/util/searchSettings';
import makeToast from '@/components/util/Toast'; import makeToast from '@/components/util/Toast';
import { useSetDefaultBackTo } from '@/components/context/NavbarContext'; import { useSetDefaultBackTo } from '@/components/context/NavbarContext';
@@ -24,7 +24,7 @@ export default function SearchSettings() {
useSetDefaultBackTo('settings'); useSetDefaultBackTo('settings');
const setSettingValue = (key: SearchMetadataKeys, value: boolean) => { const setSettingValue = (key: SearchMetadataKeys, value: boolean) => {
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [[key, value]]).catch(() =>
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'), makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),
); );
}; };

View File

@@ -10,7 +10,7 @@ import { OverridableComponent } from '@mui/material/OverridableComponent';
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon'; import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
import { ParseKeys } from 'i18next'; import { ParseKeys } from 'i18next';
import { Location } from 'react-router-dom'; import { Location } from 'react-router-dom';
import { ExtensionType } from '@/lib/graphql/generated/graphql.ts'; import { ExtensionType, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts';
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State }; type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
@@ -77,6 +77,8 @@ export type Metadata<Keys extends string = string, Values = string> = {
[key in Keys]: Values; [key in Keys]: Values;
}; };
export type GqlMetaHolder = { meta?: MetaType[] };
export type MetadataHolder<Keys extends string = string, Values = string> = { export type MetadataHolder<Keys extends string = string, Values = string> = {
meta?: Metadata<Keys, Values>; meta?: Metadata<Keys, Values>;
}; };
@@ -236,7 +238,7 @@ export interface IReaderProps {
curPage: number; curPage: number;
initialPage: number; initialPage: number;
settings: IReaderSettings; settings: IReaderSettings;
manga: IMangaCard | IManga; manga: MangaType;
chapter: IChapter | IPartialChapter; chapter: IChapter | IPartialChapter;
nextChapter: () => void; nextChapter: () => void;
prevChapter: () => void; prevChapter: () => void;

View File

@@ -6,21 +6,17 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { mutate } from 'swr';
import { import {
AllowedMetadataValueTypes, AllowedMetadataValueTypes,
AppMetadataKeys, AppMetadataKeys,
ICategory, GqlMetaHolder,
IManga,
IMangaCard,
IMangaChapter,
IMetadataMigration, IMetadataMigration,
Metadata, Metadata,
MetadataHolder, MetadataHolder,
MetadataKeyValuePair, MetadataKeyValuePair,
} from '@/typings'; } from '@/typings';
import requestManager, { RequestManager } from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { MetaType } from '@/lib/graphql/generated/graphql.ts'; import { CategoryType, ChapterType, MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts';
const APP_METADATA_KEY_PREFIX = 'webUI_'; const APP_METADATA_KEY_PREFIX = 'webUI_';
@@ -118,6 +114,27 @@ const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedM
return value as T; return value as T;
}; };
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
if (!gqlMetadata) {
return undefined;
}
const metadata: Metadata = {};
gqlMetadata.forEach(({ key, value }) => {
metadata[key] = value;
});
return metadata;
};
export const convertToGqlMeta = (metadata?: Metadata): MetaType[] | undefined => {
if (!metadata) {
return undefined;
}
return Object.entries(metadata).map(([key, value]) => ({ key, value }));
};
const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => { const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
const appMetadata: Metadata = {}; const appMetadata: Metadata = {};
@@ -277,6 +294,8 @@ export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKey
return appMetadata; return appMetadata;
}; };
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHolder => { const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHolder => {
if (wrap) { if (wrap) {
return { return {
@@ -294,85 +313,54 @@ const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHol
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global'; type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global';
export const requestUpdateMetadataValue = async ( export const requestUpdateMetadataValue = async (
metadataHolder: MetadataHolder, metadataHolder: GqlMetaHolder,
holderType: MetadataHolderType, holderType: MetadataHolderType,
key: AppMetadataKeys, key: AppMetadataKeys,
value: AllowedMetadataValueTypes, value: AllowedMetadataValueTypes,
): Promise<void> => { ): Promise<void> => {
const metadataKey = getMetadataKey(key); const metadataKey = getMetadataKey(key);
const mutatedMetadata = {
...metadataHolder.meta,
[metadataKey]: `${value}`,
};
let endpoint: string;
switch (holderType) { switch (holderType) {
case 'category': case 'category':
endpoint = `category/${(metadataHolder as ICategory).id}/meta`; await requestManager.setCategoryMeta((metadataHolder as CategoryType).id, metadataKey, value).response;
await requestManager.setCategoryMeta((metadataHolder as ICategory).id, metadataKey, value).response;
break; break;
case 'chapter': case 'chapter':
// eslint-disable-next-line no-case-declarations await requestManager.setChapterMeta((metadataHolder as ChapterType).id, metadataKey, value).response;
const { manga, chapter } = metadataHolder as IMangaChapter;
endpoint = `manga/${manga.id}/chapter/${chapter.index}/meta`;
await requestManager.setChapterMeta(chapter.id, metadataKey, value).response;
break; break;
case 'global': case 'global':
endpoint = 'meta';
await requestManager.setGlobalMetadata(metadataKey, value).response; await requestManager.setGlobalMetadata(metadataKey, value).response;
break; break;
case 'manga': case 'manga':
endpoint = `manga/${(metadataHolder as IManga).id}/meta`; await requestManager.setMangaMeta((metadataHolder as MangaType).id, metadataKey, value).response;
await requestManager.setMangaMeta((metadataHolder as IManga).id, metadataKey, value).response;
break; break;
default: default:
throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`); throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`);
} }
const urlToMutate = `${RequestManager.API_VERSION}${endpoint}`;
mutate(
urlToMutate,
{ ...metadataHolder, ...wrapMetadataWithMetaKey(holderType !== 'global', mutatedMetadata) },
{ revalidate: false },
);
}; };
export const requestUpdateMetadata = async ( export const requestUpdateMetadata = async (
metadataHolder: MetadataHolder, metadataHolder: GqlMetaHolder,
holderType: MetadataHolderType, holderType: MetadataHolderType,
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][], keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
): Promise<void[]> => ): Promise<void[]> =>
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value))); Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
export const requestUpdateServerMetadata = async ( export const requestUpdateServerMetadata = async (
serverMetadata: Metadata, serverMetadata: MetaType[],
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata({ meta: serverMetadata }, 'global', keysToValues); ): Promise<void[]> => requestUpdateMetadata({ meta: serverMetadata }, 'global', keysToValues);
export const requestUpdateMangaMetadata = async ( export const requestUpdateMangaMetadata = async (
manga: IMangaCard | IManga, manga: MangaType,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues); ): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues);
export const requestUpdateChapterMetadata = async ( export const requestUpdateChapterMetadata = async (
mangaChapter: IMangaChapter, chapter: ChapterType,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(mangaChapter.chapter, 'chapter', keysToValues); ): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues);
export const requestUpdateCategoryMetadata = async ( export const requestUpdateCategoryMetadata = async (
category: ICategory, category: CategoryType,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues); ): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues);
export const convertGqlMetadata = (gqlMetadata?: MetaType[]): Metadata | undefined => {
if (!gqlMetadata) {
return undefined;
}
const metadata: Metadata = {};
gqlMetadata.forEach(({ key, value }) => {
metadata[key] = value;
});
return metadata;
};

View File

@@ -6,14 +6,15 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { IManga, Metadata, MetadataHolder, IReaderSettings, MetadataKeyValuePair } from '@/typings'; import { Metadata, IReaderSettings, MetadataKeyValuePair, GqlMetaHolder } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { import {
convertGqlMetadata, convertFromGqlMeta,
getMetadataFrom, getMetadataFrom,
requestUpdateMangaMetadata, requestUpdateMangaMetadata,
requestUpdateServerMetadata, requestUpdateServerMetadata,
} from '@/util/metadata'; } from '@/util/metadata';
import { MangaType, MetaType } from '@/lib/graphql/generated/graphql.ts';
type UndefinedReaderSettings = { type UndefinedReaderSettings = {
[setting in keyof IReaderSettings]: IReaderSettings[setting] | undefined; [setting in keyof IReaderSettings]: IReaderSettings[setting] | undefined;
@@ -42,10 +43,10 @@ export const getReaderSettingsFromMetadata = (
): IReaderSettings => getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration); ): IReaderSettings => getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration);
export const getReaderSettingsFor = ( export const getReaderSettingsFor = (
{ meta }: MetadataHolder, { meta }: GqlMetaHolder = {},
defaultSettings?: IReaderSettings, defaultSettings?: IReaderSettings,
applyMetadataMigration?: boolean, applyMetadataMigration?: boolean,
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration); ): IReaderSettings => getReaderSettingsFromMetadata(convertFromGqlMeta(meta), defaultSettings, applyMetadataMigration);
export const useDefaultReaderSettings = (): { export const useDefaultReaderSettings = (): {
metadata?: Metadata; metadata?: Metadata;
@@ -53,7 +54,7 @@ export const useDefaultReaderSettings = (): {
loading: boolean; loading: boolean;
} => { } => {
const { data, loading } = requestManager.useGetGlobalMeta(); const { data, loading } = requestManager.useGetGlobalMeta();
const metadata = convertGqlMetadata(data?.metas.nodes); const metadata = convertFromGqlMeta(data?.metas.nodes);
const settings = getReaderSettingsWithDefaultValueFallback<IReaderSettings>(metadata); const settings = getReaderSettingsWithDefaultValueFallback<IReaderSettings>(metadata);
return { metadata, settings, loading }; return { metadata, settings, loading };
@@ -67,11 +68,12 @@ export const useDefaultReaderSettings = (): {
* @param defaultSettings * @param defaultSettings
*/ */
export const checkAndHandleMissingStoredReaderSettings = async ( export const checkAndHandleMissingStoredReaderSettings = async (
metadataHolder: IManga | MetadataHolder, metadataHolder: Required<GqlMetaHolder> | MetaType[],
metadataHolderType: 'manga' | 'server', metadataHolderType: 'manga' | 'server',
defaultSettings: IReaderSettings, defaultSettings: IReaderSettings,
): Promise<void | void[]> => { ): Promise<void | void[]> => {
const meta = metadataHolder.meta ?? (metadataHolder as Metadata); const getMeta = () => (Array.isArray(metadataHolder) ? metadataHolder : metadataHolder.meta);
const meta = convertFromGqlMeta(getMeta())!;
const settingsToCheck = getReaderSettingsWithDefaultValueFallback( const settingsToCheck = getReaderSettingsWithDefaultValueFallback(
meta, meta,
{ {
@@ -85,7 +87,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
}, },
false, false,
); );
const newSettings = getReaderSettingsFor({ meta }, defaultSettings); const newSettings = getReaderSettingsFor({ meta: getMeta() }, defaultSettings);
const undefinedSettings = Object.entries(settingsToCheck).filter((setting) => setting[1] === undefined); const undefinedSettings = Object.entries(settingsToCheck).filter((setting) => setting[1] === undefined);
@@ -101,9 +103,9 @@ export const checkAndHandleMissingStoredReaderSettings = async (
} }
if (metadataHolderType === 'manga') { if (metadataHolderType === 'manga') {
await requestUpdateMangaMetadata(metadataHolder as IManga, settingsToUpdate); await requestUpdateMangaMetadata(metadataHolder as MangaType, settingsToUpdate);
return; return;
} }
await requestUpdateServerMetadata(meta, settingsToUpdate); await requestUpdateServerMetadata(metadataHolder as MetaType[], settingsToUpdate);
}; };

View File

@@ -8,7 +8,7 @@
import { Metadata, ISearchSettings } from '@/typings'; import { Metadata, ISearchSettings } from '@/typings';
import requestManager from '@/lib/requests/RequestManager.ts'; import requestManager from '@/lib/requests/RequestManager.ts';
import { convertGqlMetadata, getMetadataFrom } from '@/util/metadata'; import { convertFromGqlMeta, getMetadataFrom } from '@/util/metadata';
export const getDefaultSettings = (): ISearchSettings => ({ export const getDefaultSettings = (): ISearchSettings => ({
ignoreFilters: false, ignoreFilters: false,
@@ -25,7 +25,7 @@ export const useSearchSettings = (): {
loading: boolean; loading: boolean;
} => { } => {
const { data, loading } = requestManager.useGetGlobalMeta(); const { data, loading } = requestManager.useGetGlobalMeta();
const metadata = convertGqlMetadata(data?.metas.nodes); const metadata = convertFromGqlMeta(data?.metas.nodes);
const settings = getSearchSettingsWithDefaultValueFallback(metadata); const settings = getSearchSettingsWithDefaultValueFallback(metadata);
return { metadata, settings, loading }; return { metadata, settings, loading };