There is a possibility that the source changed the manga title after the track record was bound. In that case it is possible that the track record won't be found anymore when executing the search. Thus, instead of using the manga title as the search string, the existing track records title should be used instead to make sure that the bound track record will be included in the search results.
256 lines
11 KiB
TypeScript
256 lines
11 KiB
TypeScript
/*
|
|
* 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 Button from '@mui/material/Button';
|
|
import List from '@mui/material/List';
|
|
import Stack from '@mui/material/Stack';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import ArrowBack from '@mui/icons-material/ArrowBack';
|
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
|
import DialogContent from '@mui/material/DialogContent';
|
|
import DialogTitle from '@mui/material/DialogTitle';
|
|
import InputAdornment from '@mui/material/InputAdornment';
|
|
import InfoIcon from '@mui/icons-material/Info';
|
|
import PopupState, { bindPopover, bindTrigger } from 'material-ui-popup-state';
|
|
import Popover from '@mui/material/Popover';
|
|
import Typography from '@mui/material/Typography';
|
|
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
|
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
|
|
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
|
|
import { SearchTextField } from '@/base/components/inputs/SearchTextField.tsx';
|
|
import { makeToast } from '@/base/utils/Toast.ts';
|
|
import { TrackerMangaCard } from '@/features/tracker/components/cards/TrackerMangaCard.tsx';
|
|
import { DIALOG_PADDING } from '@/features/tracker/Tracker.constants.ts';
|
|
import { useGetOptionForDirection } from '@/features/theme/services/ThemeCreator.ts';
|
|
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
|
import type { MangaType } from '@/lib/graphql/generated/graphql.ts';
|
|
import type { MangaIdInfo } from '@/features/manga/Manga.types.ts';
|
|
|
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
|
import { applyStyles } from '@/base/utils/ApplyStyles.ts';
|
|
import type { TrackerIdInfo, TTrackerBind } from '@/features/tracker/Tracker.types.ts';
|
|
import { Tracker } from '@/features/tracker/Tracker.types.ts';
|
|
import { CustomButtonIcon } from '@/base/components/buttons/CustomButtonIcon.tsx';
|
|
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
|
import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
|
|
|
|
const TrackButton = ({
|
|
mangaId,
|
|
selectedTrackerRemoteId,
|
|
trackerId,
|
|
closeSearchMode,
|
|
supportsPrivateTracking,
|
|
}: {
|
|
trackerId: TrackerIdInfo['id'];
|
|
mangaId: MangaIdInfo['id'];
|
|
selectedTrackerRemoteId: string | undefined;
|
|
closeSearchMode: () => void;
|
|
supportsPrivateTracking: boolean;
|
|
}) => {
|
|
const { t } = useLingui();
|
|
const [bindTracker, bindTrackerMutation] = requestManager.useBindTracker();
|
|
|
|
const trackManga = (asPrivate: boolean) => {
|
|
if (selectedTrackerRemoteId === undefined) {
|
|
return;
|
|
}
|
|
|
|
bindTracker({
|
|
variables: { input: { mangaId, remoteId: selectedTrackerRemoteId, trackerId, private: asPrivate } },
|
|
})
|
|
.then(() => {
|
|
makeToast(t`Tracked manga`, 'success');
|
|
closeSearchMode();
|
|
})
|
|
.catch((e) => makeToast(t`Could not track manga`, 'error', getErrorMessage(e)));
|
|
};
|
|
|
|
return (
|
|
<Stack
|
|
direction="row"
|
|
sx={{
|
|
justifyContent: 'center',
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
paddingBottom: DIALOG_PADDING,
|
|
gap: 2,
|
|
px: DIALOG_PADDING,
|
|
}}
|
|
>
|
|
<Button
|
|
disabled={bindTrackerMutation.loading}
|
|
size="large"
|
|
variant="contained"
|
|
onClick={() => trackManga(false)}
|
|
sx={{ flexBasis: '65%' }}
|
|
>
|
|
{t`Track`}
|
|
</Button>
|
|
{supportsPrivateTracking && (
|
|
<CustomTooltip title={t`Track privately`} disabled={bindTrackerMutation.loading}>
|
|
<CustomButtonIcon
|
|
disabled={bindTrackerMutation.loading}
|
|
sx={{ flexBasis: '10%', maxWidth: '100px' }}
|
|
variant="contained"
|
|
onClick={() => trackManga(true)}
|
|
>
|
|
<VisibilityOffIcon />
|
|
</CustomButtonIcon>
|
|
</CustomTooltip>
|
|
)}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export const TrackerSearch = ({
|
|
manga,
|
|
tracker,
|
|
closeSearchMode,
|
|
trackedId,
|
|
trackedTitle,
|
|
}: {
|
|
manga: MangaIdInfo & Pick<MangaType, 'title'>;
|
|
tracker: TTrackerBind;
|
|
closeSearchMode: () => void;
|
|
trackedId?: string;
|
|
trackedTitle?: string;
|
|
}) => {
|
|
const { t } = useLingui();
|
|
const getOptionForDirection = useGetOptionForDirection();
|
|
|
|
const [searchString, setSearchString] = useState<string>(trackedTitle ?? manga.title);
|
|
const [tmpSearchString, setTmpSearchString] = useState(searchString);
|
|
|
|
const [selectedTrackerRemoteId, setSelectedTrackerRemoteId] = useState<string | undefined>(trackedId);
|
|
|
|
const trackerSearch = requestManager.useTrackerSearch(tracker.id, searchString);
|
|
const searchResults = trackerSearch.data?.searchTracker.trackSearches ?? STABLE_EMPTY_ARRAY;
|
|
|
|
const hasResults = !!searchResults.length;
|
|
const hasNoResults = !trackerSearch.loading && !trackerSearch.error && !hasResults;
|
|
const hasError = !!trackerSearch.error && !trackerSearch.loading;
|
|
|
|
useEffect(() => {
|
|
setSelectedTrackerRemoteId(trackedId);
|
|
|
|
return () =>
|
|
trackerSearch.abortRequest(new Error(`MangaTrackerSearchCard(${tracker.id}, ${manga.id}): search changed`));
|
|
}, [searchString]);
|
|
|
|
const showTrackButton =
|
|
useMemo(
|
|
() =>
|
|
!!selectedTrackerRemoteId &&
|
|
!!searchResults.find((searchResult) => searchResult.remoteId === selectedTrackerRemoteId),
|
|
[selectedTrackerRemoteId, searchResults],
|
|
) && !hasError;
|
|
|
|
return (
|
|
<>
|
|
<DialogTitle sx={{ padding: DIALOG_PADDING }}>
|
|
<Stack
|
|
direction="row"
|
|
sx={{
|
|
gap: '10px',
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<IconButton onClick={closeSearchMode}>
|
|
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
|
|
</IconButton>
|
|
<SearchTextField
|
|
sx={{ width: '100%' }}
|
|
variant="standard"
|
|
value={tmpSearchString}
|
|
onChange={(e) => setTmpSearchString(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
setSearchString(tmpSearchString);
|
|
}
|
|
}}
|
|
onCancel={() => setTmpSearchString('')}
|
|
slotProps={{
|
|
input: {
|
|
startAdornment: tracker.id === Tracker.MYANIMELIST && (
|
|
<InputAdornment position="start">
|
|
<PopupState variant="popover" popupId="tracker-search-info">
|
|
{(popupState) => (
|
|
<>
|
|
<IconButton {...bindTrigger(popupState)} color="inherit">
|
|
<InfoIcon />
|
|
</IconButton>
|
|
<Popover
|
|
{...bindPopover(popupState)}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'left',
|
|
}}
|
|
>
|
|
<Typography sx={{ padding: 1, whiteSpace: 'pre-line' }}>
|
|
{t`Search for a ID via "id:<ID>" (e.g. "id:13")\nLimit search to your lists via "my:<Title>" (e.g. "my:One Piece")`}
|
|
</Typography>
|
|
</Popover>
|
|
</>
|
|
)}
|
|
</PopupState>
|
|
</InputAdornment>
|
|
),
|
|
},
|
|
}}
|
|
/>
|
|
</Stack>
|
|
</DialogTitle>
|
|
<DialogContent
|
|
dividers
|
|
sx={{
|
|
padding: DIALOG_PADDING,
|
|
height: '100vh',
|
|
...applyStyles(hasNoResults || hasError, { position: 'relative' }),
|
|
}}
|
|
>
|
|
{hasNoResults && <EmptyViewAbsoluteCentered message={t`No manga found`} />}
|
|
{trackerSearch.loading && <LoadingPlaceholder />}
|
|
{hasError && (
|
|
<EmptyViewAbsoluteCentered
|
|
message={t`Unable to load data`}
|
|
messageExtra={getErrorMessage(trackerSearch.error)}
|
|
retry={() =>
|
|
trackerSearch.refetch().catch(defaultPromiseErrorHandler('TrackerSearch::refetch'))
|
|
}
|
|
/>
|
|
)}
|
|
<List sx={{ padding: 0 }}>
|
|
{hasResults &&
|
|
searchResults.map((trackerManga) => (
|
|
<TrackerMangaCard
|
|
key={trackerManga.id}
|
|
manga={trackerManga}
|
|
selected={trackerManga.remoteId === selectedTrackerRemoteId}
|
|
onSelect={() => setSelectedTrackerRemoteId(trackerManga.remoteId)}
|
|
/>
|
|
))}
|
|
</List>
|
|
{showTrackButton && (
|
|
<TrackButton
|
|
mangaId={manga.id}
|
|
trackerId={tracker.id}
|
|
closeSearchMode={closeSearchMode}
|
|
selectedTrackerRemoteId={selectedTrackerRemoteId}
|
|
supportsPrivateTracking={tracker.supportsPrivateTracking}
|
|
/>
|
|
)}
|
|
</DialogContent>
|
|
</>
|
|
);
|
|
};
|