Files
suwayomi-material-you-webui/src/modules/tracker/components/TrackerSearch.tsx

202 lines
9.0 KiB
TypeScript
Raw Normal View History

2024-03-10 17:36:22 +01:00
/*
* 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 { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import Stack from '@mui/material/Stack';
2024-03-10 17:36:22 +01:00
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';
2024-03-10 17:36:22 +01:00
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
2024-05-07 14:42:02 +02:00
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 { requestManager } from '@/lib/requests/RequestManager.ts';
2024-10-05 16:09:34 +02:00
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx';
import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx';
2024-10-05 21:32:31 +02:00
import { Tracker, TTrackerBase } from '@/modules/tracker/services/Trackers.ts';
2024-10-05 16:09:34 +02:00
import { SearchTextField } from '@/modules/core/components/inputs/SearchTextField.tsx';
2024-10-05 23:22:42 +02:00
import { makeToast } from '@/modules/core/utils/Toast.ts';
2024-10-05 21:32:31 +02:00
import { TrackerMangaCard } from '@/modules/tracker/components/cards/TrackerMangaCard.tsx';
import { DIALOG_PADDING } from '@/modules/tracker/Tracker.constants.ts';
import { useGetOptionForDirection } from '@/modules/theme/services/ThemeCreator.ts';
2024-10-05 16:09:34 +02:00
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
2024-07-08 18:02:50 +02:00
import { MangaType } from '@/lib/graphql/generated/graphql.ts';
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts';
2024-12-20 17:24:40 +01:00
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
2024-03-10 17:36:22 +01:00
export const TrackerSearch = ({
2024-07-08 18:02:50 +02:00
manga,
2024-03-10 17:36:22 +01:00
tracker,
closeSearchMode,
trackedId,
}: {
2024-07-08 18:02:50 +02:00
manga: MangaIdInfo & Pick<MangaType, 'title'>;
tracker: Pick<TTrackerBase, 'id'>;
2024-03-10 17:36:22 +01:00
closeSearchMode: () => void;
trackedId?: string;
}) => {
const { t } = useTranslation();
const getOptionForDirection = useGetOptionForDirection();
2024-03-10 17:36:22 +01:00
2024-07-08 18:02:50 +02:00
const [searchString, setSearchString] = useState<string>(manga.title);
2024-03-10 17:36:22 +01:00
const [tmpSearchString, setTmpSearchString] = useState(searchString);
const [selectedTrackerRemoteId, setSelectedTrackerRemoteId] = useState<string | undefined>(trackedId);
const trackerSearch = requestManager.useTrackerSearch(tracker.id, searchString, {
notifyOnNetworkStatusChange: true,
});
2024-03-10 17:36:22 +01:00
const searchResults = trackerSearch.data?.searchTracker.trackSearches ?? [];
const hasResults = !!searchResults.length;
useEffect(() => {
setSelectedTrackerRemoteId(trackedId);
return () =>
2024-07-08 18:02:50 +02:00
trackerSearch.abortRequest(new Error(`MangaTrackerSearchCard(${tracker.id}, ${manga.id}): search changed`));
2024-03-10 17:36:22 +01:00
}, [searchString]);
const [bindTracker, bindTrackerMutation] = requestManager.useBindTracker();
const showTrackButton =
useMemo(
() =>
!!selectedTrackerRemoteId &&
!!searchResults.find((searchResult) => searchResult.remoteId === selectedTrackerRemoteId),
[selectedTrackerRemoteId, searchResults],
) &&
!trackerSearch.loading &&
!trackerSearch.error;
const trackManga = () => {
if (selectedTrackerRemoteId === undefined) {
return;
}
2024-07-08 18:02:50 +02:00
bindTracker({ variables: { mangaId: manga.id, remoteId: selectedTrackerRemoteId, trackerId: tracker.id } })
2024-03-10 17:36:22 +01:00
.then(() => {
makeToast(t('manga.action.track.add.label.success'), 'success');
closeSearchMode();
})
2024-12-20 17:24:40 +01:00
.catch((e) => makeToast(t('manga.action.track.add.label.error'), 'error', getErrorMessage(e)));
2024-03-10 17:36:22 +01:00
};
return (
<>
<DialogTitle sx={{ padding: DIALOG_PADDING }}>
2024-09-03 17:15:47 +02:00
<Stack
direction="row"
sx={{
gap: '10px',
alignItems: 'center',
}}
>
2024-03-10 17:36:22 +01:00
<IconButton onClick={closeSearchMode}>
{getOptionForDirection(<ArrowBack />, <ArrowForwardIcon />)}
2024-03-10 17:36:22 +01:00
</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('')}
2024-05-07 14:42:02 +02:00
InputProps={{
startAdornment: tracker.id === Tracker.MYANIMELIST && (
<InputAdornment position="start">
<PopupState variant="popover" popupId="tracker-search-info">
{(popupState) => (
<>
2024-09-06 16:59:27 +02:00
<IconButton {...bindTrigger(popupState)} color="inherit">
2024-05-07 14:42:02 +02:00
<InfoIcon />
</IconButton>
<Popover
{...bindPopover(popupState)}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Typography sx={{ padding: 1, whiteSpace: 'pre-line' }}>
{t('tracking.my_anime_list.search.label.hint')}
</Typography>
</Popover>
</>
)}
</PopupState>
</InputAdornment>
),
}}
2024-03-10 17:36:22 +01:00
/>
</Stack>
</DialogTitle>
<DialogContent dividers sx={{ padding: DIALOG_PADDING, height: '100vh' }}>
2024-03-10 17:36:22 +01:00
{!trackerSearch.loading && !trackerSearch.error && !hasResults && (
2024-04-29 15:29:33 +02:00
<EmptyViewAbsoluteCentered message={t('manga.error.label.no_mangas_found')} />
2024-03-10 17:36:22 +01:00
)}
{trackerSearch.loading && <LoadingPlaceholder />}
{trackerSearch.error && !trackerSearch.loading && (
2024-04-29 15:29:33 +02:00
<EmptyViewAbsoluteCentered
2024-03-10 17:36:22 +01:00
message={t('global.error.label.failed_to_load_data')}
messageExtra={trackerSearch.error.message}
retry={() =>
trackerSearch.refetch().catch(defaultPromiseErrorHandler('TrackerSearch::refetch'))
}
2024-03-10 17:36:22 +01:00
/>
)}
<List sx={{ padding: 0 }}>
{hasResults &&
searchResults.map((trackerManga) => (
<TrackerMangaCard
key={trackerManga.id}
manga={trackerManga}
selected={trackerManga.remoteId === selectedTrackerRemoteId}
onSelect={() => setSelectedTrackerRemoteId(trackerManga.remoteId)}
/>
))}
</List>
{showTrackButton && (
<Stack
direction="row"
sx={{
2024-09-03 17:15:47 +02:00
justifyContent: 'center',
2024-03-10 17:36:22 +01:00
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingBottom: DIALOG_PADDING,
2024-03-10 17:36:22 +01:00
}}
>
<Button
disabled={bindTrackerMutation.loading}
size="large"
variant="contained"
onClick={trackManga}
sx={{ width: '75%' }}
>
{t('manga.action.track.add.label.action')}
</Button>
</Stack>
)}
</DialogContent>
</>
);
};