Reduce requested manga data in queries

This commit is contained in:
schroda
2024-07-08 18:02:50 +02:00
parent 4b30e9bc48
commit 3d21266f92
47 changed files with 759 additions and 471 deletions

View File

@@ -52,7 +52,7 @@ export const MangaCard = (props: MangaCardProps) => {
mode === 'source', mode === 'source',
); );
const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.source?.id, manga.title); const mangaLinkTo = getMangaLinkTo(mode, manga.id, manga.sourceId, manga.title);
const nextChapterIndexToRead = firstUnreadChapter?.sourceOrder ?? 1; const nextChapterIndexToRead = firstUnreadChapter?.sourceOrder ?? 1;
const isLatestChapterRead = chapters?.totalCount === latestReadChapter?.sourceOrder; const isLatestChapterRead = chapters?.totalCount === latestReadChapter?.sourceOrder;

View File

@@ -17,11 +17,11 @@ import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
import { MangaCard } from '@/components/MangaCard'; import { MangaCard } from '@/components/MangaCard';
import { GridLayout } from '@/components/context/LibraryOptionsContext'; import { GridLayout } from '@/components/context/LibraryOptionsContext';
import { useLocalStorage, useSessionStorage } from '@/util/useStorage.tsx'; import { useLocalStorage, useSessionStorage } from '@/util/useStorage.tsx';
import { TManga, TPartialManga } from '@/typings.ts';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx'; import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab.tsx';
import { AppStorage } from '@/util/AppStorage.ts'; import { AppStorage } from '@/util/AppStorage.ts';
import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx'; import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => ( const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
<Grid {...props} ref={ref} container sx={{ paddingLeft: '5px', paddingRight: '13px' }}> <Grid {...props} ref={ref} container sx={{ paddingLeft: '5px', paddingRight: '13px' }}>
@@ -46,12 +46,14 @@ const GridItemContainerWithDimension = (
); );
}; };
type TManga = MangaCardProps['manga'];
const createMangaCard = ( const createMangaCard = (
manga: TPartialManga, manga: TManga,
gridLayout?: GridLayout, gridLayout?: GridLayout,
inLibraryIndicator?: boolean, inLibraryIndicator?: boolean,
isSelectModeActive: boolean = false, isSelectModeActive: boolean = false,
selectedMangaIds?: TManga['id'][], selectedMangaIds?: MangaType['id'][],
handleSelection?: DefaultGridProps['handleSelection'], handleSelection?: DefaultGridProps['handleSelection'],
mode?: MangaCardProps['mode'], mode?: MangaCardProps['mode'],
) => ( ) => (
@@ -68,13 +70,13 @@ const createMangaCard = (
type DefaultGridProps = Pick<MangaCardProps, 'mode'> & { type DefaultGridProps = Pick<MangaCardProps, 'mode'> & {
isLoading: boolean; isLoading: boolean;
mangas: TPartialManga[]; mangas: TManga[];
inLibraryIndicator?: boolean; inLibraryIndicator?: boolean;
GridItemContainer: (props: GridTypeMap['props'] & Partial<GridItemProps>) => JSX.Element; GridItemContainer: (props: GridTypeMap['props'] & Partial<GridItemProps>) => JSX.Element;
gridLayout?: GridLayout; gridLayout?: GridLayout;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
selectedMangaIds?: Required<TManga['id']>[]; selectedMangaIds?: Required<MangaType['id']>[];
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
}; };
const HorizontalGrid = forwardRef( const HorizontalGrid = forwardRef(

View File

@@ -19,7 +19,6 @@ import DownloadIcon from '@mui/icons-material/Download';
import DoneAllIcon from '@mui/icons-material/DoneAll'; import DoneAllIcon from '@mui/icons-material/DoneAll';
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state'; import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import { TManga } from '@/typings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ChapterCard } from '@/components/chapter/ChapterCard.tsx'; import { ChapterCard } from '@/components/chapter/ChapterCard.tsx';
import { ResumeFab } from '@/components/manga/ResumeFAB.tsx'; import { ResumeFab } from '@/components/manga/ResumeFAB.tsx';
@@ -32,6 +31,7 @@ import {
DownloadType, DownloadType,
GetChaptersMangaQuery, GetChaptersMangaQuery,
GetChaptersMangaQueryVariables, GetChaptersMangaQueryVariables,
MangaScreenFieldsFragment,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx'; import { SelectableCollectionSelectAll } from '@/components/collection/SelectableCollectionSelectAll.tsx';
@@ -72,7 +72,10 @@ export interface IChapterWithMeta {
} }
interface IProps { interface IProps {
manga: TManga; manga: Pick<
MangaScreenFieldsFragment,
'id' | 'firstUnreadChapter' | 'chapters' | 'latestReadChapter' | 'unreadCount' | 'downloadCount'
>;
isRefreshing: boolean; isRefreshing: boolean;
} }

View File

@@ -8,10 +8,10 @@
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TManga } from '@/typings.ts';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { Mangas } from '@/lib/data/Mangas.ts'; import { Mangas } from '@/lib/data/Mangas.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
const DownloadRange = { const DownloadRange = {
NEXT_1: 1, NEXT_1: 1,
@@ -25,7 +25,7 @@ export const ChaptersDownloadActionMenuItems = ({
mangaIds, mangaIds,
closeMenu, closeMenu,
}: { }: {
mangaIds: TManga['id'][]; mangaIds: MangaType['id'][];
closeMenu: () => void; closeMenu: () => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@@ -9,14 +9,12 @@
import React, { useEffect, useLayoutEffect } from 'react'; import React, { useEffect, useLayoutEffect } from 'react';
import { StringParam, useQueryParam } from 'use-query-params'; import { StringParam, useQueryParam } from 'use-query-params';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TManga } from '@/typings';
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
import { IMangaGridProps, MangaGrid } from '@/components/MangaGrid'; import { IMangaGridProps, MangaGrid } from '@/components/MangaGrid';
interface LibraryMangaGridProps interface LibraryMangaGridProps
extends Required<Pick<IMangaGridProps, 'isSelectModeActive' | 'selectedMangaIds' | 'handleSelection'>>, extends Required<Pick<IMangaGridProps, 'isSelectModeActive' | 'selectedMangaIds' | 'handleSelection' | 'mangas'>>,
Pick<IMangaGridProps, 'retry' | 'message' | 'messageExtra'> { Pick<IMangaGridProps, 'retry' | 'message' | 'messageExtra'> {
mangas: TManga[];
showFilteredOutMessage: boolean; showFilteredOutMessage: boolean;
isLoading: boolean; isLoading: boolean;
} }

View File

@@ -8,10 +8,11 @@
import { StringParam, useQueryParam } from 'use-query-params'; import { StringParam, useQueryParam } from 'use-query-params';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { LibraryOptions, LibrarySortMode, NullAndUndefined, TManga } from '@/typings.ts'; import { LibraryOptions, LibrarySortMode, NullAndUndefined } from '@/typings.ts';
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx'; import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { Trackers } from '@/lib/data/Trackers.ts'; import { ChapterType, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
const triStateFilter = ( const triStateFilter = (
triState: NullAndUndefined<boolean>, triState: NullAndUndefined<boolean>,
@@ -35,22 +36,26 @@ const triStateFilterNumber = (triState: NullAndUndefined<boolean>, count?: numbe
() => count === 0, () => count === 0,
); );
const queryFilter = (query: NullAndUndefined<string>, { title }: TManga): boolean => { type TMangaQueryFilter = Pick<MangaType, 'title'>;
const queryFilter = (query: NullAndUndefined<string>, { title }: TMangaQueryFilter): boolean => {
if (!query) return true; if (!query) return true;
return title.toLowerCase().includes(query.toLowerCase()); return title.toLowerCase().includes(query.toLowerCase());
}; };
const queryGenreFilter = (query: NullAndUndefined<string>, { genre }: TManga): boolean => { type TMangaQueryGenreFilter = Pick<MangaType, 'genre'>;
const queryGenreFilter = (query: NullAndUndefined<string>, { genre }: TMangaQueryGenreFilter): boolean => {
if (!query) return true; if (!query) return true;
const queries = query.split(',').map((str) => str.toLowerCase().trim()); const queries = query.split(',').map((str) => str.toLowerCase().trim());
return queries.every((element) => genre.map((el) => el.toLowerCase()).includes(element)); return queries.every((element) => genre.map((el) => el.toLowerCase()).includes(element));
}; };
const trackerFilter = (trackFilters: LibraryOptions['tracker'], manga: TManga): boolean => type TMangaTrackerFilter = { trackRecords: { nodes: Pick<TrackRecordType, 'id' | 'trackerId'>[] } };
const trackerFilter = (trackFilters: LibraryOptions['tracker'], manga: TMangaTrackerFilter): boolean =>
Object.entries(trackFilters) Object.entries(trackFilters)
.map(([trackFilterId, trackFilterState]) => { .map(([trackFilterId, trackFilterState]) => {
const mangaTrackers = Trackers.getTrackers(manga.trackRecords.nodes); const isTrackerBound = manga.trackRecords.nodes.some(
const isTrackerBound = mangaTrackers.some((tracker) => tracker.id === Number(trackFilterId)); (trackRecord) => trackRecord.trackerId === Number(trackFilterId),
);
return triStateFilter( return triStateFilter(
trackFilterState, trackFilterState,
@@ -60,15 +65,19 @@ const trackerFilter = (trackFilters: LibraryOptions['tracker'], manga: TManga):
}) })
.every((matchesFilter) => matchesFilter); .every((matchesFilter) => matchesFilter);
const filterManga = ( type TMangaFilter = TMangaQueryFilter &
mangas: TManga[], TMangaQueryGenreFilter &
TMangaTrackerFilter &
Pick<MangaType, 'downloadCount' | 'unreadCount' | 'bookmarkCount'>;
const filterManga = <Manga extends TMangaFilter>(
mangas: Manga[],
query: NullAndUndefined<string>, query: NullAndUndefined<string>,
unread: NullAndUndefined<boolean>, unread: NullAndUndefined<boolean>,
downloaded: NullAndUndefined<boolean>, downloaded: NullAndUndefined<boolean>,
bookmarked: NullAndUndefined<boolean>, bookmarked: NullAndUndefined<boolean>,
tracker: LibraryOptions['tracker'], tracker: LibraryOptions['tracker'],
ignoreFilters: boolean, ignoreFilters: boolean,
): TManga[] => ): Manga[] =>
mangas.filter((manga) => { mangas.filter((manga) => {
const ignoreFiltersWhileSearching = ignoreFilters && query?.length; const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
const matchesSearch = queryFilter(query, manga) || queryGenreFilter(query, manga); const matchesSearch = queryFilter(query, manga) || queryGenreFilter(query, manga);
@@ -86,11 +95,16 @@ const sortByNumber = (a: number | string = 0, b: number | string = 0) => Number(
const sortByString = (a: string, b: string): number => a.localeCompare(b); const sortByString = (a: string, b: string): number => a.localeCompare(b);
const sortManga = ( type TMangaSort = Pick<MangaType, 'title' | 'inLibraryAt' | 'unreadCount'> & {
manga: TManga[], lastReadChapter?: Pick<ChapterType, 'lastReadAt'> | null;
latestUploadedChapter?: Pick<ChapterType, 'uploadDate'> | null;
latestFetchedChapter?: Pick<ChapterType, 'fetchedAt'> | null;
};
const sortManga = <Manga extends TMangaSort>(
manga: Manga[],
sort: NullAndUndefined<LibrarySortMode>, sort: NullAndUndefined<LibrarySortMode>,
desc: NullAndUndefined<boolean>, desc: NullAndUndefined<boolean>,
): TManga[] => { ): Manga[] => {
const result = [...manga]; const result = [...manga];
switch (sort) { switch (sort) {
@@ -125,7 +139,12 @@ const sortManga = (
return result; return result;
}; };
export const useGetVisibleLibraryMangas = (mangas: TManga[]) => { export const useGetVisibleLibraryMangas = <Manga extends MangaIdInfo & TMangaFilter & TMangaSort>(
mangas: Manga[],
): {
visibleMangas: Manga[];
showFilteredOutMessage: boolean;
} => {
const [query] = useQueryParam('query', StringParam); const [query] = useQueryParam('query', StringParam);
const { options } = useLibraryOptionsContext(); const { options } = useLibraryOptionsContext();
const { unread, downloaded, bookmarked, tracker } = options; const { unread, downloaded, bookmarked, tracker } = options;

View File

@@ -19,8 +19,14 @@ import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import SyncIcon from '@mui/icons-material/Sync'; import SyncIcon from '@mui/icons-material/Sync';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import { TManga } from '@/typings.ts'; import {
import { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts'; actionToTranslationKey,
MangaAction,
MangaDownloadInfo,
MangaIdInfo,
Mangas,
MangaUnreadInfo,
} from '@/lib/data/Mangas.ts';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { MenuItem } from '@/components/menu/MenuItem.tsx'; import { MenuItem } from '@/components/menu/MenuItem.tsx';
import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts'; import { createGetMenuItemTitle, createIsMenuItemDisabled, createShouldShowMenuItem } from '@/components/menu/util.ts';
@@ -29,18 +35,19 @@ import { TrackManga } from '@/components/tracker/TrackManga.tsx';
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx'; import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
import { ChaptersDownloadActionMenuItems } from '@/components/chapter/ChaptersDownloadActionMenuItems.tsx'; import { ChaptersDownloadActionMenuItems } from '@/components/chapter/ChaptersDownloadActionMenuItems.tsx';
import { NestedMenuItem } from '@/components/menu/NestedMenuItem.tsx'; import { NestedMenuItem } from '@/components/menu/NestedMenuItem.tsx';
import { MangaChapterStatFieldsFragment, MangaType } from '@/lib/graphql/generated/graphql.ts';
const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const; const ACTION_DISABLES_SELECTION_MODE: MangaAction[] = ['remove_from_library'] as const;
type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void }; type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void };
export type SingleModeProps = { export type SingleModeProps = {
manga: Pick<TManga, 'id' | 'title' | 'source' | 'trackRecords'> & MangaDownloadInfo & MangaUnreadInfo; manga: Pick<MangaType, 'id' | 'title' | 'sourceId'> & MangaDownloadInfo & MangaUnreadInfo;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
}; };
type SelectModeProps = { type SelectModeProps = {
selectedMangas: TManga[]; selectedMangas: MangaChapterStatFieldsFragment[];
}; };
type Props = type Props =
@@ -82,7 +89,7 @@ export const MangaActionMenuItems = ({
onClose(true); onClose(true);
}; };
const performAction = (action: MangaAction, mangas: TManga[]) => { const performAction = (action: MangaAction, mangas: MangaIdInfo[]) => {
Mangas.performAction(action, manga ? [manga.id] : Mangas.getIds(mangas), { Mangas.performAction(action, manga ? [manga.id] : Mangas.getIds(mangas), {
wasManuallyMarkedAsRead: true, wasManuallyMarkedAsRead: true,
}).catch(defaultPromiseErrorHandler(`MangaActionMenuItems:performAction(${action})`)); }).catch(defaultPromiseErrorHandler(`MangaActionMenuItems:performAction(${action})`));
@@ -150,7 +157,7 @@ export const MangaActionMenuItems = ({
)} )}
{isSingleMode && ( {isSingleMode && (
<Link <Link
to={`/migrate/source/${manga?.source?.id}/manga/${manga?.id}/search?query=${manga?.title}`} to={`/migrate/source/${manga?.sourceId}/manga/${manga?.id}/search?query=${manga?.title}`}
state={{ mangaTitle: manga?.title }} state={{ mangaTitle: manga?.title }}
style={{ textDecoration: 'none', color: 'inherit' }} style={{ textDecoration: 'none', color: 'inherit' }}
> >

View File

@@ -8,24 +8,37 @@
import { LongPressPointerHandlers, LongPressResult } from 'use-long-press/lib/use-long-press.types'; import { LongPressPointerHandlers, LongPressResult } from 'use-long-press/lib/use-long-press.types';
import { PopupState } from 'material-ui-popup-state/hooks'; import { PopupState } from 'material-ui-popup-state/hooks';
import { TManga, TPartialManga } from '@/typings.ts';
import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx'; import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
import { MangaChapterCountInfo, MangaThumbnailInfo } from '@/lib/data/Mangas.ts';
import { ChapterType, MangaType } from '@/lib/graphql/generated/graphql.ts';
import { SingleModeProps } from '@/components/manga/MangaActionMenuItems.tsx';
export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate'; export type MangaCardMode = 'default' | 'source' | 'migrate.search' | 'migrate.select' | 'duplicate';
type MangaCardBaseProps = Pick<MangaType, 'id' | 'title' | 'sourceId'> &
SingleModeProps['manga'] &
Partial<Pick<MangaType, 'inLibrary' | 'downloadCount' | 'unreadCount'>> &
MangaChapterCountInfo & {
latestReadChapter?: Pick<ChapterType, 'id' | 'sourceOrder'> | null;
firstUnreadChapter?: Pick<ChapterType, 'id' | 'sourceOrder'> | null;
};
type MangaCardSpecificProps = MangaCardBaseProps & MangaThumbnailInfo;
export interface MangaCardProps { export interface MangaCardProps {
manga: TPartialManga; manga: MangaCardBaseProps;
gridLayout?: GridLayout; gridLayout?: GridLayout;
inLibraryIndicator?: boolean; inLibraryIndicator?: boolean;
selected?: boolean | null; selected?: boolean | null;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
mode?: MangaCardMode; mode?: MangaCardMode;
} }
export type SpecificMangaCardProps = MangaCardProps & export type SpecificMangaCardProps = Omit<MangaCardProps, 'manga'> &
Pick<ReturnType<typeof useManageMangaLibraryState>, 'isInLibrary'> & { Pick<ReturnType<typeof useManageMangaLibraryState>, 'isInLibrary'> & {
manga: MangaCardSpecificProps;
longPressBind: LongPressResult<LongPressPointerHandlers>; longPressBind: LongPressResult<LongPressPointerHandlers>;
popupState: PopupState; popupState: PopupState;
handleClick: (event: React.MouseEvent | React.TouchEvent) => void; handleClick: (event: React.MouseEvent | React.TouchEvent) => void;

View File

@@ -10,22 +10,21 @@ 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, { ComponentProps, useEffect, useMemo } from 'react'; import { ComponentProps, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next'; import { t as translate } from 'i18next';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { useLongPress } from 'use-long-press'; import { useLongPress } from 'use-long-press';
import { TManga } from '@/typings';
import { makeToast } from '@/components/util/Toast'; import { makeToast } from '@/components/util/Toast';
import { Mangas } from '@/lib/data/Mangas.ts'; import { Mangas, MangaThumbnailInfo, MangaTrackRecordInfo } from '@/lib/data/Mangas.ts';
import { SpinnerImage } from '@/components/util/SpinnerImage.tsx'; import { SpinnerImage } from '@/components/util/SpinnerImage.tsx';
import { CustomIconButton } from '@/components/atoms/CustomIconButton'; import { CustomIconButton } from '@/components/atoms/CustomIconButton';
import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx'; import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx';
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx'; import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
import { Metadata as BaseMetadata } from '@/components/atoms/Metadata.tsx'; import { Metadata as BaseMetadata } from '@/components/atoms/Metadata.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { SourceType } from '@/lib/graphql/generated/graphql.ts'; import { MangaType, SourceType } from '@/lib/graphql/generated/graphql.ts';
const DetailsWrapper = styled('div')(({ theme }) => ({ const DetailsWrapper = styled('div')(({ theme }) => ({
width: '100%', width: '100%',
@@ -156,10 +155,6 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
return button; return button;
}; };
interface IProps {
manga: TManga;
}
function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null): string { function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null): string {
if (!source) { if (!source) {
return translate('global.label.unknown'); return translate('global.label.unknown');
@@ -172,7 +167,18 @@ function getValueOrUnknown(val?: string | null) {
return val ?? translate('global.label.unknown'); return val ?? translate('global.label.unknown');
} }
export const MangaDetails: React.FC<IProps> = ({ manga }) => { export const MangaDetails = ({
manga,
}: {
manga: Pick<
MangaType,
'id' | 'title' | 'author' | 'artist' | 'status' | 'inLibrary' | 'realUrl' | 'description' | 'genre'
> &
MangaThumbnailInfo &
MangaTrackRecordInfo & {
source?: Pick<SourceType, 'id' | 'displayName'> | null;
};
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {

View File

@@ -16,8 +16,8 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
import { PopupState } from 'material-ui-popup-state/hooks'; import { PopupState } from 'material-ui-popup-state/hooks';
import { bindTrigger } from 'material-ui-popup-state'; import { bindTrigger } from 'material-ui-popup-state';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts'; import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { TManga } from '@/typings.ts';
import { MediaQuery } from '@/lib/ui/MediaQuery.tsx'; import { MediaQuery } from '@/lib/ui/MediaQuery.tsx';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
export const MangaOptionButton = forwardRef( export const MangaOptionButton = forwardRef(
( (
@@ -30,7 +30,7 @@ export const MangaOptionButton = forwardRef(
}: { }: {
id: number; id: number;
selected?: boolean | null; selected?: boolean | null;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection']; handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
asCheckbox?: boolean; asCheckbox?: boolean;
popupState: PopupState; popupState: PopupState;
}, },

View File

@@ -21,11 +21,11 @@ import { Link } from 'react-router-dom';
import SyncAltIcon from '@mui/icons-material/SyncAlt'; import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery'; import useMediaQuery from '@mui/material/useMediaQuery';
import { TManga } from '@/typings.ts';
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx'; import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
interface IProps { interface IProps {
manga: TManga; manga: Pick<MangaType, 'id' | 'inLibrary' | 'sourceId' | 'title'>;
onRefresh: () => any; onRefresh: () => any;
refreshing: boolean; refreshing: boolean;
} }
@@ -64,7 +64,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<> <>
<Tooltip title={t('global.button.migrate')}> <Tooltip title={t('global.button.migrate')}>
<Link <Link
to={`/migrate/source/${manga.source?.id}/manga/${manga.id}/search?query=${manga.title}`} to={`/migrate/source/${manga.sourceId}/manga/${manga.id}/search?query=${manga.title}`}
state={{ mangaTitle: manga.title }} state={{ mangaTitle: manga.title }}
style={{ textDecoration: 'none', color: 'inherit' }} style={{ textDecoration: 'none', color: 'inherit' }}
> >
@@ -122,7 +122,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<MenuItem <MenuItem
key="migrate" key="migrate"
component={Link} component={Link}
to={`/migrate/source/${manga.source?.id}/manga/${manga.id}/search?query=${manga.title}`} to={`/migrate/source/${manga.sourceId}/manga/${manga.id}/search?query=${manga.title}`}
state={{ mangaTitle: manga.title }} state={{ mangaTitle: manga.title }}
style={{ textDecoration: 'none', color: 'inherit' }} style={{ textDecoration: 'none', color: 'inherit' }}
> >

View File

@@ -16,20 +16,21 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/components/util/Toast.tsx'; import { makeToast } from '@/components/util/Toast.tsx';
import { TrackManga } from '@/components/tracker/TrackManga.tsx'; import { TrackManga } from '@/components/tracker/TrackManga.tsx';
import { Trackers } from '@/lib/data/Trackers.ts'; import { Trackers } from '@/lib/data/Trackers.ts';
import { TManga } from '@/typings.ts';
import { CustomIconButton } from '@/components/atoms/CustomIconButton.tsx'; import { CustomIconButton } from '@/components/atoms/CustomIconButton.tsx';
import { GetTrackersSettingsQuery } from '@/lib/graphql/generated/graphql.ts'; import { GetTrackersSettingsQuery, MangaType } from '@/lib/graphql/generated/graphql.ts';
import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts'; import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts';
import { MangaTrackRecordInfo } from '@/lib/data/Mangas.ts';
export const TrackMangaButton = ({ manga }: { manga: TManga }) => { export const TrackMangaButton = ({ manga }: { manga: MangaTrackRecordInfo & Pick<MangaType, 'title'> }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS); const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
const trackers = trackerList.data?.trackers.nodes ?? [];
const mangaTrackers = manga.trackRecords.nodes; const mangaTrackers = manga.trackRecords.nodes;
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []); const loggedInTrackers = Trackers.getLoggedIn(trackers);
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers)); const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers, trackers));
const handleClick = (openPopup: () => void) => { const handleClick = (openPopup: () => void) => {
if (trackerList.error) { if (trackerList.error) {

View File

@@ -16,19 +16,18 @@ import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings
import { Categories } from '@/lib/data/Categories.ts'; import { Categories } from '@/lib/data/Categories.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { Mangas } from '@/lib/data/Mangas.ts'; import { Mangas } from '@/lib/data/Mangas.ts';
import { TManga } from '@/typings.ts';
import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx'; import { awaitConfirmation } from '@/lib/ui/AwaitableDialog.tsx';
import { GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables } from '@/lib/graphql/generated/graphql.ts'; import { GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables, MangaType } from '@/lib/graphql/generated/graphql.ts';
import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts';
export const useManageMangaLibraryState = ( export const useManageMangaLibraryState = (
manga: Pick<TManga, 'id' | 'title' | 'inLibrary'>, manga: Pick<MangaType, 'id' | 'title'> & Partial<Pick<MangaType, 'inLibrary'>>,
confirmRemoval: boolean = false, confirmRemoval: boolean = false,
) => { ) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const [isInLibrary, setIsInLibrary] = useState(manga.inLibrary); const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary);
const addToLibrary = useCallback( const addToLibrary = useCallback(
(didSubmit: boolean, addToCategories: number[] = [], removeFromCategories: number[] = []) => { (didSubmit: boolean, addToCategories: number[] = [], removeFromCategories: number[] = []) => {
@@ -162,6 +161,6 @@ export const useManageMangaLibraryState = (
* *
* To work around this issue, the currently known in library state gets returned here * To work around this issue, the currently known in library state gets returned here
*/ */
isInLibrary: Mangas.getFromCache<TManga>(manga.id)?.inLibrary ?? isInLibrary, isInLibrary: Mangas.getFromCache(manga.id)?.inLibrary ?? isInLibrary,
}; };
}; };

View File

@@ -27,12 +27,13 @@ import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import Collapse from '@mui/material/Collapse'; import Collapse from '@mui/material/Collapse';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings, TManga } from '@/typings'; import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings } from '@/typings';
import { ReaderSettingsOptions } from '@/components/reader/ReaderSettingsOptions'; import { ReaderSettingsOptions } from '@/components/reader/ReaderSettingsOptions';
import { useBackButton } from '@/util/useBackButton.ts'; import { useBackButton } from '@/util/useBackButton.ts';
import { Select } from '@/components/atoms/Select.tsx'; import { Select } from '@/components/atoms/Select.tsx';
import { getOptionForDirection } from '@/theme.ts'; import { getOptionForDirection } from '@/theme.ts';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts'; import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
import { MangaChapterCountInfo, MangaIdInfo } from '@/lib/data/Mangas.ts';
const Root = styled('div')({ const Root = styled('div')({
zIndex: 10, zIndex: 10,
@@ -123,7 +124,7 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
interface IProps { interface IProps {
settings: IReaderSettings; settings: IReaderSettings;
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void; setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
manga: TManga; manga: MangaIdInfo & MangaChapterCountInfo;
chapter: Pick<ChapterType, 'name' | 'sourceOrder' | 'pageCount'>; chapter: Pick<ChapterType, 'name' | 'sourceOrder' | 'pageCount'>;
chapters: Pick<ChapterType, 'id' | 'sourceOrder' | 'name' | 'chapterNumber' | 'scanlator'>[]; chapters: Pick<ChapterType, 'id' | 'sourceOrder' | 'name' | 'chapterNumber' | 'scanlator'>[];
curPage: number; curPage: number;

View File

@@ -25,8 +25,14 @@ import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
import { makeToast } from '@/components/util/Toast.tsx'; import { makeToast } from '@/components/util/Toast.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { updateMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { updateMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables } from '@/lib/graphql/generated/graphql.ts'; import {
GetCategoriesBaseQuery,
GetCategoriesBaseQueryVariables,
GetMangaCategoriesQuery,
GetMangaCategoriesQueryVariables,
} from '@/lib/graphql/generated/graphql.ts';
import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_CATEGORIES_BASE } from '@/lib/graphql/queries/CategoryQuery.ts';
import { GET_MANGA_CATEGORIES } from '@/lib/graphql/queries/MangaQuery.ts';
type BaseProps = { type BaseProps = {
open: boolean; open: boolean;
@@ -47,7 +53,11 @@ export type CategorySelectProps =
| (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps); | (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps);
const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => { const useGetMangaCategoryIds = (mangaId: number | undefined): number[] => {
const { data: mangaResult } = requestManager.useGetManga(mangaId ?? -1, { skip: mangaId === undefined }); const { data: mangaResult } = requestManager.useGetManga<GetMangaCategoriesQuery, GetMangaCategoriesQueryVariables>(
GET_MANGA_CATEGORIES,
mangaId ?? -1,
{ skip: mangaId === undefined },
);
return useMemo(() => { return useMemo(() => {
if (mangaId === undefined || !mangaResult) { if (mangaId === undefined || !mangaResult) {
@@ -90,6 +100,7 @@ export function CategorySelect(props: CategorySelectProps) {
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false); const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
const mangaCategoryIds = useGetMangaCategoryIds(mangaId); const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
const { data } = requestManager.useGetCategories<GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables>( const { data } = requestManager.useGetCategories<GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables>(
GET_CATEGORIES_BASE, GET_CATEGORIES_BASE,
); );

View File

@@ -0,0 +1,17 @@
/*
* 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 { MangaGrid, IMangaGridProps } from '@/components/MangaGrid';
type TMangaBaseGrid = Omit<IMangaGridProps['mangas'][number], 'downloadCount' | 'unreadCount' | 'chapters'>;
export function BaseMangaGrid(props: Omit<IMangaGridProps, 'mangas'> & { mangas: TMangaBaseGrid[] }) {
const { mangas } = props;
return <MangaGrid {...props} mangas={mangas as IMangaGridProps['mangas']} />;
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useTranslation } from 'react-i18next';
import { MangaGrid, IMangaGridProps } from '@/components/MangaGrid';
import { TPartialManga } from '@/typings.ts';
function filterManga(mangas: TPartialManga[]): TPartialManga[] {
return mangas;
}
export function SourceMangaGrid(props: IMangaGridProps) {
const { t } = useTranslation();
const { mangas, isLoading, hasNextPage, loadMore, message, messageExtra, gridLayout } = props;
const filteredManga = filterManga(mangas);
const showFilteredOutMessage = filteredManga.length === 0 && mangas.length > 0;
return (
<MangaGrid
mangas={filteredManga}
isLoading={isLoading}
hasNextPage={hasNextPage}
loadMore={loadMore}
message={showFilteredOutMessage ? t('manga.error.label.no_matches') : message}
messageExtra={messageExtra}
gridLayout={gridLayout}
inLibraryIndicator
mode="source"
/>
);
}

View File

@@ -16,11 +16,12 @@ import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCe
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
import { Trackers } from '@/lib/data/Trackers.ts'; import { Trackers } from '@/lib/data/Trackers.ts';
import { TrackerCard, TrackerMode } from '@/components/tracker/TrackerCard.tsx'; import { TrackerCard, TrackerMode } from '@/components/tracker/TrackerCard.tsx';
import { TManga } from '@/typings.ts';
import { makeToast } from '@/components/util/Toast.tsx'; import { makeToast } from '@/components/util/Toast.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { GetTrackersBindQuery } from '@/lib/graphql/generated/graphql.ts'; import { GetMangaTrackRecordsQuery, GetTrackersBindQuery, MangaType } from '@/lib/graphql/generated/graphql.ts';
import { GET_TRACKERS_BIND } from '@/lib/graphql/queries/TrackerQuery.ts'; import { GET_TRACKERS_BIND } from '@/lib/graphql/queries/TrackerQuery.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
import { GET_MANGA_TRACK_RECORDS } from '@/lib/graphql/queries/MangaQuery.ts';
const getTrackerMode = (id: number, trackersInUse: number[], searchModeForTracker?: number): TrackerMode => { const getTrackerMode = (id: number, trackersInUse: number[], searchModeForTracker?: number): TrackerMode => {
if (id === searchModeForTracker) { if (id === searchModeForTracker) {
@@ -34,7 +35,7 @@ const getTrackerMode = (id: number, trackersInUse: number[], searchModeForTracke
return TrackerMode.UNTRACKED; return TrackerMode.UNTRACKED;
}; };
export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords'> }) => { export const TrackManga = ({ manga }: { manga: MangaIdInfo & Pick<MangaType, 'title'> }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -43,26 +44,32 @@ export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords
const trackerList = requestManager.useGetTrackerList<GetTrackersBindQuery>(GET_TRACKERS_BIND, { const trackerList = requestManager.useGetTrackerList<GetTrackersBindQuery>(GET_TRACKERS_BIND, {
notifyOnNetworkStatusChange: true, notifyOnNetworkStatusChange: true,
}); });
const mangaTrackers = manga.trackRecords.nodes; const trackers = trackerList.data?.trackers.nodes ?? [];
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []); const mangaTrackRecordsList = requestManager.useGetManga<GetMangaTrackRecordsQuery>(
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers)); GET_MANGA_TRACK_RECORDS,
manga.id,
);
const mangaTrackRecords = mangaTrackRecordsList.data?.manga.trackRecords.nodes ?? [];
const loggedInTrackers = Trackers.getLoggedIn(trackers);
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackRecords, trackers));
const trackersInUseIds = Trackers.getIds(trackersInUse); const trackersInUseIds = Trackers.getIds(trackersInUse);
const isSearchActive = searchModeForTracker !== undefined; const isSearchActive = searchModeForTracker !== undefined;
const OptionalDialogContent = useMemo(() => (isSearchActive ? Box : DialogContent), [isSearchActive]); const OptionalDialogContent = useMemo(() => (isSearchActive ? Box : DialogContent), [isSearchActive]);
useEffect(() => { useEffect(() => {
Promise.all(manga.trackRecords.nodes.map((trackRecord) => requestManager.fetchTrackBind(trackRecord.id))).catch( Promise.all(mangaTrackRecords.map((trackRecord) => requestManager.fetchTrackBind(trackRecord.id))).catch(() =>
() => makeToast(t('tracking.error.label.could_not_fetch_track_info'), 'error'), makeToast(t('tracking.error.label.could_not_fetch_track_info'), 'error'),
); );
}, [manga.id]); }, [mangaTrackRecords]);
const trackerComponents = useMemo( const trackerComponents = useMemo(
() => () =>
loggedInTrackers.map((tracker) => { loggedInTrackers.map((tracker) => {
const mode = getTrackerMode(tracker.id, trackersInUseIds, searchModeForTracker); const mode = getTrackerMode(tracker.id, trackersInUseIds, searchModeForTracker);
const trackRecord = Trackers.getTrackRecordFor(tracker, manga.trackRecords.nodes); const trackRecord = Trackers.getTrackRecordFor(tracker, mangaTrackRecords);
const isSearchForTracker = mode === TrackerMode.SEARCH; const isSearchForTracker = mode === TrackerMode.SEARCH;
if (isSearchActive && !isSearchForTracker) { if (isSearchActive && !isSearchForTracker) {
@@ -73,27 +80,39 @@ export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords
<TrackerCard <TrackerCard
key={tracker.id} key={tracker.id}
tracker={tracker} tracker={tracker}
mangaId={manga.id} manga={manga}
trackRecord={trackRecord} trackRecord={trackRecord}
mode={mode} mode={mode}
setSearchMode={(id) => setSearchModeForTracker(id)} setSearchMode={(id) => setSearchModeForTracker(id)}
/> />
); );
}), }),
[trackersInUseIds, searchModeForTracker, manga.id], [trackersInUseIds, searchModeForTracker, mangaTrackRecords],
); );
if (trackerList.error) { const error = trackerList.error ?? mangaTrackRecordsList.error;
if (error) {
return ( return (
<EmptyViewAbsoluteCentered <EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')} message={t('global.error.label.failed_to_load_data')}
messageExtra={trackerList.error.message} messageExtra={error.message}
retry={() => trackerList.refetch().catch(defaultPromiseErrorHandler('TrackManga::refetch'))} retry={() => {
if (trackerList.error) {
trackerList.refetch().catch(defaultPromiseErrorHandler('TrackManga::refetch: trackerList'));
}
if (mangaTrackRecordsList.error) {
mangaTrackRecordsList
.refetch()
.catch(defaultPromiseErrorHandler('TrackManga::refetch: mangaTrackRecordsList'));
}
}}
/> />
); );
} }
if (trackerList.loading) { const loading = trackerList.loading || mangaTrackRecordsList.loading;
if (loading) {
return <LoadingPlaceholder />; return <LoadingPlaceholder />;
} }

View File

@@ -10,6 +10,8 @@ import { TrackerUntrackedCard } from '@/components/tracker/TrackerUntrackedCard.
import { TrackerSearch } from '@/components/tracker/TrackerSearch.tsx'; import { TrackerSearch } from '@/components/tracker/TrackerSearch.tsx';
import { TrackerActiveCard } from '@/components/tracker/TrackerActiveCard.tsx'; import { TrackerActiveCard } from '@/components/tracker/TrackerActiveCard.tsx';
import { TTrackerBind, TTrackRecordBind } from '@/lib/data/Trackers.ts'; import { TTrackerBind, TTrackRecordBind } from '@/lib/data/Trackers.ts';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
export enum TrackerMode { export enum TrackerMode {
UNTRACKED, UNTRACKED,
@@ -19,13 +21,13 @@ export enum TrackerMode {
export const TrackerCard = ({ export const TrackerCard = ({
tracker, tracker,
mangaId, manga,
trackRecord, trackRecord,
mode, mode,
setSearchMode, setSearchMode,
}: { }: {
tracker: TTrackerBind; tracker: TTrackerBind;
mangaId: number; manga: MangaIdInfo & Pick<MangaType, 'title'>;
trackRecord?: TTrackRecordBind; trackRecord?: TTrackRecordBind;
mode: TrackerMode; mode: TrackerMode;
setSearchMode: (id?: number) => void; setSearchMode: (id?: number) => void;
@@ -37,7 +39,7 @@ export const TrackerCard = ({
if (mode === TrackerMode.SEARCH) { if (mode === TrackerMode.SEARCH) {
return ( return (
<TrackerSearch <TrackerSearch
mangaId={mangaId} manga={manga}
tracker={tracker} tracker={tracker}
trackedId={trackRecord?.remoteId} trackedId={trackRecord?.remoteId}
closeSearchMode={() => setSearchMode(undefined)} closeSearchMode={() => setSearchMode(undefined)}
@@ -46,7 +48,7 @@ export const TrackerCard = ({
} }
if (mode === TrackerMode.INFO && !trackRecord) { if (mode === TrackerMode.INFO && !trackRecord) {
throw new Error(`TrackerCard: unable to find track record for tracker "${tracker.id}" of manga "${mangaId}"}`); throw new Error(`TrackerCard: unable to find track record for tracker "${tracker.id}" of manga "${manga.id}"}`);
} }
return ( return (

View File

@@ -31,24 +31,23 @@ import { TrackerMangaCard } from '@/components/tracker/TrackerMangaCard.tsx';
import { DIALOG_PADDING } from '@/components/tracker/constants.ts'; import { DIALOG_PADDING } from '@/components/tracker/constants.ts';
import { getOptionForDirection } from '@/theme.ts'; import { getOptionForDirection } from '@/theme.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
export const TrackerSearch = ({ export const TrackerSearch = ({
mangaId, manga,
tracker, tracker,
closeSearchMode, closeSearchMode,
trackedId, trackedId,
}: { }: {
mangaId: number; manga: MangaIdInfo & Pick<MangaType, 'title'>;
tracker: Pick<TTrackerBase, 'id'>; tracker: Pick<TTrackerBase, 'id'>;
closeSearchMode: () => void; closeSearchMode: () => void;
trackedId?: string; trackedId?: string;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
// can't be undefined, since this can only be opened from the manga screen const [searchString, setSearchString] = useState<string>(manga.title);
const manga = requestManager.useGetManga(mangaId);
const [searchString, setSearchString] = useState<string>(manga.data!.manga.title);
const [tmpSearchString, setTmpSearchString] = useState(searchString); const [tmpSearchString, setTmpSearchString] = useState(searchString);
const [selectedTrackerRemoteId, setSelectedTrackerRemoteId] = useState<string | undefined>(trackedId); const [selectedTrackerRemoteId, setSelectedTrackerRemoteId] = useState<string | undefined>(trackedId);
@@ -62,7 +61,7 @@ export const TrackerSearch = ({
setSelectedTrackerRemoteId(trackedId); setSelectedTrackerRemoteId(trackedId);
return () => return () =>
trackerSearch.abortRequest(new Error(`MangaTrackerSearchCard(${tracker.id}, ${mangaId}): search changed`)); trackerSearch.abortRequest(new Error(`MangaTrackerSearchCard(${tracker.id}, ${manga.id}): search changed`));
}, [searchString]); }, [searchString]);
const [bindTracker, bindTrackerMutation] = requestManager.useBindTracker(); const [bindTracker, bindTrackerMutation] = requestManager.useBindTracker();
@@ -82,7 +81,7 @@ export const TrackerSearch = ({
return; return;
} }
bindTracker({ variables: { mangaId, remoteId: selectedTrackerRemoteId, trackerId: tracker.id } }) bindTracker({ variables: { mangaId: manga.id, remoteId: selectedTrackerRemoteId, trackerId: tracker.id } })
.then(() => { .then(() => {
makeToast(t('manga.action.track.add.label.success'), 'success'); makeToast(t('manga.action.track.add.label.success'), 'success');
closeSearchMode(); closeSearchMode();

View File

@@ -9,12 +9,13 @@
import { t as translate } from 'i18next'; import { t as translate } from 'i18next';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { DocumentNode } from '@apollo/client'; import { DocumentNode } from '@apollo/client';
import { ChapterOffset, TManga, TranslationKey } from '@/typings.ts'; import { ChapterOffset, TranslationKey } from '@/typings.ts';
import { makeToast } from '@/components/util/Toast.tsx'; import { makeToast } from '@/components/util/Toast.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { ChapterListFieldsFragment, ChapterType } from '@/lib/graphql/generated/graphql.ts'; import { ChapterListFieldsFragment, ChapterType } from '@/lib/graphql/generated/graphql.ts';
import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts'; import { CHAPTER_LIST_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread'; export type ChapterAction = 'download' | 'delete' | 'bookmark' | 'unbookmark' | 'mark_as_read' | 'mark_as_unread';
@@ -209,7 +210,7 @@ export class Chapters {
static async markAsRead( static async markAsRead(
chapters: (ChapterDownloadInfo & ChapterBookmarkInfo)[], chapters: (ChapterDownloadInfo & ChapterBookmarkInfo)[],
wasManuallyMarkedAsRead: boolean = false, wasManuallyMarkedAsRead: boolean = false,
trackProgressMangaId?: TManga['id'], trackProgressMangaId?: MangaIdInfo['id'],
): Promise<void> { ): Promise<void> {
const { deleteChaptersManuallyMarkedRead, deleteChaptersWithBookmark, updateProgressManualMarkRead } = const { deleteChaptersManuallyMarkedRead, deleteChaptersWithBookmark, updateProgressManualMarkRead } =
await getMetadataServerSettings(); await getMetadataServerSettings();
@@ -279,7 +280,7 @@ export class Chapters {
}: Action extends 'mark_as_read' }: Action extends 'mark_as_read'
? { ? {
wasManuallyMarkedAsRead: boolean; wasManuallyMarkedAsRead: boolean;
trackProgressMangaId?: TManga['id']; trackProgressMangaId?: MangaIdInfo['id'];
chapters: (ChapterDownloadInfo & ChapterBookmarkInfo & ChapterReadInfo)[]; chapters: (ChapterDownloadInfo & ChapterBookmarkInfo & ChapterReadInfo)[];
} }
: { : {

View File

@@ -8,19 +8,26 @@
import { t as translate } from 'i18next'; import { t as translate } from 'i18next';
import { DocumentNode } from '@apollo/client/core'; import { DocumentNode } from '@apollo/client/core';
import { MetadataMigrationSettings, TManga, TranslationKey } from '@/typings.ts'; import { MetadataMigrationSettings, TranslationKey } from '@/typings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { import {
ChapterConditionInput, ChapterConditionInput,
GetMangasBaseQuery,
GetMangasBaseQueryVariables,
GetMangasChapterIdsWithStateQuery, GetMangasChapterIdsWithStateQuery,
GetMangaToMigrateQuery, GetMangaToMigrateQuery,
GetMangaToMigrateToFetchMutation, GetMangaToMigrateToFetchMutation,
MangaBaseFieldsFragment,
MangaReaderFieldsFragment,
MangaType,
TrackRecordType,
UpdateMangaCategoriesPatchInput, UpdateMangaCategoriesPatchInput,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { Chapters } from '@/lib/data/Chapters.ts'; import { Chapters } from '@/lib/data/Chapters.ts';
import { makeToast } from '@/components/util/Toast.tsx'; import { makeToast } from '@/components/util/Toast.tsx';
import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { FULL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts'; import { GET_MANGAS_BASE } from '@/lib/graphql/queries/MangaQuery.ts';
import { MANGA_BASE_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
export type MangaAction = export type MangaAction =
| 'download' | 'download'
@@ -108,10 +115,16 @@ export const actionToTranslationKey: {
}, },
}; };
export type MangaChapterCountInfo = { chapters: Pick<TManga['chapters'], 'totalCount'> }; export type TMangaReader = MangaReaderFieldsFragment;
export type MangaDownloadInfo = Pick<TManga, 'downloadCount'> & MangaChapterCountInfo;
export type MangaUnreadInfo = Pick<TManga, 'unreadCount'> & MangaChapterCountInfo; export type MangaIdInfo = Pick<MangaType, 'id'>;
export type MangaThumbnailInfo = Pick<TManga, 'thumbnailUrl' | 'thumbnailUrlLastFetched'>; export type MangaChapterCountInfo = { chapters: Pick<MangaType['chapters'], 'totalCount'> };
export type MangaDownloadInfo = Pick<MangaType, 'downloadCount'> & MangaChapterCountInfo;
export type MangaUnreadInfo = Pick<MangaType, 'unreadCount'> & MangaChapterCountInfo;
export type MangaThumbnailInfo = Pick<MangaType, 'thumbnailUrl' | 'thumbnailUrlLastFetched'>;
export type MangaTrackRecordInfo = MangaIdInfo & {
trackRecords: { nodes: Pick<TrackRecordType, 'id' | 'trackerId'>[] };
};
export type MigrateMode = 'copy' | 'migrate'; export type MigrateMode = 'copy' | 'migrate';
@@ -162,14 +175,14 @@ type PerformActionOptions<Action extends MangaAction> = Action extends 'mark_as_
: DefaultActionOption; : DefaultActionOption;
export class Mangas { export class Mangas {
static getIds(mangas: { id: number }[]): number[] { static getIds(mangas: MangaIdInfo[]): number[] {
return mangas.map((manga) => manga.id); return mangas.map((manga) => manga.id);
} }
static getFromCache<T>( static getFromCache<T = MangaBaseFieldsFragment>(
id: number, id: MangaIdInfo['id'],
fragment: DocumentNode = FULL_MANGA_FIELDS, fragment: DocumentNode = MANGA_BASE_FIELDS,
fragmentName: string = 'FULL_MANGA_FIELDS', fragmentName: string = 'MANGA_BASE_FIELDS',
): T | null { ): T | null {
return requestManager.graphQLClient.client.cache.readFragment<T>({ return requestManager.graphQLClient.client.cache.readFragment<T>({
id: requestManager.graphQLClient.client.cache.identify({ id: requestManager.graphQLClient.client.cache.identify({
@@ -236,8 +249,10 @@ export class Mangas {
return requestManager.getValidImgUrlFor(thumbnailUrl); return requestManager.getValidImgUrlFor(thumbnailUrl);
} }
static getDuplicateLibraryMangas(title: string): ReturnType<typeof requestManager.getMangas> { static getDuplicateLibraryMangas(
return requestManager.getMangas({ title: string,
): ReturnType<typeof requestManager.getMangas<GetMangasBaseQuery, GetMangasBaseQueryVariables>> {
return requestManager.getMangas<GetMangasBaseQuery, GetMangasBaseQueryVariables>(GET_MANGAS_BASE, {
condition: { inLibrary: true }, condition: { inLibrary: true },
filter: { title: { likeInsensitive: title } }, filter: { title: { likeInsensitive: title } },
}); });
@@ -429,7 +444,7 @@ export class Mangas {
} }
static async migrate( static async migrate(
mangaId: number, mangaId: MangaIdInfo['id'],
mangaIdToMigrateTo: number, mangaIdToMigrateTo: number,
{ {
mode, mode,

View File

@@ -49,12 +49,13 @@ export type TTrackerSearch = TTrackerBase & Pick<TrackerType, 'authUrl'>;
export type TTrackerBind = TTrackerBase & Pick<TrackerType, 'icon' | 'supportsTrackDeletion' | 'scores' | 'statuses'>; export type TTrackerBind = TTrackerBase & Pick<TrackerType, 'icon' | 'supportsTrackDeletion' | 'scores' | 'statuses'>;
type TrackerIdInfo = Pick<TrackerType, 'id'>;
type LoggedInInfo = Pick<TrackerType, 'isLoggedIn' | 'isTokenExpired'>; type LoggedInInfo = Pick<TrackerType, 'isLoggedIn' | 'isTokenExpired'>;
type TrackRecordTrackerInfo = { tracker: TTrackerBind }; type TrackRecordTrackerInfo = Pick<TrackRecordType, 'trackerId'>;
export class Trackers { export class Trackers {
static getIds(trackers: { id: number }[]): number[] { static getIds(trackers: TrackerIdInfo[]): number[] {
return trackers.map((tracker) => tracker.id); return trackers.map((tracker) => tracker.id);
} }
@@ -74,16 +75,19 @@ export class Trackers {
return trackers.filter(this.isLoggedIn); return trackers.filter(this.isLoggedIn);
} }
static getTrackers<TrackRecord extends TrackRecordTrackerInfo>( static getTrackers<TrackRecord extends TrackRecordTrackerInfo, Tracker extends TrackerIdInfo>(
trackRecords: TrackRecord[], trackRecords: TrackRecord[],
): TrackRecord['tracker'][] { trackers: Tracker[],
return trackRecords.map((trackRecord) => trackRecord.tracker); ): Tracker[] {
return trackRecords
.map((trackRecord) => trackers.find((tracker) => tracker.id === trackRecord.trackerId))
.filter((tracker) => !!tracker);
} }
static getTrackRecordFor<TrackRecord extends TrackRecordTrackerInfo>( static getTrackRecordFor<TrackRecord extends TrackRecordTrackerInfo>(
tracker: { id: number }, tracker: TrackerIdInfo,
trackRecords: TrackRecord[], trackRecords: TrackRecord[],
): TrackRecord | undefined { ): TrackRecord | undefined {
return trackRecords.find((trackRecord) => trackRecord.tracker.id === tracker.id); return trackRecords.find((trackRecord) => trackRecord.trackerId === tracker.id);
} }
} }

View File

@@ -7,9 +7,6 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { SOURCE_BASE_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts';
import { TRACKER_BIND_FIELDS } from '@/lib/graphql/fragments/TrackFragments.ts';
import { TRACK_RECORD_BIND_FIELDS } from '@/lib/graphql/fragments/TrackRecordFragments.ts';
export const PAGE_INFO = gql` export const PAGE_INFO = gql`
fragment PAGE_INFO on PageInfo { fragment PAGE_INFO on PageInfo {
@@ -27,147 +24,6 @@ export const GLOBAL_METADATA = gql`
} }
`; `;
export const FULL_CATEGORY_FIELDS = gql`
fragment FULL_CATEGORY_FIELDS on CategoryType {
default
id
includeInUpdate
includeInDownload
name
order
meta {
key
value
}
mangas {
totalCount
}
}
`;
export const FULL_TRACK_RECORD_FIELDS = gql`
${TRACK_RECORD_BIND_FIELDS}
${TRACKER_BIND_FIELDS}
fragment FULL_TRACK_RECORD_FIELDS on TrackRecordType {
...TRACK_RECORD_BIND_FIELDS
tracker {
...TRACKER_BIND_FIELDS
}
}
`;
export const BASE_MANGA_FIELDS = gql`
${SOURCE_BASE_FIELDS}
${FULL_TRACK_RECORD_FIELDS}
fragment BASE_MANGA_FIELDS on MangaType {
artist
author
chaptersLastFetchedAt
description
genre
id
inLibrary
inLibraryAt
initialized
lastFetchedAt
meta {
key
value
}
realUrl
source {
...SOURCE_BASE_FIELDS
}
status
thumbnailUrl
thumbnailUrlLastFetched
title
url
trackRecords {
totalCount
nodes {
...FULL_TRACK_RECORD_FIELDS
}
}
}
`;
export const PARTIAL_MANGA_FIELDS = gql`
${BASE_MANGA_FIELDS}
${FULL_CATEGORY_FIELDS}
fragment PARTIAL_MANGA_FIELDS on MangaType {
...BASE_MANGA_FIELDS
unreadCount
downloadCount
bookmarkCount
categories {
nodes {
...FULL_CATEGORY_FIELDS
}
totalCount
}
chapters {
totalCount
}
}
`;
export const FULL_CHAPTER_FIELDS = gql`
fragment FULL_CHAPTER_FIELDS on ChapterType {
chapterNumber
fetchedAt
id
isBookmarked
isDownloaded
isRead
lastPageRead
lastReadAt
mangaId
manga {
id
title
inLibrary
thumbnailUrl
lastFetchedAt
}
meta {
key
value
}
name
pageCount
realUrl
scanlator
sourceOrder
uploadDate
url
}
`;
export const FULL_MANGA_FIELDS = gql`
${PARTIAL_MANGA_FIELDS}
${FULL_CHAPTER_FIELDS}
fragment FULL_MANGA_FIELDS on MangaType {
...PARTIAL_MANGA_FIELDS
lastReadChapter {
...FULL_CHAPTER_FIELDS
}
latestReadChapter {
...FULL_CHAPTER_FIELDS
}
latestFetchedChapter {
...FULL_CHAPTER_FIELDS
}
latestUploadedChapter {
...FULL_CHAPTER_FIELDS
}
firstUnreadChapter {
...FULL_CHAPTER_FIELDS
}
}
`;
export const ABOUT_WEBUI = gql` export const ABOUT_WEBUI = gql`
fragment ABOUT_WEBUI on AboutWebUI { fragment ABOUT_WEBUI on AboutWebUI {
channel channel

View File

@@ -7,19 +7,7 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { MANGA_BASE_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
const MANGA_BASE_FIELDS = gql`
fragment MANGA_BASE_FIELDS on MangaType {
id
title
thumbnailUrl
thumbnailUrlLastFetched
inLibrary
initialized
}
`;
export const CHAPTER_BASE_FIELDS = gql` export const CHAPTER_BASE_FIELDS = gql`
fragment CHAPTER_BASE_FIELDS on ChapterType { fragment CHAPTER_BASE_FIELDS on ChapterType {

View File

@@ -0,0 +1,144 @@
/*
* 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 gql from 'graphql-tag';
export const MANGA_BASE_FIELDS = gql`
fragment MANGA_BASE_FIELDS on MangaType {
id
title
thumbnailUrl
thumbnailUrlLastFetched
inLibrary
initialized
sourceId
}
`;
export const MANGA_CHAPTER_STAT_FIELDS = gql`
fragment MANGA_CHAPTER_STAT_FIELDS on MangaType {
id
unreadCount
downloadCount
bookmarkCount
chapters {
totalCount
}
}
`;
export const MANGA_READER_FIELDS = gql`
${MANGA_BASE_FIELDS}
fragment MANGA_READER_FIELDS on MangaType {
...MANGA_BASE_FIELDS
meta {
key
value
}
chapters {
totalCount
}
trackRecords {
totalCount
}
}
`;
export const MANGA_LIBRARY_FIELDS = gql`
${MANGA_BASE_FIELDS}
${MANGA_CHAPTER_STAT_FIELDS}
fragment MANGA_LIBRARY_FIELDS on MangaType {
...MANGA_BASE_FIELDS
...MANGA_CHAPTER_STAT_FIELDS
genre
lastFetchedAt
inLibraryAt
trackRecords {
totalCount
nodes {
id
trackerId
}
}
firstUnreadChapter {
id
sourceOrder
}
lastReadChapter {
id
sourceOrder
lastReadAt
}
latestReadChapter {
id
sourceOrder
lastReadAt
}
latestFetchedChapter {
id
fetchedAt
}
latestUploadedChapter {
id
uploadDate
}
}
`;
export const MANGA_SCREEN_FIELDS = gql`
${MANGA_LIBRARY_FIELDS}
fragment MANGA_SCREEN_FIELDS on MangaType {
...MANGA_LIBRARY_FIELDS
artist
author
description
status
realUrl
sourceId
source {
id
displayName
}
trackRecords {
totalCount
nodes {
id
trackerId
}
}
}
`;
export const MANGA_LIBRARY_DUPLICATE_SCREEN_FIELDS = gql`
${MANGA_BASE_FIELDS}
${MANGA_CHAPTER_STAT_FIELDS}
fragment MANGA_LIBRARY_DUPLICATE_SCREEN_FIELDS on MangaType {
...MANGA_BASE_FIELDS
...MANGA_CHAPTER_STAT_FIELDS
description
}
`;

View File

@@ -7,7 +7,7 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { FULL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts'; import { MANGA_CHAPTER_STAT_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
const UPDATER_MANGA_FIELDS = gql` const UPDATER_MANGA_FIELDS = gql`
fragment UPDATER_MANGA_FIELDS on MangaType { fragment UPDATER_MANGA_FIELDS on MangaType {
@@ -19,7 +19,7 @@ const UPDATER_MANGA_FIELDS = gql`
export const UPDATER_SUBSCRIPTION_FIELDS = gql` export const UPDATER_SUBSCRIPTION_FIELDS = gql`
${UPDATER_MANGA_FIELDS} ${UPDATER_MANGA_FIELDS}
${FULL_MANGA_FIELDS} ${MANGA_CHAPTER_STAT_FIELDS}
fragment UPDATER_SUBSCRIPTION_FIELDS on UpdateStatus { fragment UPDATER_SUBSCRIPTION_FIELDS on UpdateStatus {
isRunning isRunning
@@ -28,7 +28,7 @@ export const UPDATER_SUBSCRIPTION_FIELDS = gql`
mangas { mangas {
totalCount totalCount
nodes { nodes {
...FULL_MANGA_FIELDS ...MANGA_CHAPTER_STAT_FIELDS
} }
} }
} }

View File

@@ -1,7 +1,7 @@
import {FieldPolicy, FieldReadFunction, Reference, TypePolicies, TypePolicy} from '@apollo/client/cache'; import {FieldPolicy, FieldReadFunction, Reference, TypePolicies, TypePolicy} from '@apollo/client/cache';
import { import {
GetChaptersMangaQuery, GetDownloadStatusQueryVariables, GetGlobalMetadataQueryVariables, GetChaptersMangaQuery, GetDownloadStatusQueryVariables, GetGlobalMetadataQueryVariables,
GetMangaQueryVariables, GetSourceBrowseQueryVariables, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQueryVariables, GetMangaScreenQueryVariables, GetSourceBrowseQueryVariables, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQueryVariables,
} from "@/lib/graphql/generated/graphql.ts"; } from "@/lib/graphql/generated/graphql.ts";
import {FieldFunctionOptions} from "@apollo/client/cache/inmemory/policies"; import {FieldFunctionOptions} from "@apollo/client/cache/inmemory/policies";
export type AboutServerPayloadKeySpecifier = ('buildTime' | 'buildType' | 'discord' | 'github' | 'name' | 'revision' | 'version' | AboutServerPayloadKeySpecifier)[]; export type AboutServerPayloadKeySpecifier = ('buildTime' | 'buildType' | 'discord' | 'github' | 'name' | 'revision' | 'version' | AboutServerPayloadKeySpecifier)[];
@@ -583,7 +583,7 @@ export type QueryFieldPolicy = {
extensions?: FieldPolicy<any> | FieldReadFunction<any>, extensions?: FieldPolicy<any> | FieldReadFunction<any>,
getWebUIUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>>, getWebUIUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>>,
lastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>, lastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
manga?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetMangaQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetMangaQueryVariables>>, manga?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>>,
mangas?: FieldPolicy<any> | FieldReadFunction<any>, mangas?: FieldPolicy<any> | FieldReadFunction<any>,
meta?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>>, meta?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>>,
metas?: FieldPolicy<any> | FieldReadFunction<any>, metas?: FieldPolicy<any> | FieldReadFunction<any>,

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { FULL_MANGA_FIELDS } from '@/lib/graphql/Fragments'; import { MANGA_SCREEN_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
export const DELETE_MANGA_METADATA = gql` export const DELETE_MANGA_METADATA = gql`
mutation DELETE_MANGA_METADATA($input: DeleteMangaMetaInput!) { mutation DELETE_MANGA_METADATA($input: DeleteMangaMetaInput!) {
@@ -37,12 +37,13 @@ export const DELETE_MANGA_METADATA = gql`
// makes the server fetch and return the manga // makes the server fetch and return the manga
export const GET_MANGA_FETCH = gql` export const GET_MANGA_FETCH = gql`
${FULL_MANGA_FIELDS} ${MANGA_SCREEN_FIELDS}
mutation GET_MANGA_FETCH($input: FetchMangaInput!) { mutation GET_MANGA_FETCH($input: FetchMangaInput!) {
fetchManga(input: $input) { fetchManga(input: $input) {
clientMutationId clientMutationId
manga { manga {
...FULL_MANGA_FIELDS ...MANGA_SCREEN_FIELDS
} }
} }
} }

View File

@@ -7,17 +7,18 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { BASE_MANGA_FIELDS } from '@/lib/graphql/Fragments';
import { SOURCE_SETTING_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts'; import { SOURCE_SETTING_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts';
import { MANGA_BASE_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
export const GET_SOURCE_MANGAS_FETCH = gql` export const GET_SOURCE_MANGAS_FETCH = gql`
${BASE_MANGA_FIELDS} ${MANGA_BASE_FIELDS}
mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) { mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) {
fetchSourceManga(input: $input) { fetchSourceManga(input: $input) {
clientMutationId clientMutationId
hasNextPage hasNextPage
mangas { mangas {
...BASE_MANGA_FIELDS ...MANGA_BASE_FIELDS
} }
} }
} }

View File

@@ -55,15 +55,13 @@ export const TRACKER_BIND = gql`
bindTrack(input: { mangaId: $mangaId, remoteId: $remoteId, trackerId: $trackerId }) { bindTrack(input: { mangaId: $mangaId, remoteId: $remoteId, trackerId: $trackerId }) {
trackRecord { trackRecord {
...TRACK_RECORD_BIND_FIELDS ...TRACK_RECORD_BIND_FIELDS
tracker {
id
}
manga { manga {
id id
trackRecords { trackRecords {
totalCount totalCount
nodes { nodes {
id id
trackerId
} }
} }
} }
@@ -83,6 +81,7 @@ export const TRACKER_UNBIND = gql`
totalCount totalCount
nodes { nodes {
id id
trackerId
} }
} }
} }
@@ -104,6 +103,7 @@ export const TRACKER_UPDATE_BIND = gql`
totalCount totalCount
nodes { nodes {
id id
trackerId
} }
} }
} }

View File

@@ -7,12 +7,13 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { FULL_MANGA_FIELDS, PAGE_INFO } from '@/lib/graphql/Fragments'; import { PAGE_INFO } from '@/lib/graphql/Fragments';
import { import {
CATEGORY_BASE_FIELDS, CATEGORY_BASE_FIELDS,
CATEGORY_LIBRARY_FIELDS, CATEGORY_LIBRARY_FIELDS,
CATEGORY_SETTING_FIELDS, CATEGORY_SETTING_FIELDS,
} from '@/lib/graphql/fragments/CategoryFragments.ts'; } from '@/lib/graphql/fragments/CategoryFragments.ts';
import { MANGA_LIBRARY_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
export const GET_CATEGORIES_BASE = gql` export const GET_CATEGORIES_BASE = gql`
${CATEGORY_BASE_FIELDS} ${CATEGORY_BASE_FIELDS}
@@ -126,14 +127,15 @@ export const GET_CATEGORIES_SETTINGS = gql`
`; `;
export const GET_CATEGORY_MANGAS = gql` export const GET_CATEGORY_MANGAS = gql`
${FULL_MANGA_FIELDS} ${MANGA_LIBRARY_FIELDS}
${PAGE_INFO} ${PAGE_INFO}
query GET_CATEGORY_MANGAS($id: Int!) { query GET_CATEGORY_MANGAS($id: Int!) {
category(id: $id) { category(id: $id) {
id id
mangas { mangas {
nodes { nodes {
...FULL_MANGA_FIELDS ...MANGA_LIBRARY_FIELDS
} }
pageInfo { pageInfo {
...PAGE_INFO ...PAGE_INFO

View File

@@ -7,15 +7,68 @@
*/ */
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { FULL_CHAPTER_FIELDS, FULL_MANGA_FIELDS, PAGE_INFO } from '@/lib/graphql/Fragments'; import { PAGE_INFO } from '@/lib/graphql/Fragments';
import {
MANGA_BASE_FIELDS,
MANGA_LIBRARY_DUPLICATE_SCREEN_FIELDS,
MANGA_LIBRARY_FIELDS,
MANGA_READER_FIELDS,
MANGA_SCREEN_FIELDS,
} from '@/lib/graphql/fragments/MangaFragments.ts';
import { TRACK_RECORD_BIND_FIELDS } from '@/lib/graphql/fragments/TrackRecordFragments.ts';
// returns the current manga from the database // returns the current manga from the database
export const GET_MANGA = gql` export const GET_MANGA_SCREEN = gql`
${FULL_MANGA_FIELDS} ${MANGA_SCREEN_FIELDS}
${FULL_CHAPTER_FIELDS}
query GET_MANGA($id: Int!) { query GET_MANGA_SCREEN($id: Int!) {
manga(id: $id) { manga(id: $id) {
...FULL_MANGA_FIELDS ...MANGA_SCREEN_FIELDS
}
}
`;
// returns the current manga from the database
export const GET_MANGA_READER = gql`
${MANGA_READER_FIELDS}
query GET_MANGA_READER($id: Int!) {
manga(id: $id) {
...MANGA_READER_FIELDS
}
}
`;
// returns the current manga from the database
export const GET_MANGA_TRACK_RECORDS = gql`
${TRACK_RECORD_BIND_FIELDS}
query GET_MANGA_TRACK_RECORDS($id: Int!) {
manga(id: $id) {
id
trackRecords {
totalCount
nodes {
...TRACK_RECORD_BIND_FIELDS
}
}
}
}
`;
// returns the current manga from the database
export const GET_MANGA_CATEGORIES = gql`
query GET_MANGA_CATEGORIES($id: Int!) {
manga(id: $id) {
id
categories {
totalCount
nodes {
id
}
}
} }
} }
`; `;
@@ -62,11 +115,11 @@ export const GET_MANGA_TO_MIGRATE = gql`
`; `;
// returns the current manga from the database // returns the current manga from the database
export const GET_MANGAS = gql` export const GET_MANGAS_BASE = gql`
${FULL_MANGA_FIELDS} ${MANGA_BASE_FIELDS}
${FULL_CHAPTER_FIELDS}
${PAGE_INFO} ${PAGE_INFO}
query GET_MANGAS(
query GET_MANGAS_BASE(
$after: Cursor $after: Cursor
$before: Cursor $before: Cursor
$condition: MangaConditionInput $condition: MangaConditionInput
@@ -89,7 +142,83 @@ export const GET_MANGAS = gql`
orderByType: $orderByType orderByType: $orderByType
) { ) {
nodes { nodes {
...FULL_MANGA_FIELDS ...MANGA_BASE_FIELDS
}
pageInfo {
...PAGE_INFO
}
totalCount
}
}
`;
// returns the current manga from the database
export const GET_MANGAS_LIBRARY = gql`
${MANGA_LIBRARY_FIELDS}
${PAGE_INFO}
query GET_MANGAS_LIBRARY(
$after: Cursor
$before: Cursor
$condition: MangaConditionInput
$filter: MangaFilterInput
$first: Int
$last: Int
$offset: Int
$orderBy: MangaOrderBy
$orderByType: SortOrder
) {
mangas(
after: $after
before: $before
condition: $condition
filter: $filter
first: $first
last: $last
offset: $offset
orderBy: $orderBy
orderByType: $orderByType
) {
nodes {
...MANGA_LIBRARY_FIELDS
}
pageInfo {
...PAGE_INFO
}
totalCount
}
}
`;
// returns the current manga from the database
export const GET_MANGAS_DUPLICATES = gql`
${MANGA_LIBRARY_DUPLICATE_SCREEN_FIELDS}
${PAGE_INFO}
query GET_MANGAS_DUPLICATES(
$after: Cursor
$before: Cursor
$condition: MangaConditionInput
$filter: MangaFilterInput
$first: Int
$last: Int
$offset: Int
$orderBy: MangaOrderBy
$orderByType: SortOrder
) {
mangas(
after: $after
before: $before
condition: $condition
filter: $filter
first: $first
last: $last
offset: $offset
orderBy: $orderBy
orderByType: $orderByType
) {
nodes {
...MANGA_LIBRARY_DUPLICATE_SCREEN_FIELDS
} }
pageInfo { pageInfo {
...PAGE_INFO ...PAGE_INFO
@@ -106,9 +235,7 @@ export const GET_MIGRATABLE_SOURCE_MANGAS = gql`
id id
title title
thumbnailUrl thumbnailUrl
source { sourceId
id
}
categories { categories {
nodes { nodes {
id id

View File

@@ -14,13 +14,13 @@ import {
Metadata, Metadata,
MetadataHolder, MetadataHolder,
MetadataKeyValuePair, MetadataKeyValuePair,
TManga,
} from '@/typings.ts'; } from '@/typings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { MetaType, SourceType } from '@/lib/graphql/generated/graphql.ts'; import { MetaType, SourceType } from '@/lib/graphql/generated/graphql.ts';
import { DEFAULT_DEVICE, getActiveDevice } from '@/util/device.ts'; import { DEFAULT_DEVICE, getActiveDevice } from '@/util/device.ts';
import { CategoryIdInfo } from '@/lib/data/Categories.ts'; import { CategoryIdInfo } from '@/lib/data/Categories.ts';
import { ChapterIdInfo } from '@/lib/data/Chapters.ts'; import { ChapterIdInfo } from '@/lib/data/Chapters.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
const APP_METADATA_KEY_PREFIX = 'webUI_'; const APP_METADATA_KEY_PREFIX = 'webUI_';
@@ -378,7 +378,7 @@ export const requestUpdateMetadataValue = async (
await requestManager.setGlobalMetadata(metadataKey, value).response; await requestManager.setGlobalMetadata(metadataKey, value).response;
break; break;
case 'manga': case 'manga':
await requestManager.setMangaMeta((metadataHolder as TManga).id, metadataKey, value).response; await requestManager.setMangaMeta((metadataHolder as MangaIdInfo).id, metadataKey, value).response;
break; break;
case 'source': case 'source':
await requestManager.setSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey, value) await requestManager.setSourceMeta((metadataHolder as Pick<SourceType, 'id'>).id, metadataKey, value)
@@ -400,7 +400,7 @@ export const requestUpdateServerMetadata = async (keysToValues: MetadataKeyValue
requestUpdateMetadata({}, 'global', keysToValues); requestUpdateMetadata({}, 'global', keysToValues);
export const requestUpdateMangaMetadata = async ( export const requestUpdateMangaMetadata = async (
manga: TManga, manga: MangaIdInfo & GqlMetaHolder,
keysToValues: MetadataKeyValuePair[], keysToValues: MetadataKeyValuePair[],
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues); ): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues);

View File

@@ -6,7 +6,7 @@
* 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 { Metadata, IReaderSettings, MetadataKeyValuePair, GqlMetaHolder, TManga } from '@/typings.ts'; import { Metadata, IReaderSettings, MetadataKeyValuePair, GqlMetaHolder } from '@/typings.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { import {
convertFromGqlMeta, convertFromGqlMeta,
@@ -15,6 +15,7 @@ import {
requestUpdateServerMetadata, requestUpdateServerMetadata,
} from '@/lib/metadata/metadata.ts'; } from '@/lib/metadata/metadata.ts';
import { MetaType } from '@/lib/graphql/generated/graphql.ts'; import { MetaType } from '@/lib/graphql/generated/graphql.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
type UndefinedReaderSettings = { type UndefinedReaderSettings = {
[setting in keyof IReaderSettings]: IReaderSettings[setting] | undefined; [setting in keyof IReaderSettings]: IReaderSettings[setting] | undefined;
@@ -109,7 +110,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
} }
if (metadataHolderType === 'manga') { if (metadataHolderType === 'manga') {
await requestUpdateMangaMetadata(metadataHolder as TManga, settingsToUpdate); await requestUpdateMangaMetadata(metadataHolder as MangaIdInfo & GqlMetaHolder, settingsToUpdate);
return; return;
} }

View File

@@ -81,10 +81,6 @@ import {
GetMangaChaptersFetchMutationVariables, GetMangaChaptersFetchMutationVariables,
GetMangaFetchMutation, GetMangaFetchMutation,
GetMangaFetchMutationVariables, GetMangaFetchMutationVariables,
GetMangaQuery,
GetMangaQueryVariables,
GetMangasQuery,
GetMangasQueryVariables,
GetRestoreStatusQuery, GetRestoreStatusQuery,
GetRestoreStatusQueryVariables, GetRestoreStatusQueryVariables,
GetServerSettingsQuery, GetServerSettingsQuery,
@@ -197,6 +193,8 @@ import {
GetChaptersUpdatesQueryVariables, GetChaptersUpdatesQueryVariables,
GetSourcesListQuery, GetSourcesListQuery,
GetSourcesListQueryVariables, GetSourcesListQueryVariables,
GetMangasLibraryQuery,
GetMangasLibraryQueryVariables,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts'; import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts';
import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts'; import { SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
@@ -223,9 +221,9 @@ import {
UPDATE_MANGAS_CATEGORIES, UPDATE_MANGAS_CATEGORIES,
} from '@/lib/graphql/mutations/MangaMutation.ts'; } from '@/lib/graphql/mutations/MangaMutation.ts';
import { import {
GET_MANGA,
GET_MANGA_TO_MIGRATE, GET_MANGA_TO_MIGRATE,
GET_MANGAS, GET_MANGA_TRACK_RECORDS,
GET_MANGAS_LIBRARY,
GET_MIGRATABLE_SOURCE_MANGAS, GET_MIGRATABLE_SOURCE_MANGAS,
} from '@/lib/graphql/queries/MangaQuery.ts'; } from '@/lib/graphql/queries/MangaQuery.ts';
import { import {
@@ -283,7 +281,7 @@ import { DOWNLOAD_STATUS_SUBSCRIPTION } from '@/lib/graphql/subscriptions/Downlo
import { UPDATER_SUBSCRIPTION } from '@/lib/graphql/subscriptions/UpdaterSubscription.ts'; import { UPDATER_SUBSCRIPTION } from '@/lib/graphql/subscriptions/UpdaterSubscription.ts';
import { GET_SERVER_SETTINGS } from '@/lib/graphql/queries/SettingsQuery.ts'; import { GET_SERVER_SETTINGS } from '@/lib/graphql/queries/SettingsQuery.ts';
import { UPDATE_SERVER_SETTINGS } from '@/lib/graphql/mutations/SettingsMutation.ts'; import { UPDATE_SERVER_SETTINGS } from '@/lib/graphql/mutations/SettingsMutation.ts';
import { BASE_MANGA_FIELDS, GLOBAL_METADATA } from '@/lib/graphql/Fragments.ts'; import { GLOBAL_METADATA } from '@/lib/graphql/Fragments.ts';
import { CLEAR_SERVER_CACHE } from '@/lib/graphql/mutations/ImageMutation.ts'; import { CLEAR_SERVER_CACHE } from '@/lib/graphql/mutations/ImageMutation.ts';
import { RESET_WEBUI_UPDATE_STATUS, UPDATE_WEBUI } from '@/lib/graphql/mutations/ServerInfoMutation.ts'; import { RESET_WEBUI_UPDATE_STATUS, UPDATE_WEBUI } from '@/lib/graphql/mutations/ServerInfoMutation.ts';
import { WEBUI_UPDATE_SUBSCRIPTION } from '@/lib/graphql/subscriptions/ServerInfoSubscription.ts'; import { WEBUI_UPDATE_SUBSCRIPTION } from '@/lib/graphql/subscriptions/ServerInfoSubscription.ts';
@@ -301,9 +299,11 @@ import {
TRACKER_UPDATE_BIND, TRACKER_UPDATE_BIND,
} from '@/lib/graphql/mutations/TrackerMutation.ts'; } from '@/lib/graphql/mutations/TrackerMutation.ts';
import { ControlledPromise } from '@/lib/ControlledPromise.ts'; import { ControlledPromise } from '@/lib/ControlledPromise.ts';
import { MetadataMigrationSettings, TManga } from '@/typings.ts'; import { MetadataMigrationSettings } from '@/typings.ts';
import { DOWNLOAD_STATUS_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts'; import { DOWNLOAD_STATUS_FIELDS } from '@/lib/graphql/fragments/DownloadFragments.ts';
import { EXTENSION_LIST_FIELDS } from '@/lib/graphql/fragments/ExtensionFragments.ts'; import { EXTENSION_LIST_FIELDS } from '@/lib/graphql/fragments/ExtensionFragments.ts';
import { MANGA_BASE_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
enum GQLMethod { enum GQLMethod {
QUERY = 'QUERY', QUERY = 'QUERY',
@@ -1555,8 +1555,8 @@ export class RequestManager {
(manga) => (manga) =>
this.graphQLClient.client.cache.readFragment<typeof manga>({ this.graphQLClient.client.cache.readFragment<typeof manga>({
id: this.graphQLClient.client.cache.identify(manga), id: this.graphQLClient.client.cache.identify(manga),
fragment: BASE_MANGA_FIELDS, fragment: MANGA_BASE_FIELDS,
fragmentName: 'BASE_MANGA_FIELDS', fragmentName: 'MANGA_BASE_FIELDS',
}) ?? manga, }) ?? manga,
), ),
}, },
@@ -1630,18 +1630,20 @@ export class RequestManager {
); );
} }
public useGetManga( public useGetManga<Data, Variables extends OperationVariables = OperationVariables>(
document: DocumentNode | TypedDocumentNode<Data, Variables>,
mangaId: number | string, mangaId: number | string,
options?: QueryHookOptions<GetMangaQuery, GetMangaQueryVariables>, options?: QueryHookOptions<Data, Variables>,
): AbortableApolloUseQueryResponse<GetMangaQuery, GetMangaQueryVariables> { ): AbortableApolloUseQueryResponse<Data, Variables> {
return this.doRequest(GQLMethod.USE_QUERY, GET_MANGA, { id: Number(mangaId) }, options); return this.doRequest(GQLMethod.USE_QUERY, document, { id: Number(mangaId) } as unknown as Variables, options);
} }
public getManga( public getManga<Data, Variables extends OperationVariables = OperationVariables>(
document: DocumentNode | TypedDocumentNode<Data, Variables>,
mangaId: number | string, mangaId: number | string,
options?: QueryOptions<GetMangaQueryVariables, GetMangaQuery>, options?: QueryOptions<Variables, Data>,
): AbortabaleApolloQueryResponse<GetMangaQuery> { ): AbortabaleApolloQueryResponse<Data> {
return this.doRequest(GQLMethod.QUERY, GET_MANGA, { id: Number(mangaId) }, options); return this.doRequest(GQLMethod.QUERY, document, { id: Number(mangaId) } as unknown as Variables, options);
} }
public getMangaToMigrate( public getMangaToMigrate(
@@ -1712,18 +1714,20 @@ export class RequestManager {
); );
} }
public useGetMangas( public useGetMangas<Data, Variables extends OperationVariables = OperationVariables>(
variables: GetMangasQueryVariables, document: DocumentNode | TypedDocumentNode<Data, Variables>,
options?: QueryHookOptions<GetMangasQuery, GetMangasQueryVariables>, variables: Variables,
): AbortableApolloUseQueryResponse<GetMangasQuery, GetMangasQueryVariables> { options?: QueryHookOptions<Data, Variables>,
return this.doRequest(GQLMethod.USE_QUERY, GET_MANGAS, variables, options); ): AbortableApolloUseQueryResponse<Data, Variables> {
return this.doRequest(GQLMethod.USE_QUERY, document, variables, options);
} }
public getMangas( public getMangas<Data, Variables extends OperationVariables = OperationVariables>(
variables: GetMangasQueryVariables, document: DocumentNode | TypedDocumentNode<Data, Variables>,
options?: QueryOptions<GetMangasQueryVariables, GetMangasQuery>, variables: Variables,
): AbortabaleApolloQueryResponse<GetMangasQuery> { options?: QueryOptions<Variables, Data>,
return this.doRequest(GQLMethod.QUERY, GET_MANGAS, variables, options); ): AbortabaleApolloQueryResponse<Data> {
return this.doRequest(GQLMethod.QUERY, document, variables, options);
} }
public useGetMigratableSourceMangas( public useGetMigratableSourceMangas(
@@ -2014,7 +2018,7 @@ export class RequestManager {
public updateChapters( public updateChapters(
ids: number[], ids: number[],
patch: UpdateChapterPatchInput & { chapterIdsToDelete?: number[]; trackProgressMangaId?: TManga['id'] }, patch: UpdateChapterPatchInput & { chapterIdsToDelete?: number[]; trackProgressMangaId?: MangaIdInfo['id'] },
options?: MutationOptions<UpdateChaptersMutation, UpdateChaptersMutationVariables>, options?: MutationOptions<UpdateChaptersMutation, UpdateChaptersMutationVariables>,
): AbortableApolloMutationResponse<UpdateChaptersMutation> { ): AbortableApolloMutationResponse<UpdateChaptersMutation> {
const { chapterIdsToDelete = [], trackProgressMangaId = -1, ...updatePatch } = patch; const { chapterIdsToDelete = [], trackProgressMangaId = -1, ...updatePatch } = patch;
@@ -2144,8 +2148,8 @@ export class RequestManager {
public useGetCategoryMangas( public useGetCategoryMangas(
id: number, id: number,
options?: QueryHookOptions<GetMangasQuery, GetMangasQueryVariables>, options?: QueryHookOptions<GetMangasLibraryQuery, GetMangasLibraryQueryVariables>,
): AbortableApolloUseQueryResponse<GetMangasQuery, GetMangasQueryVariables> { ): AbortableApolloUseQueryResponse<GetMangasLibraryQuery, GetMangasLibraryQueryVariables> {
const isDefaultCategory = id === 0; const isDefaultCategory = id === 0;
if (isDefaultCategory) { if (isDefaultCategory) {
// hacky way of loading the default category mangas - some stuff won't work but since that is not used anyway, it won't be a problem // hacky way of loading the default category mangas - some stuff won't work but since that is not used anyway, it won't be a problem
@@ -2165,10 +2169,10 @@ export class RequestManager {
__typename: 'Query', __typename: 'Query',
} }
: undefined, : undefined,
} as unknown as AbortableApolloUseQueryResponse<GetMangasQuery, GetMangasQueryVariables>; } as unknown as AbortableApolloUseQueryResponse<GetMangasLibraryQuery, GetMangasLibraryQueryVariables>;
} }
return this.useGetMangas({ condition: { inLibrary: true, categoryIds: [id] } }, options); return this.useGetMangas(GET_MANGAS_LIBRARY, { condition: { inLibrary: true, categoryIds: [id] } }, options);
} }
public deleteCategory( public deleteCategory(
@@ -2597,7 +2601,7 @@ export class RequestManager {
GQLMethod.MUTATION, GQLMethod.MUTATION,
TRACKER_UNBIND, TRACKER_UNBIND,
{ input: { recordId, deleteRemoteTrack } }, { input: { recordId, deleteRemoteTrack } },
{ refetchQueries: [GET_MANGA], ...options }, { refetchQueries: [GET_MANGA_TRACK_RECORDS], ...options },
); );
} }

View File

@@ -23,17 +23,22 @@ import { UpdateChecker } from '@/components/library/UpdateChecker';
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { NavBarContext } from '@/components/context/NavbarContext.tsx';
import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts';
import { TManga } from '@/typings.ts';
import { SelectableCollectionSelectMode } from '@/components/collection/SelectableCollectionSelectMode.tsx'; import { SelectableCollectionSelectMode } from '@/components/collection/SelectableCollectionSelectMode.tsx';
import { useGetVisibleLibraryMangas } from '@/components/library/useGetVisibleLibraryMangas.ts'; import { useGetVisibleLibraryMangas } from '@/components/library/useGetVisibleLibraryMangas.ts';
import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx'; import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx';
import { PARTIAL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts';
import { MangaActionMenuItems } from '@/components/manga/MangaActionMenuItems.tsx'; import { MangaActionMenuItems } from '@/components/manga/MangaActionMenuItems.tsx';
import { TabsMenu } from '@/components/tabs/TabsMenu.tsx'; import { TabsMenu } from '@/components/tabs/TabsMenu.tsx';
import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx'; import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { GetCategoriesLibraryQuery, GetCategoriesLibraryQueryVariables } from '@/lib/graphql/generated/graphql.ts'; import {
GetCategoriesLibraryQuery,
GetCategoriesLibraryQueryVariables,
MangaChapterStatFieldsFragment,
MangaType,
} from '@/lib/graphql/generated/graphql.ts';
import { GET_CATEGORIES_LIBRARY } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_CATEGORIES_LIBRARY } from '@/lib/graphql/queries/CategoryQuery.ts';
import { Mangas } from '@/lib/data/Mangas.ts';
import { MANGA_CHAPTER_STAT_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts';
const TitleWithSizeTag = styled('span')({ const TitleWithSizeTag = styled('span')({
display: 'flex', display: 'flex',
@@ -95,7 +100,7 @@ export function Library() {
handleSelectAll, handleSelectAll,
handleSelection, handleSelection,
clearSelection, clearSelection,
} = useSelectableCollection<TManga['id'], string>(mangas.length, { } = useSelectableCollection<MangaType['id'], string>(mangas.length, {
itemIds: mangaIds, itemIds: mangaIds,
currentKey: activeTab?.id.toString(), currentKey: activeTab?.id.toString(),
}); });
@@ -107,14 +112,15 @@ export function Library() {
const selectedMangas = useMemo( const selectedMangas = useMemo(
() => () =>
selectedItemIds.map( selectedItemIds
(id) => .map((id) =>
requestManager.graphQLClient.client.cache.readFragment<TManga>({ Mangas.getFromCache<MangaChapterStatFieldsFragment>(
id: requestManager.graphQLClient.client.cache.identify({ __typename: 'MangaType', id }), id,
fragment: PARTIAL_MANGA_FIELDS, MANGA_CHAPTER_STAT_FIELDS,
fragmentName: 'PARTIAL_MANGA_FIELDS', 'MANGA_CHAPTER_STAT_FIELDS',
})!,
), ),
)
.filter((manga) => !!manga),
[selectedItemIds.length, mangas], [selectedItemIds.length, mangas],
); );

View File

@@ -24,6 +24,8 @@ import { MangaDetails } from '@/components/manga/MangaDetails';
import { MangaToolbarMenu } from '@/components/manga/MangaToolbarMenu'; import { MangaToolbarMenu } from '@/components/manga/MangaToolbarMenu';
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx'; import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
import { GetMangaScreenQuery } from '@/lib/graphql/generated/graphql.ts';
import { GET_MANGA_SCREEN } from '@/lib/graphql/queries/MangaQuery.ts';
export const Manga: React.FC = () => { export const Manga: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -32,7 +34,13 @@ export const Manga: React.FC = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const autofetchedRef = useRef(false); const autofetchedRef = useRef(false);
const { data, error: mangaError, loading: isLoading, networkStatus, refetch } = requestManager.useGetManga(id); const {
data,
error: mangaError,
loading: isLoading,
networkStatus,
refetch,
} = requestManager.useGetManga<GetMangaScreenQuery>(GET_MANGA_SCREEN, id);
const isValidating = isNetworkRequestInFlight(networkStatus); const isValidating = isNetworkRequestInFlight(networkStatus);
const manga = data?.manga; const manga = data?.manga;

View File

@@ -14,8 +14,6 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { TMigratableSource } from '@/components/MigrationCard.tsx'; import { TMigratableSource } from '@/components/MigrationCard.tsx';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx'; import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
import { MangaGrid } from '@/components/MangaGrid.tsx';
import { TPartialManga } from '@/typings.ts';
import { GridLayouts } from '@/components/source/GridLayouts.tsx'; import { GridLayouts } from '@/components/source/GridLayouts.tsx';
import { useLocalStorage } from '@/util/useStorage.tsx'; import { useLocalStorage } from '@/util/useStorage.tsx';
import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx';
@@ -23,6 +21,7 @@ import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts
import { GetSourceMigratableQuery, GetSourceMigratableQueryVariables } from '@/lib/graphql/generated/graphql.ts'; import { GetSourceMigratableQuery, GetSourceMigratableQueryVariables } from '@/lib/graphql/generated/graphql.ts';
import { GET_SOURCE_MIGRATABLE } from '@/lib/graphql/queries/SourceQuery.ts'; import { GET_SOURCE_MIGRATABLE } from '@/lib/graphql/queries/SourceQuery.ts';
import { SOURCE_BASE_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts'; import { SOURCE_BASE_FIELDS } from '@/lib/graphql/fragments/SourceFragments.ts';
import { BaseMangaGrid } from '@/components/source/BaseMangaGrid.tsx';
export const Migrate = () => { export const Migrate = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -117,11 +116,11 @@ export const Migrate = () => {
} }
return ( return (
<MangaGrid <BaseMangaGrid
hasNextPage={false} hasNextPage={false}
loadMore={() => {}} loadMore={() => {}}
isLoading={areMangasLoading} isLoading={areMangasLoading}
mangas={(migratableSourceMangasData?.mangas.nodes ?? []) as TPartialManga[]} mangas={migratableSourceMangasData?.mangas.nodes ?? []}
gridLayout={gridLayout} gridLayout={gridLayout}
mode="migrate.search" mode="migrate.search"
/> />

View File

@@ -11,7 +11,7 @@ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'r
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings, ReaderType, TManga } from '@/typings'; import { AllowedMetadataValueTypes, ChapterOffset, IReaderSettings, ReaderType } from '@/typings';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { import {
checkAndHandleMissingStoredReaderSettings, checkAndHandleMissingStoredReaderSettings,
@@ -31,6 +31,7 @@ import { useDebounce } from '@/util/useDebounce.ts';
import { import {
GetChaptersReaderQuery, GetChaptersReaderQuery,
GetChaptersReaderQueryVariables, GetChaptersReaderQueryVariables,
GetMangaReaderQuery,
UpdateChapterPatchInput, UpdateChapterPatchInput,
} from '@/lib/graphql/generated/graphql.ts'; } from '@/lib/graphql/generated/graphql.ts';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts'; import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
@@ -38,6 +39,8 @@ import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts
import { Chapters } from '@/lib/data/Chapters.ts'; import { Chapters } from '@/lib/data/Chapters.ts';
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx'; import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
import { GET_CHAPTERS_READER } from '@/lib/graphql/queries/ChapterQuery.ts'; import { GET_CHAPTERS_READER } from '@/lib/graphql/queries/ChapterQuery.ts';
import { GET_MANGA_READER } from '@/lib/graphql/queries/MangaQuery.ts';
import { TMangaReader } from '@/lib/data/Mangas.ts';
type TChapter = GetChaptersReaderQuery['chapters']['nodes'][number]; type TChapter = GetChaptersReaderQuery['chapters']['nodes'][number];
@@ -94,7 +97,7 @@ export function Reader() {
lastReadAt: 0, lastReadAt: 0,
chapters: { totalCount: 0 }, chapters: { totalCount: 0 },
trackRecords: { totalCount: 0 }, trackRecords: { totalCount: 0 },
}) as unknown as TManga, }) as unknown as TMangaReader,
[mangaId], [mangaId],
); );
@@ -103,7 +106,7 @@ export function Reader() {
loading: isMangaLoading, loading: isMangaLoading,
error: mangaError, error: mangaError,
refetch: refetchManga, refetch: refetchManga,
} = requestManager.useGetManga(mangaId); } = requestManager.useGetManga<GetMangaReaderQuery>(GET_MANGA_READER, mangaId);
const loadedChapter = useRef<TChapter | null>(null); const loadedChapter = useRef<TChapter | null>(null);
const isChapterLoaded = const isChapterLoaded =
Number(mangaId) === loadedChapter.current?.mangaId && Number(mangaId) === loadedChapter.current?.mangaId &&

View File

@@ -19,7 +19,6 @@ import { langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from '@/uti
import { translateExtensionLanguage } from '@/screens/util/Extensions'; import { translateExtensionLanguage } from '@/screens/util/Extensions';
import { AppbarSearch } from '@/components/util/AppbarSearch'; import { AppbarSearch } from '@/components/util/AppbarSearch';
import { LangSelect } from '@/components/navbar/action/LangSelect'; import { LangSelect } from '@/components/navbar/action/LangSelect';
import { MangaGrid } from '@/components/MangaGrid';
import { useDebounce } from '@/util/useDebounce.ts'; import { useDebounce } from '@/util/useDebounce.ts';
import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { NavBarContext } from '@/components/context/NavbarContext.tsx';
import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx'; import { MangaCardProps } from '@/components/manga/MangaCard.types.tsx';
@@ -28,6 +27,7 @@ import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx'; import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
import { SourceType } from '@/lib/graphql/generated/graphql.ts'; import { SourceType } from '@/lib/graphql/generated/graphql.ts';
import { BaseMangaGrid } from '@/components/source/BaseMangaGrid.tsx';
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean }; type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean };
type SourceToLoadingStateMap = Map<string, SourceLoadingState>; type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
@@ -166,7 +166,7 @@ const SourceSearchPreview = React.memo(
} }
/> />
) : ( ) : (
<MangaGrid <BaseMangaGrid
mangas={mangas} mangas={mangas}
isLoading={isLoading} isLoading={isLoading}
hasNextPage={false} hasNextPage={false}

View File

@@ -20,7 +20,7 @@ import { styled } from '@mui/material/styles';
import FavoriteIcon from '@mui/icons-material/Favorite'; import FavoriteIcon from '@mui/icons-material/Favorite';
import NewReleasesIcon from '@mui/icons-material/NewReleases'; import NewReleasesIcon from '@mui/icons-material/NewReleases';
import FilterListIcon from '@mui/icons-material/FilterList'; import FilterListIcon from '@mui/icons-material/FilterList';
import { IPos, TPartialManga, TranslationKey } from '@/typings'; import { IPos, TranslationKey } from '@/typings';
import { import {
requestManager, requestManager,
AbortableApolloUseMutationPaginatedResponse, AbortableApolloUseMutationPaginatedResponse,
@@ -30,7 +30,7 @@ import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsCon
import { SourceGridLayout } from '@/components/source/SourceGridLayout'; import { SourceGridLayout } from '@/components/source/SourceGridLayout';
import { AppbarSearch } from '@/components/util/AppbarSearch'; import { AppbarSearch } from '@/components/util/AppbarSearch';
import { SourceOptions } from '@/components/source/SourceOptions'; import { SourceOptions } from '@/components/source/SourceOptions';
import { SourceMangaGrid } from '@/components/source/SourceMangaGrid'; import { BaseMangaGrid } from '@/components/source/BaseMangaGrid.tsx';
import { import {
GetSourceBrowseQuery, GetSourceBrowseQuery,
GetSourceBrowseQueryVariables, GetSourceBrowseQueryVariables,
@@ -45,6 +45,7 @@ import { getGridSnapshotKey } from '@/components/MangaGrid.tsx';
import { createUpdateSourceMetadata, getSourceMetadata } from '@/lib/metadata/sourceMetadata.ts'; import { createUpdateSourceMetadata, getSourceMetadata } from '@/lib/metadata/sourceMetadata.ts';
import { makeToast } from '@/components/util/Toast.tsx'; import { makeToast } from '@/components/util/Toast.tsx';
import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.ts'; import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
const ContentTypeMenu = styled('div')(({ theme }) => ({ const ContentTypeMenu = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@@ -92,9 +93,9 @@ const SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY: { [contentType in SourceContentType]
[SourceContentType.SEARCH]: 'manga.error.label.no_matches', [SourceContentType.SEARCH]: 'manga.error.label.no_matches',
}; };
const getUniqueMangas = (mangas: TPartialManga[]): TPartialManga[] => { const getUniqueMangas = <Manga extends MangaIdInfo>(mangas: Manga[]): Manga[] => {
const mangaIdToManga: Record<TPartialManga['id'], TPartialManga> = {}; const mangaIdToManga: Record<string, Manga> = {};
const uniqueMangas: TPartialManga[] = []; const uniqueMangas: Manga[] = [];
mangas.forEach((manga) => { mangas.forEach((manga) => {
const isDuplicate = !!mangaIdToManga[manga.id]; const isDuplicate = !!mangaIdToManga[manga.id];
@@ -470,7 +471,7 @@ export function SourceMangas() {
{t('global.button.filter')} {t('global.button.filter')}
</ContentTypeButton> </ContentTypeButton>
</ContentTypeMenu> </ContentTypeMenu>
<SourceMangaGrid <BaseMangaGrid
key={contentType} key={contentType}
mangas={mangas} mangas={mangas}
hasNextPage={hasNextPage} hasNextPage={hasNextPage}
@@ -479,6 +480,8 @@ export function SourceMangas() {
messageExtra={messageExtra} messageExtra={messageExtra}
isLoading={isLoading} isLoading={isLoading}
gridLayout={options.SourcegridLayout} gridLayout={options.SourcegridLayout}
mode="source"
inLibraryIndicator
/> />
{contentType === SourceContentType.SEARCH && ( {contentType === SourceContentType.SEARCH && (
<SourceOptions <SourceOptions

View File

@@ -15,8 +15,6 @@ import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { MangaGrid } from '@/components/MangaGrid.tsx';
import { TManga } from '@/typings';
import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { NavBarContext } from '@/components/context/NavbarContext.tsx';
import { useLocalStorage } from '@/util/useStorage.tsx'; import { useLocalStorage } from '@/util/useStorage.tsx';
import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx'; import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx';
@@ -28,20 +26,34 @@ import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts
import { MangaCard } from '@/components/MangaCard.tsx'; import { MangaCard } from '@/components/MangaCard.tsx';
import { StyledGroupedVirtuoso } from '@/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { StyledGroupedVirtuoso } from '@/components/virtuoso/StyledGroupedVirtuoso.tsx';
import { StyledGroupHeader } from '@/components/virtuoso/StyledGroupHeader.tsx'; import { StyledGroupHeader } from '@/components/virtuoso/StyledGroupHeader.tsx';
import {
GetMangasDuplicatesQuery,
GetMangasDuplicatesQueryVariables,
MangaType,
} from '@/lib/graphql/generated/graphql.ts';
import { GET_MANGAS_DUPLICATES } from '@/lib/graphql/queries/MangaQuery.ts';
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
import { BaseMangaGrid } from '@/components/source/BaseMangaGrid.tsx';
import { IMangaGridProps } from '@/components/MangaGrid.tsx';
const findDuplicatesByTitle = (libraryMangas: TManga[]): Record<string, TManga[]> => { const findDuplicatesByTitle = <Manga extends Pick<MangaType, 'title'>>(
libraryMangas: Manga[],
): Record<string, Manga[]> => {
const titleToMangas = Object.groupBy(libraryMangas, ({ title }) => title.toLowerCase().trim()); const titleToMangas = Object.groupBy(libraryMangas, ({ title }) => title.toLowerCase().trim());
return Object.fromEntries( return Object.fromEntries(
Object.entries(titleToMangas) Object.entries(titleToMangas)
.filter((titleToMangaMap): titleToMangaMap is [string, TManga[]] => (titleToMangaMap[1]?.length ?? 0) > 1) .filter((titleToMangaMap): titleToMangaMap is [string, Manga[]] => (titleToMangaMap[1]?.length ?? 0) > 1)
.map(([, mangas]) => [mangas[0].title, mangas]), .map(([, mangas]) => [mangas[0].title, mangas]),
); );
}; };
const findDuplicatesByTitleAndAlternativeTitles = (libraryMangas: TManga[]): Record<string, TManga[]> => { type TMangaDuplicate = Pick<MangaType, 'id' | 'title' | 'description'>;
const idToDuplicateStatus: Record<TManga['id'], boolean> = {}; const findDuplicatesByTitleAndAlternativeTitles = <Manga extends TMangaDuplicate>(
const titleToMangas: Record<string, TManga[]> = {}; libraryMangas: Manga[],
): Record<string, Manga[]> => {
const idToDuplicateStatus: Record<MangaIdInfo['id'], boolean> = {};
const titleToMangas: Record<string, Manga[]> = {};
libraryMangas.forEach((mangaToCheck) => libraryMangas.forEach((mangaToCheck) =>
libraryMangas.forEach((libraryManga) => { libraryMangas.forEach((libraryManga) => {
@@ -123,10 +135,13 @@ export const LibraryDuplicates = () => {
}; };
}, [t, gridLayout, checkAlternativeTitles]); }, [t, gridLayout, checkAlternativeTitles]);
const { data, loading, error, refetch } = requestManager.useGetMangas({ condition: { inLibrary: true } }); const { data, loading, error, refetch } = requestManager.useGetMangas<
GetMangasDuplicatesQuery,
GetMangasDuplicatesQueryVariables
>(GET_MANGAS_DUPLICATES, { condition: { inLibrary: true } });
const mangasByTitle = useMemo(() => { const mangasByTitle = useMemo(() => {
const libraryMangas: TManga[] = data?.mangas.nodes ?? []; const libraryMangas: TMangaDuplicate[] = data?.mangas.nodes ?? [];
if (checkAlternativeTitles) { if (checkAlternativeTitles) {
return findDuplicatesByTitleAndAlternativeTitles(libraryMangas); return findDuplicatesByTitleAndAlternativeTitles(libraryMangas);
@@ -172,7 +187,7 @@ export const LibraryDuplicates = () => {
itemContent={(index) => ( itemContent={(index) => (
<Box key={duplicatedMangas[index].id} sx={{ px: 1, pb: 1 }}> <Box key={duplicatedMangas[index].id} sx={{ px: 1, pb: 1 }}>
<MangaCard <MangaCard
manga={duplicatedMangas[index]} manga={duplicatedMangas[index] as IMangaGridProps['mangas'][number]}
gridLayout={gridLayout} gridLayout={gridLayout}
selected={null} selected={null}
mode="duplicate" mode="duplicate"
@@ -188,12 +203,13 @@ export const LibraryDuplicates = () => {
<StyledGroupHeader sx={{ pt: index === 0 ? undefined : 0, pb: 0 }} variant="h5" isFirstItem={false}> <StyledGroupHeader sx={{ pt: index === 0 ? undefined : 0, pb: 0 }} variant="h5" isFirstItem={false}>
{title} {title}
</StyledGroupHeader> </StyledGroupHeader>
<MangaGrid <BaseMangaGrid
mangas={mangasByTitle[title]} mangas={mangasByTitle[title] as IMangaGridProps['mangas']}
hasNextPage={false} hasNextPage={false}
loadMore={() => {}} loadMore={() => {}}
isLoading={false} isLoading={false}
gridLayout={gridLayout} gridLayout={gridLayout}
inLibraryIndicator={false}
horizontal horizontal
mode="duplicate" mode="duplicate"
/> />

View File

@@ -29,14 +29,23 @@ import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCe
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts'; import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder.tsx';
import { ListItemLink } from '@/components/util/ListItemLink.tsx'; import { ListItemLink } from '@/components/util/ListItemLink.tsx';
import { GetCategoriesSettingsQuery, GetCategoriesSettingsQueryVariables } from '@/lib/graphql/generated/graphql.ts'; import {
GetCategoriesSettingsQuery,
GetCategoriesSettingsQueryVariables,
GetMangasBaseQuery,
GetMangasBaseQueryVariables,
} from '@/lib/graphql/generated/graphql.ts';
import { GET_CATEGORIES_SETTINGS } from '@/lib/graphql/queries/CategoryQuery.ts'; import { GET_CATEGORIES_SETTINGS } from '@/lib/graphql/queries/CategoryQuery.ts';
import { GET_MANGAS_BASE } from '@/lib/graphql/queries/MangaQuery.ts';
const removeNonLibraryMangasFromCategories = async (): Promise<void> => { const removeNonLibraryMangasFromCategories = async (): Promise<void> => {
try { try {
const nonLibraryMangas = await requestManager.getMangas({ const nonLibraryMangas = await requestManager.getMangas<GetMangasBaseQuery, GetMangasBaseQueryVariables>(
GET_MANGAS_BASE,
{
filter: { inLibrary: { equalTo: false }, categoryId: { isNull: false } }, filter: { inLibrary: { equalTo: false }, categoryId: { isNull: false } },
}).response; },
).response;
const mangaIdsToRemove = Mangas.getIds(nonLibraryMangas.data.mangas.nodes); const mangaIdsToRemove = Mangas.getIds(nonLibraryMangas.data.mangas.nodes);

View File

@@ -12,10 +12,10 @@ import { ParseKeys } from 'i18next';
import { Location } from 'react-router-dom'; import { Location } from 'react-router-dom';
import { import {
GetChaptersReaderQuery, GetChaptersReaderQuery,
GetMangaQuery,
GetServerSettingsQuery, GetServerSettingsQuery,
GetSourceBrowseQuery, GetSourceBrowseQuery,
GetSourceSettingsQuery, GetSourceSettingsQuery,
MangaReaderFieldsFragment,
MetaType, MetaType,
SourcePreferenceChangeInput, SourcePreferenceChangeInput,
TrackerType, TrackerType,
@@ -84,13 +84,6 @@ export type AppMetadataKeys = MetadataServerSettingKeys | MangaMetadataKeys | Se
export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes]; export type MetadataKeyValuePair = [AppMetadataKeys, AllowedMetadataValueTypes];
export type TManga = GetMangaQuery['manga'];
export type TPartialManga = OptionalProperty<
TManga,
'unreadCount' | 'downloadCount' | 'bookmarkCount' | 'categories' | 'chapters'
>;
export interface INavbarOverride { export interface INavbarOverride {
status: boolean; status: boolean;
value: any; value: any;
@@ -187,7 +180,7 @@ export interface IReaderProps {
curPage: number; curPage: number;
initialPage: number; initialPage: number;
settings: IReaderSettings; settings: IReaderSettings;
manga: TManga; manga: MangaReaderFieldsFragment;
chapter: GetChaptersReaderQuery['chapters']['nodes'][number]; chapter: GetChaptersReaderQuery['chapters']['nodes'][number];
nextChapter: () => void; nextChapter: () => void;
prevChapter: () => void; prevChapter: () => void;

View File

@@ -42,7 +42,7 @@ const addImports = format(
`import {FieldPolicy, FieldReadFunction, Reference, TypePolicies, TypePolicy} from '@apollo/client/cache'; `import {FieldPolicy, FieldReadFunction, Reference, TypePolicies, TypePolicy} from '@apollo/client/cache';
import { import {
\tGetChaptersMangaQuery, GetDownloadStatusQueryVariables, GetGlobalMetadataQueryVariables, \tGetChaptersMangaQuery, GetDownloadStatusQueryVariables, GetGlobalMetadataQueryVariables,
\tGetMangaQueryVariables, GetSourceBrowseQueryVariables, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQueryVariables, \tGetMangaScreenQueryVariables, GetSourceBrowseQueryVariables, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQueryVariables,
} from "@/lib/graphql/generated/graphql.ts"; } from "@/lib/graphql/generated/graphql.ts";
import {FieldFunctionOptions} from "@apollo/client/cache/inmemory/policies";`, import {FieldFunctionOptions} from "@apollo/client/cache/inmemory/policies";`,
); );
@@ -93,7 +93,7 @@ const fixTypingOfQueryTypePolicies = format(
\textensions?: FieldPolicy<any> | FieldReadFunction<any>, \textensions?: FieldPolicy<any> | FieldReadFunction<any>,
\tgetWebUIUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>>, \tgetWebUIUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>>,
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>, \tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
\tmanga?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetMangaQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetMangaQueryVariables>>, \tmanga?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>>,
\tmangas?: FieldPolicy<any> | FieldReadFunction<any>, \tmangas?: FieldPolicy<any> | FieldReadFunction<any>,
\tmeta?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>>, \tmeta?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetGlobalMetadataQueryVariables>>,
\tmetas?: FieldPolicy<any> | FieldReadFunction<any>, \tmetas?: FieldPolicy<any> | FieldReadFunction<any>,