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',
);
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 isLatestChapterRead = chapters?.totalCount === latestReadChapter?.sourceOrder;

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,10 +8,11 @@
import { StringParam, useQueryParam } from 'use-query-params';
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 { 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 = (
triState: NullAndUndefined<boolean>,
@@ -35,22 +36,26 @@ const triStateFilterNumber = (triState: NullAndUndefined<boolean>, count?: numbe
() => 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;
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;
const queries = query.split(',').map((str) => str.toLowerCase().trim());
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)
.map(([trackFilterId, trackFilterState]) => {
const mangaTrackers = Trackers.getTrackers(manga.trackRecords.nodes);
const isTrackerBound = mangaTrackers.some((tracker) => tracker.id === Number(trackFilterId));
const isTrackerBound = manga.trackRecords.nodes.some(
(trackRecord) => trackRecord.trackerId === Number(trackFilterId),
);
return triStateFilter(
trackFilterState,
@@ -60,15 +65,19 @@ const trackerFilter = (trackFilters: LibraryOptions['tracker'], manga: TManga):
})
.every((matchesFilter) => matchesFilter);
const filterManga = (
mangas: TManga[],
type TMangaFilter = TMangaQueryFilter &
TMangaQueryGenreFilter &
TMangaTrackerFilter &
Pick<MangaType, 'downloadCount' | 'unreadCount' | 'bookmarkCount'>;
const filterManga = <Manga extends TMangaFilter>(
mangas: Manga[],
query: NullAndUndefined<string>,
unread: NullAndUndefined<boolean>,
downloaded: NullAndUndefined<boolean>,
bookmarked: NullAndUndefined<boolean>,
tracker: LibraryOptions['tracker'],
ignoreFilters: boolean,
): TManga[] =>
): Manga[] =>
mangas.filter((manga) => {
const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
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 sortManga = (
manga: TManga[],
type TMangaSort = Pick<MangaType, 'title' | 'inLibraryAt' | 'unreadCount'> & {
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>,
desc: NullAndUndefined<boolean>,
): TManga[] => {
): Manga[] => {
const result = [...manga];
switch (sort) {
@@ -125,7 +139,12 @@ const sortManga = (
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 { options } = useLibraryOptionsContext();
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 SyncIcon from '@mui/icons-material/Sync';
import Dialog from '@mui/material/Dialog';
import { TManga } from '@/typings.ts';
import { actionToTranslationKey, MangaAction, MangaDownloadInfo, Mangas, MangaUnreadInfo } from '@/lib/data/Mangas.ts';
import {
actionToTranslationKey,
MangaAction,
MangaDownloadInfo,
MangaIdInfo,
Mangas,
MangaUnreadInfo,
} from '@/lib/data/Mangas.ts';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
import { MenuItem } from '@/components/menu/MenuItem.tsx';
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 { ChaptersDownloadActionMenuItems } from '@/components/chapter/ChaptersDownloadActionMenuItems.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;
type BaseProps = { onClose: (selectionModeState: boolean) => void; setHideMenu: (hide: boolean) => void };
export type SingleModeProps = {
manga: Pick<TManga, 'id' | 'title' | 'source' | 'trackRecords'> & MangaDownloadInfo & MangaUnreadInfo;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
manga: Pick<MangaType, 'id' | 'title' | 'sourceId'> & MangaDownloadInfo & MangaUnreadInfo;
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
};
type SelectModeProps = {
selectedMangas: TManga[];
selectedMangas: MangaChapterStatFieldsFragment[];
};
type Props =
@@ -82,7 +89,7 @@ export const MangaActionMenuItems = ({
onClose(true);
};
const performAction = (action: MangaAction, mangas: TManga[]) => {
const performAction = (action: MangaAction, mangas: MangaIdInfo[]) => {
Mangas.performAction(action, manga ? [manga.id] : Mangas.getIds(mangas), {
wasManuallyMarkedAsRead: true,
}).catch(defaultPromiseErrorHandler(`MangaActionMenuItems:performAction(${action})`));
@@ -150,7 +157,7 @@ export const MangaActionMenuItems = ({
)}
{isSingleMode && (
<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 }}
style={{ textDecoration: 'none', color: 'inherit' }}
>

View File

@@ -8,24 +8,37 @@
import { LongPressPointerHandlers, LongPressResult } from 'use-long-press/lib/use-long-press.types';
import { PopupState } from 'material-ui-popup-state/hooks';
import { TManga, TPartialManga } from '@/typings.ts';
import { GridLayout } from '@/components/context/LibraryOptionsContext.tsx';
import { SelectableCollectionReturnType } from '@/components/collection/useSelectableCollection.ts';
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';
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 {
manga: TPartialManga;
manga: MangaCardBaseProps;
gridLayout?: GridLayout;
inLibraryIndicator?: boolean;
selected?: boolean | null;
handleSelection?: SelectableCollectionReturnType<TManga['id']>['handleSelection'];
handleSelection?: SelectableCollectionReturnType<MangaType['id']>['handleSelection'];
mode?: MangaCardMode;
}
export type SpecificMangaCardProps = MangaCardProps &
export type SpecificMangaCardProps = Omit<MangaCardProps, 'manga'> &
Pick<ReturnType<typeof useManageMangaLibraryState>, 'isInLibrary'> & {
manga: MangaCardSpecificProps;
longPressBind: LongPressResult<LongPressPointerHandlers>;
popupState: PopupState;
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 PublicIcon from '@mui/icons-material/Public';
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 { t as translate } from 'i18next';
import Link from '@mui/material/Link';
import Typography from '@mui/material/Typography';
import { useLongPress } from 'use-long-press';
import { TManga } from '@/typings';
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 { CustomIconButton } from '@/components/atoms/CustomIconButton';
import { TrackMangaButton } from '@/components/manga/TrackMangaButton.tsx';
import { useManageMangaLibraryState } from '@/components/manga/useManageMangaLibraryState.tsx';
import { Metadata as BaseMetadata } from '@/components/atoms/Metadata.tsx';
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 }) => ({
width: '100%',
@@ -156,10 +155,6 @@ const OpenSourceButton = ({ url }: { url?: string | null }) => {
return button;
};
interface IProps {
manga: TManga;
}
function getSourceName(source?: Pick<SourceType, 'id' | 'displayName'> | null): string {
if (!source) {
return translate('global.label.unknown');
@@ -172,7 +167,18 @@ function getValueOrUnknown(val?: string | null) {
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();
useEffect(() => {

View File

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

View File

@@ -21,11 +21,11 @@ import { Link } from 'react-router-dom';
import SyncAltIcon from '@mui/icons-material/SyncAlt';
import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';
import { TManga } from '@/typings.ts';
import { useCategorySelect } from '@/components/navbar/action/useCategorySelect.tsx';
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
interface IProps {
manga: TManga;
manga: Pick<MangaType, 'id' | 'inLibrary' | 'sourceId' | 'title'>;
onRefresh: () => any;
refreshing: boolean;
}
@@ -64,7 +64,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<>
<Tooltip title={t('global.button.migrate')}>
<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 }}
style={{ textDecoration: 'none', color: 'inherit' }}
>
@@ -122,7 +122,7 @@ export const MangaToolbarMenu = ({ manga, onRefresh, refreshing }: IProps) => {
<MenuItem
key="migrate"
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 }}
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 { TrackManga } from '@/components/tracker/TrackManga.tsx';
import { Trackers } from '@/lib/data/Trackers.ts';
import { TManga } from '@/typings.ts';
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 { 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 navigate = useNavigate();
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
const trackers = trackerList.data?.trackers.nodes ?? [];
const mangaTrackers = manga.trackRecords.nodes;
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers));
const loggedInTrackers = Trackers.getLoggedIn(trackers);
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers, trackers));
const handleClick = (openPopup: () => void) => {
if (trackerList.error) {

View File

@@ -16,19 +16,18 @@ import { getMetadataServerSettings } from '@/lib/metadata/metadataServerSettings
import { Categories } from '@/lib/data/Categories.ts';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
import { Mangas } from '@/lib/data/Mangas.ts';
import { TManga } from '@/typings.ts';
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';
export const useManageMangaLibraryState = (
manga: Pick<TManga, 'id' | 'title' | 'inLibrary'>,
manga: Pick<MangaType, 'id' | 'title'> & Partial<Pick<MangaType, 'inLibrary'>>,
confirmRemoval: boolean = false,
) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [isInLibrary, setIsInLibrary] = useState(manga.inLibrary);
const [isInLibrary, setIsInLibrary] = useState(!!manga.inLibrary);
const addToLibrary = useCallback(
(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
*/
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 Collapse from '@mui/material/Collapse';
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 { useBackButton } from '@/util/useBackButton.ts';
import { Select } from '@/components/atoms/Select.tsx';
import { getOptionForDirection } from '@/theme.ts';
import { ChapterType } from '@/lib/graphql/generated/graphql.ts';
import { MangaChapterCountInfo, MangaIdInfo } from '@/lib/data/Mangas.ts';
const Root = styled('div')({
zIndex: 10,
@@ -123,7 +124,7 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
interface IProps {
settings: IReaderSettings;
setSettingValue: (key: keyof IReaderSettings, value: AllowedMetadataValueTypes, persist?: boolean) => void;
manga: TManga;
manga: MangaIdInfo & MangaChapterCountInfo;
chapter: Pick<ChapterType, 'name' | 'sourceOrder' | 'pageCount'>;
chapters: Pick<ChapterType, 'id' | 'sourceOrder' | 'name' | 'chapterNumber' | 'scanlator'>[];
curPage: number;

View File

@@ -25,8 +25,14 @@ import { CheckboxInput } from '@/components/atoms/CheckboxInput.tsx';
import { makeToast } from '@/components/util/Toast.tsx';
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.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_MANGA_CATEGORIES } from '@/lib/graphql/queries/MangaQuery.ts';
type BaseProps = {
open: boolean;
@@ -47,7 +53,11 @@ export type CategorySelectProps =
| (BaseProps & PropertiesNever<SingleMangaModeProps> & MultiMangaModeProps);
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(() => {
if (mangaId === undefined || !mangaResult) {
@@ -90,6 +100,7 @@ export function CategorySelect(props: CategorySelectProps) {
const [doNotShowAddToLibraryDialogAgain, setDoNotShowAddToLibraryDialogAgain] = useState(false);
const mangaCategoryIds = useGetMangaCategoryIds(mangaId);
const { data } = requestManager.useGetCategories<GetCategoriesBaseQuery, GetCategoriesBaseQueryVariables>(
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 { Trackers } from '@/lib/data/Trackers.ts';
import { TrackerCard, TrackerMode } from '@/components/tracker/TrackerCard.tsx';
import { TManga } from '@/typings.ts';
import { makeToast } from '@/components/util/Toast.tsx';
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 { 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 => {
if (id === searchModeForTracker) {
@@ -34,7 +35,7 @@ const getTrackerMode = (id: number, trackersInUse: number[], searchModeForTracke
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 navigate = useNavigate();
@@ -43,26 +44,32 @@ export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords
const trackerList = requestManager.useGetTrackerList<GetTrackersBindQuery>(GET_TRACKERS_BIND, {
notifyOnNetworkStatusChange: true,
});
const mangaTrackers = manga.trackRecords.nodes;
const trackers = trackerList.data?.trackers.nodes ?? [];
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
const trackersInUse = Trackers.getLoggedIn(Trackers.getTrackers(mangaTrackers));
const mangaTrackRecordsList = requestManager.useGetManga<GetMangaTrackRecordsQuery>(
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 isSearchActive = searchModeForTracker !== undefined;
const OptionalDialogContent = useMemo(() => (isSearchActive ? Box : DialogContent), [isSearchActive]);
useEffect(() => {
Promise.all(manga.trackRecords.nodes.map((trackRecord) => requestManager.fetchTrackBind(trackRecord.id))).catch(
() => makeToast(t('tracking.error.label.could_not_fetch_track_info'), 'error'),
Promise.all(mangaTrackRecords.map((trackRecord) => requestManager.fetchTrackBind(trackRecord.id))).catch(() =>
makeToast(t('tracking.error.label.could_not_fetch_track_info'), 'error'),
);
}, [manga.id]);
}, [mangaTrackRecords]);
const trackerComponents = useMemo(
() =>
loggedInTrackers.map((tracker) => {
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;
if (isSearchActive && !isSearchForTracker) {
@@ -73,27 +80,39 @@ export const TrackManga = ({ manga }: { manga: Pick<TManga, 'id' | 'trackRecords
<TrackerCard
key={tracker.id}
tracker={tracker}
mangaId={manga.id}
manga={manga}
trackRecord={trackRecord}
mode={mode}
setSearchMode={(id) => setSearchModeForTracker(id)}
/>
);
}),
[trackersInUseIds, searchModeForTracker, manga.id],
[trackersInUseIds, searchModeForTracker, mangaTrackRecords],
);
if (trackerList.error) {
const error = trackerList.error ?? mangaTrackRecordsList.error;
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={trackerList.error.message}
retry={() => trackerList.refetch().catch(defaultPromiseErrorHandler('TrackManga::refetch'))}
messageExtra={error.message}
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 />;
}

View File

@@ -10,6 +10,8 @@ import { TrackerUntrackedCard } from '@/components/tracker/TrackerUntrackedCard.
import { TrackerSearch } from '@/components/tracker/TrackerSearch.tsx';
import { TrackerActiveCard } from '@/components/tracker/TrackerActiveCard.tsx';
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 {
UNTRACKED,
@@ -19,13 +21,13 @@ export enum TrackerMode {
export const TrackerCard = ({
tracker,
mangaId,
manga,
trackRecord,
mode,
setSearchMode,
}: {
tracker: TTrackerBind;
mangaId: number;
manga: MangaIdInfo & Pick<MangaType, 'title'>;
trackRecord?: TTrackRecordBind;
mode: TrackerMode;
setSearchMode: (id?: number) => void;
@@ -37,7 +39,7 @@ export const TrackerCard = ({
if (mode === TrackerMode.SEARCH) {
return (
<TrackerSearch
mangaId={mangaId}
manga={manga}
tracker={tracker}
trackedId={trackRecord?.remoteId}
closeSearchMode={() => setSearchMode(undefined)}
@@ -46,7 +48,7 @@ export const TrackerCard = ({
}
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 (

View File

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