Reduce requested manga data in queries
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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' }}
|
||||
>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
17
src/components/source/BaseMangaGrid.tsx
Normal file
17
src/components/source/BaseMangaGrid.tsx
Normal 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']} />;
|
||||
}
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user