/* * 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 ( {supportsPrivateTracking && ( trackManga(true)} > )} ); }; export const TrackerSearch = ({ manga, tracker, closeSearchMode, trackedId, trackedTitle, }: { manga: MangaIdInfo & Pick; tracker: TTrackerBind; closeSearchMode: () => void; trackedId?: string; trackedTitle?: string; }) => { const { t } = useLingui(); const getOptionForDirection = useGetOptionForDirection(); const [searchString, setSearchString] = useState(trackedTitle ?? manga.title); const [tmpSearchString, setTmpSearchString] = useState(searchString); const [selectedTrackerRemoteId, setSelectedTrackerRemoteId] = useState(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 ( <> {getOptionForDirection(, )} setTmpSearchString(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { setSearchString(tmpSearchString); } }} onCancel={() => setTmpSearchString('')} slotProps={{ input: { startAdornment: tracker.id === Tracker.MYANIMELIST && ( {(popupState) => ( <> {t`Search for a ID via "id:" (e.g. "id:13")\nLimit search to your lists via "my:" (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> </> ); };