diff --git a/.eslintrc.json b/.eslintrc.json index b3d82d53..7d13849a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -106,6 +106,7 @@ ], "paths": [ { "name": "react-router", "importNames": ["useNavigate"], "message": "Please use @/hooks/useNavigateVoid instead." }, + { "name": "react-toastify", "importNames": ["toast"], "message": "Please use @/components/Toast instead." }, { "name": "usehooks-ts", "importNames": ["useEventCallback"], "message": "Please use @/hooks/useEventCallback instead." } ] } diff --git a/src/components/Collection/Series/EditSeriesTabs/Action.tsx b/src/components/Collection/Series/EditSeriesTabs/Action.tsx index 94405afd..caa31629 100644 --- a/src/components/Collection/Series/EditSeriesTabs/Action.tsx +++ b/src/components/Collection/Series/EditSeriesTabs/Action.tsx @@ -20,4 +20,4 @@ const Action = ( ); -export default Action; +export default React.memo(Action); diff --git a/src/components/Collection/Series/EditSeriesTabs/FileActionsTab.tsx b/src/components/Collection/Series/EditSeriesTabs/FileActionsTab.tsx index cd87d1cf..97367e00 100644 --- a/src/components/Collection/Series/EditSeriesTabs/FileActionsTab.tsx +++ b/src/components/Collection/Series/EditSeriesTabs/FileActionsTab.tsx @@ -1,7 +1,6 @@ import React from 'react'; import Action from '@/components/Collection/Series/EditSeriesTabs/Action'; -import toast from '@/components/Toast'; import { useRehashSeriesFilesMutation, useRescanSeriesFilesMutation } from '@/core/react-query/series/mutations'; type Props = { @@ -9,26 +8,20 @@ type Props = { }; const FileActionsTab = ({ seriesId }: Props) => { - const { mutate: rehashSeriesFiles } = useRehashSeriesFilesMutation(); - const { mutate: rescanSeriesFiles } = useRescanSeriesFilesMutation(); + const { mutate: rehashSeriesFiles } = useRehashSeriesFilesMutation(seriesId); + const { mutate: rescanSeriesFiles } = useRescanSeriesFilesMutation(seriesId); return (
- rescanSeriesFiles(seriesId, { - onSuccess: () => toast.success('Series files rescan queued!'), - })} + onClick={rescanSeriesFiles} /> - rehashSeriesFiles(seriesId, { - onSuccess: () => toast.success('Series files rehash queued!'), - })} + onClick={rehashSeriesFiles} />
); diff --git a/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx b/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx index f33441ad..8c97b99e 100644 --- a/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx +++ b/src/components/Collection/Series/EditSeriesTabs/NameTab.tsx @@ -4,7 +4,6 @@ import cx from 'classnames'; import { useToggle } from 'usehooks-ts'; import Input from '@/components/Input/Input'; -import toast from '@/components/Toast'; import { useOverrideSeriesTitleMutation } from '@/core/react-query/series/mutations'; import { useSeriesQuery } from '@/core/react-query/series/queries'; @@ -18,7 +17,7 @@ const NameTab = ({ seriesId }: Props) => { const { data: seriesData, isError, isFetching, isSuccess } = useSeriesQuery(seriesId, { includeDataFrom: ['AniDB'] }); - const { mutate: overrideTitle } = useOverrideSeriesTitleMutation(); + const { mutate: overrideTitle } = useOverrideSeriesTitleMutation(seriesId); useEffect(() => { setName(seriesData?.Name ?? ''); @@ -50,12 +49,8 @@ const NameTab = ({ seriesId }: Props) => { icon: mdiCheckUnderlineCircleOutline, className: 'text-panel-text-primary', onClick: () => - overrideTitle({ seriesId: seriesData.IDs.ID, Title: name }, { - onSuccess: () => { - toast.success('Name updated successfully!'); - toggleNameEditable(); - }, - onError: () => toast.error('Name could not be updated!'), + overrideTitle(name, { + onSuccess: () => toggleNameEditable(), }), tooltip: 'Save name', }, @@ -66,7 +61,6 @@ const NameTab = ({ seriesId }: Props) => { name, nameEditable, overrideTitle, - seriesData?.IDs.ID, seriesData?.Name, toggleNameEditable, ]); diff --git a/src/components/Collection/Series/EditSeriesTabs/UpdateActionsTab.tsx b/src/components/Collection/Series/EditSeriesTabs/UpdateActionsTab.tsx index 445615f7..5827612f 100644 --- a/src/components/Collection/Series/EditSeriesTabs/UpdateActionsTab.tsx +++ b/src/components/Collection/Series/EditSeriesTabs/UpdateActionsTab.tsx @@ -1,30 +1,36 @@ import React from 'react'; import Action from '@/components/Collection/Series/EditSeriesTabs/Action'; -import toast from '@/components/Toast'; import { useAutoSearchTmdbMatchMutation, useRefreshSeriesAniDBInfoMutation, useRefreshSeriesTMDBInfoMutation, + useRefreshSeriesTraktInfoMutation, + useSyncSeriesTraktMutation, useUpdateSeriesTMDBImagesMutation, } from '@/core/react-query/series/mutations'; +import useEventCallback from '@/hooks/useEventCallback'; type Props = { seriesId: number; }; const UpdateActionsTab = ({ seriesId }: Props) => { - const { mutate: refreshAnidb } = useRefreshSeriesAniDBInfoMutation(); - const { mutate: autoMatchTmdb } = useAutoSearchTmdbMatchMutation(); - const { mutate: refreshTmdb } = useRefreshSeriesTMDBInfoMutation(); - const { mutate: updateTmdbImages } = useUpdateSeriesTMDBImagesMutation(); + const { mutate: refreshAnidb } = useRefreshSeriesAniDBInfoMutation(seriesId); + const { mutate: autoMatchTmdb } = useAutoSearchTmdbMatchMutation(seriesId); + const { mutate: refreshTmdb } = useRefreshSeriesTMDBInfoMutation(seriesId); + const { mutate: updateTmdbImagesMutation } = useUpdateSeriesTMDBImagesMutation(seriesId); + const { mutate: refreshTrakt } = useRefreshSeriesTraktInfoMutation(seriesId); + const { mutate: syncTrakt } = useSyncSeriesTraktMutation(seriesId); const triggerAnidbRefresh = (force: boolean, cacheOnly: boolean) => { - refreshAnidb({ seriesId, force, cacheOnly }, { - onSuccess: () => toast.success('AniDB refresh queued!'), - }); + refreshAnidb({ force, cacheOnly }); }; + const updateTmdbImagesForce = useEventCallback(() => { + updateTmdbImagesMutation({ force: true }); + }); + return (
{ - autoMatchTmdb(seriesId, { - onSuccess: () => toast.success('TMDB refresh queued!'), - })} + onClick={autoMatchTmdb} /> - refreshTmdb(seriesId, { - onSuccess: () => toast.success('TMDB refresh queued!'), - })} + onClick={refreshTmdb} /> - updateTmdbImages({ seriesId, force: true }, { - onSuccess: () => toast.success('TMDB image download queued!'), - })} + onClick={updateTmdbImagesForce} + /> + +
); diff --git a/src/components/Collection/Series/SeriesRating.tsx b/src/components/Collection/Series/SeriesRating.tsx index 469a3294..e6ea4d81 100644 --- a/src/components/Collection/Series/SeriesRating.tsx +++ b/src/components/Collection/Series/SeriesRating.tsx @@ -3,7 +3,6 @@ import { mdiStar, mdiStarOutline } from '@mdi/js'; import { Icon } from '@mdi/react'; import { toNumber } from 'lodash'; -import toast from '@/components/Toast'; import { useVoteSeriesMutation } from '@/core/react-query/series/mutations'; import useEventCallback from '@/hooks/useEventCallback'; @@ -31,15 +30,12 @@ const StarIcon = React.memo(({ handleHover, handleVote, hovered, index }: StarIc )); const SeriesRating = ({ ratingValue, seriesId }: Props) => { - const { mutate: voteSeries } = useVoteSeriesMutation(); + const { mutate: voteSeries } = useVoteSeriesMutation(seriesId); const [hoveredStar, setHoveredStar] = useState(ratingValue - 1); const handleVote = useEventCallback((event: React.MouseEvent) => { - voteSeries({ seriesId, rating: toNumber(event.currentTarget.id) + 1 }, { - onSuccess: () => toast.success('Voted!'), - onError: () => toast.error('Failed to vote!'), - }); + voteSeries(toNumber(event.currentTarget.id) + 1); }); const handleClear = useEventCallback(() => { diff --git a/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx b/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx index 1e5bc8e9..f787aa4e 100644 --- a/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx +++ b/src/components/Collection/Tags/TagsSearchAndFilterPanel.tsx @@ -5,7 +5,6 @@ import Icon from '@mdi/react'; import Checkbox from '@/components/Input/Checkbox'; import Input from '@/components/Input/Input'; import ShokoPanel from '@/components/Panels/ShokoPanel'; -import toast from '@/components/Toast'; import { useRefreshSeriesAniDBInfoMutation } from '@/core/react-query/series/mutations'; import useEventCallback from '@/hooks/useEventCallback'; @@ -22,11 +21,9 @@ type Props = { }; const TagsSearchAndFilterPanel = React.memo( ({ handleInputChange, search, seriesId, showSpoilers, sort, tagSourceFilter, toggleSort }: Props) => { - const { isPending: anidbRefreshPending, mutate: refreshAnidb } = useRefreshSeriesAniDBInfoMutation(); + const { isPending: anidbRefreshPending, mutate: refreshAnidb } = useRefreshSeriesAniDBInfoMutation(seriesId); const refreshAnidbCallback = useEventCallback(() => { - refreshAnidb({ seriesId, force: true }, { - onSuccess: () => toast.success('AniDB refresh queued!'), - }); + refreshAnidb({ force: true }); }); const searchInput = useMemo(() => ( diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index 1df3c625..e1e20251 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -1,4 +1,5 @@ import type React from 'react'; +// eslint-disable-next-line no-restricted-imports import { toast } from 'react-toastify'; import type { ToastOptions } from 'react-toastify'; import { mdiAlertCircleOutline, mdiCheckboxMarkedCircleOutline, mdiInformationOutline } from '@mdi/js'; diff --git a/src/core/react-query/series/mutations.ts b/src/core/react-query/series/mutations.ts index ab0e1e49..a5e05119 100644 --- a/src/core/react-query/series/mutations.ts +++ b/src/core/react-query/series/mutations.ts @@ -1,20 +1,21 @@ import { useMutation } from '@tanstack/react-query'; +import toast from '@/components/Toast'; import { axios } from '@/core/axios'; import { invalidateQueries } from '@/core/react-query/queryClient'; import type { - ChangeSeriesImageRequestType, DeleteSeriesRequestType, RefreshAniDBSeriesRequestType, RefreshSeriesAniDBInfoRequestType, WatchSeriesEpisodesRequestType, } from '@/core/react-query/series/types'; +import type { ImageType } from '@/core/types/api/common'; import type { SeriesAniDBSearchResult } from '@/core/types/api/series'; -export const useChangeSeriesImageMutation = () => +export const useChangeSeriesImageMutation = (seriesId: number) => useMutation({ - mutationFn: ({ image, seriesId }: ChangeSeriesImageRequestType) => + mutationFn: (image: ImageType) => axios.put( `Series/${seriesId}/Images/${image.Type}`, { @@ -22,7 +23,8 @@ export const useChangeSeriesImageMutation = () => Source: image.Source, }, ), - onSuccess: (_, { seriesId }) => { + onSuccess: (_, image) => { + toast.success(`Series ${image.Type} image has been changed.`); invalidateQueries(['series', seriesId, 'data']); invalidateQueries(['series', seriesId, 'images']); }, @@ -43,13 +45,14 @@ export const useGetSeriesAniDBMutation = () => mutationFn: (anidbId: number) => axios.get(`Series/AniDB/${anidbId}`), }); -export const useOverrideSeriesTitleMutation = () => +export const useOverrideSeriesTitleMutation = (seriesId: number) => useMutation({ - mutationFn: ({ seriesId, ...data }: { seriesId: number, Title: string }) => - axios.post(`Series/${seriesId}/OverrideTitle`, data), - onSuccess: (_, { seriesId }) => { + mutationFn: (Title: string) => axios.post(`Series/${seriesId}/OverrideTitle`, { Title }), + onSuccess: (_) => { + toast.success('Name updated successfully!'); invalidateQueries(['series', seriesId, 'data']); }, + onError: () => toast.error('Name could not be updated!'), }); export const useRefreshAniDBSeriesMutation = () => @@ -67,60 +70,81 @@ export const useRefreshAniDBSeriesMutation = () => }, }); -export const useRefreshSeriesAniDBInfoMutation = () => +export const useRefreshSeriesAniDBInfoMutation = (seriesId: number) => useMutation({ - mutationFn: ({ seriesId, ...params }: RefreshSeriesAniDBInfoRequestType) => + mutationFn: (params: RefreshSeriesAniDBInfoRequestType) => axios.post(`Series/${seriesId}/AniDB/Refresh`, null, { params }), + onSuccess: () => toast.success('AniDB refresh queued!'), }); -export const useRehashSeriesFilesMutation = () => +export const useRehashSeriesFilesMutation = (seriesId: number) => useMutation({ - mutationFn: (seriesId: number) => axios.post(`Series/${seriesId}/File/Rehash`), + mutationFn: () => axios.post(`Series/${seriesId}/File/Rehash`), + onSuccess: () => toast.success('Series files rehash queued!'), }); -export const useRescanSeriesFilesMutation = () => +export const useRescanSeriesFilesMutation = (seriesId: number) => useMutation({ - mutationFn: (seriesId: number) => axios.post(`Series/${seriesId}/File/Rescan`), + mutationFn: () => axios.post(`Series/${seriesId}/File/Rescan`), + onSuccess: () => toast.success('Series files rescan queued!'), }); -export const useVoteSeriesMutation = () => +export const useVoteSeriesMutation = (seriesId: number) => useMutation({ - mutationFn: ({ rating, seriesId }: { seriesId: number, rating: number }) => - axios.post(`Series/${seriesId}/Vote`, { Value: rating, MaxValue: 10 }), - onSuccess: (_, { seriesId }) => { + mutationFn: (rating: number) => axios.post(`Series/${seriesId}/Vote`, { Value: rating, MaxValue: 10 }), + onSuccess: (_) => { + toast.success('Voted!'); invalidateQueries(['series', seriesId, 'data']); }, + onError: () => toast.error('Failed to vote!'), }); -export const useWatchSeriesEpisodesMutation = () => +export const useWatchSeriesEpisodesMutation = (seriesId: number) => useMutation({ - mutationFn: ({ seriesId, ...params }: WatchSeriesEpisodesRequestType) => + mutationFn: (params: WatchSeriesEpisodesRequestType) => axios.post(`Series/${seriesId}/Episode/Watched`, null, { params }), - onSuccess: (_, { seriesId }) => { + onSuccess: (_, { value }) => { + toast.success(`Episodes marked as ${value ? 'watched' : 'unwatched'}!`); invalidateQueries(['series', seriesId, 'data']); invalidateQueries(['series', seriesId, 'episodes']); }, + onError: (_, { value }) => toast.error(`Failed to mark episodes as ${value ? 'watched' : 'unwatched'}!`), }); -export const useAutoSearchTmdbMatchMutation = () => +export const useAutoSearchTmdbMatchMutation = (seriesId: number) => useMutation({ - mutationFn: (seriesId: number) => axios.post(`Series/${seriesId}/TMDB/Action/AutoSearch`), + mutationFn: () => axios.post(`Series/${seriesId}/TMDB/Action/AutoSearch`), + onSuccess: () => toast.success('TMDB auto-search queued!'), }); -export const useRefreshSeriesTMDBInfoMutation = () => +export const useRefreshSeriesTMDBInfoMutation = (seriesId: number) => useMutation({ - mutationFn: (seriesId: number) => + mutationFn: () => Promise.all([ axios.post(`Series/${seriesId}/TMDB/Show/Action/Refresh`, {}), axios.post(`Series/${seriesId}/TMDB/Movie/Action/Refresh`, {}), ]), + onSuccess: () => toast.success('TMDB refresh queued!'), }); -export const useUpdateSeriesTMDBImagesMutation = () => +export const useUpdateSeriesTMDBImagesMutation = (seriesId: number) => useMutation({ - mutationFn: ({ force = false, seriesId }: { seriesId: number, force: boolean }) => + mutationFn: ({ force = false }: { force?: boolean }) => Promise.all([ axios.post(`Series/${seriesId}/TMDB/Show/Action/DownloadImages`, { force }), axios.post(`Series/${seriesId}/TMDB/Movie/Action/DownloadImages`, { force }), ]), + onSuccess: () => toast.success('TMDB image download queued!'), + }); + +export const useRefreshSeriesTraktInfoMutation = (seriesId: number) => + useMutation({ + mutationFn: () => axios.post(`Series/${seriesId}/Trakt/Refresh`), + onSuccess: () => toast.success('Trakt refresh queued!'), + }); + +export const useSyncSeriesTraktMutation = (seriesId: number) => + useMutation({ + mutationFn: () => axios.post(`Series/${seriesId}/Trakt/Sync`), + onSuccess: () => toast.success('Trakt sync queued!'), }); diff --git a/src/core/react-query/series/types.ts b/src/core/react-query/series/types.ts index 773694cd..35d60618 100644 --- a/src/core/react-query/series/types.ts +++ b/src/core/react-query/series/types.ts @@ -1,5 +1,5 @@ import type { PaginationType } from '@/core/types/api'; -import type { DataSourceType, ImageType } from '@/core/types/api/common'; +import type { DataSourceType } from '@/core/types/api/common'; import type { EpisodeTypeEnum } from '@/core/types/api/episode'; export enum IncludeOnlyFilterEnum { @@ -18,11 +18,6 @@ type SeriesEpisodesBaseRequestType = { fuzzy?: boolean; }; -export type ChangeSeriesImageRequestType = { - seriesId: number; - image: ImageType; -}; - export type DeleteSeriesRequestType = { seriesId: number; deleteFiles?: boolean; @@ -73,12 +68,10 @@ export type RefreshAniDBSeriesRequestType = { }; export type RefreshSeriesAniDBInfoRequestType = { - seriesId: number; force?: boolean; cacheOnly?: boolean; }; export type WatchSeriesEpisodesRequestType = { - seriesId: number; value: boolean; } & SeriesEpisodesBaseRequestType; diff --git a/src/pages/collection/series/SeriesCredits.tsx b/src/pages/collection/series/SeriesCredits.tsx index 19d4e5e8..1e130898 100644 --- a/src/pages/collection/series/SeriesCredits.tsx +++ b/src/pages/collection/series/SeriesCredits.tsx @@ -4,7 +4,6 @@ import { useOutletContext } from 'react-router'; import CreditsSearchAndFilterPanel from '@/components/Collection/Credits/CreditsSearchAndFilterPanel'; import StaffPanelVirtualizer from '@/components/Collection/Credits/CreditsStaffVirtualizer'; import MultiStateButton from '@/components/Input/MultiStateButton'; -import toast from '@/components/Toast'; import { useRefreshSeriesAniDBInfoMutation } from '@/core/react-query/series/mutations'; import { useSeriesCastQuery } from '@/core/react-query/series/queries'; import useEventCallback from '@/hooks/useEventCallback'; @@ -26,12 +25,12 @@ const modeStates: { label?: string, value: CreditsModeType }[] = [ const SeriesCredits = () => { const { series } = useOutletContext(); - const { isPending: pendingRefreshAniDb, mutate: refreshAniDbMutation } = useRefreshSeriesAniDBInfoMutation(); + const { isPending: pendingRefreshAniDb, mutate: refreshAniDbMutation } = useRefreshSeriesAniDBInfoMutation( + series.IDs.ID, + ); const refreshAniDb = useEventCallback(() => { - refreshAniDbMutation({ seriesId: series.IDs.ID, force: true }, { - onSuccess: () => toast.success('AniDB refresh queued!'), - }); + refreshAniDbMutation({ force: true }); }); const [mode, setMode] = useState(modeStates[0].value); diff --git a/src/pages/collection/series/SeriesEpisodes.tsx b/src/pages/collection/series/SeriesEpisodes.tsx index 73ffabf4..f1e19bb4 100644 --- a/src/pages/collection/series/SeriesEpisodes.tsx +++ b/src/pages/collection/series/SeriesEpisodes.tsx @@ -10,7 +10,6 @@ import EpisodeSearchAndFilterPanel from '@/components/Collection/Episode/Episode import EpisodeSummary from '@/components/Collection/Episode/EpisodeSummary'; import EpisodeWatchModal from '@/components/Collection/Episode/EpisodeWatchModal'; import Button from '@/components/Input/Button'; -import toast from '@/components/Toast'; import { useWatchSeriesEpisodesMutation } from '@/core/react-query/series/mutations'; import { useSeriesEpisodesInfiniteQuery } from '@/core/react-query/series/queries'; import { IncludeOnlyFilterEnum } from '@/core/react-query/series/types'; @@ -88,7 +87,7 @@ const SeriesEpisodes = () => { } = seriesEpisodesQuery; const [episodes, episodeCount] = useFlattenListResult(data); - const { mutate: watchEpisode } = useWatchSeriesEpisodesMutation(); + const { mutate: watchEpisode } = useWatchSeriesEpisodesMutation(series.IDs.ID); const hasMissingEpisodes = useMemo( () => ((series.Sizes.Missing.Episodes ?? 0) > 0), @@ -127,19 +126,8 @@ const SeriesEpisodes = () => { [fetchNextPage], ); - const handleMarkWatched = useEventCallback((watched: boolean) => { - watchEpisode({ - seriesId: series.IDs.ID, - value: watched, - ...filterOptions, - }, { - onSuccess: () => toast.success(`Episodes marked as ${watched ? 'watched' : 'unwatched'}!`), - onError: () => toast.error(`Failed to mark episodes as ${watched ? 'watched' : 'unwatched'}!`), - }); - }); - - const markFilteredWatched = useEventCallback(() => handleMarkWatched(true)); - const markFilteredUnwatched = useEventCallback(() => handleMarkWatched(false)); + const markFilteredWatched = useEventCallback(() => watchEpisode({ value: true, ...filterOptions })); + const markFilteredUnwatched = useEventCallback(() => watchEpisode({ value: false, ...filterOptions })); const resetSelection = useEventCallback(() => setSelectedEpisodes(new Set())); diff --git a/src/pages/collection/series/SeriesImages.tsx b/src/pages/collection/series/SeriesImages.tsx index 0842f657..85dc4bc6 100644 --- a/src/pages/collection/series/SeriesImages.tsx +++ b/src/pages/collection/series/SeriesImages.tsx @@ -9,7 +9,6 @@ import BackgroundImagePlaceholderDiv from '@/components/BackgroundImagePlacehold import Button from '@/components/Input/Button'; import MultiStateButton from '@/components/Input/MultiStateButton'; import ShokoPanel from '@/components/Panels/ShokoPanel'; -import toast from '@/components/Toast'; import { useChangeSeriesImageMutation } from '@/core/react-query/series/mutations'; import { useSeriesImagesQuery } from '@/core/react-query/series/queries'; import useEventCallback from '@/hooks/useEventCallback'; @@ -51,7 +50,7 @@ const SeriesImages = () => { }, [imageType]); const [selectedImage, setSelectedImage] = useState(null); const images = useSeriesImagesQuery(series.IDs.ID).data; - const { mutate: changeImage } = useChangeSeriesImageMutation(); + const { mutate: changeImage } = useChangeSeriesImageMutation(series.IDs.ID); const splitPath = split(selectedImage?.RelativeFilepath ?? '-', '/'); const filename = splitPath[0] === '-' ? '-' : splitPath.pop(); @@ -63,11 +62,8 @@ const SeriesImages = () => { const handleSetPreferredImage = useEventCallback(() => { if (!selectedImage) return; - changeImage({ seriesId: series.IDs.ID, image: selectedImage }, { - onSuccess: () => { - setSelectedImage(null); - toast.success(`Series ${selectedImage.Type} image has been changed.`); - }, + changeImage(selectedImage, { + onSuccess: () => setSelectedImage(null), }); });