From dbb5a964d8cad624fd6cca708b0d366439b7333c Mon Sep 17 00:00:00 2001 From: Harshith Mohan <26010946+harshithmohan@users.noreply.github.com> Date: Tue, 10 Dec 2024 00:11:32 +0530 Subject: [PATCH] Add duplicate files util --- .../Utilities/ReleaseManagement/Episode.tsx | 175 +++++++++++++++ .../MultiplesUtilEpisode.tsx | 101 --------- .../{MultiplesUtilList.tsx => SeriesList.tsx} | 142 +++++++----- .../Utilities/ReleaseManagement/Title.tsx | 4 +- src/components/Utilities/constants.tsx | 6 +- src/core/react-query/file/mutations.ts | 6 + src/core/react-query/file/types.ts | 6 + .../react-query/release-management/queries.ts | 26 ++- .../react-query/release-management/types.ts | 4 +- src/core/router/index.tsx | 5 +- src/core/types/api/file.ts | 2 + src/core/types/api/series.ts | 2 +- src/pages/utilities/ReleaseManagement.tsx | 210 ++++++++++++++++++ .../MultiplesUtil.tsx | 180 --------------- 14 files changed, 509 insertions(+), 360 deletions(-) create mode 100644 src/components/Utilities/ReleaseManagement/Episode.tsx delete mode 100644 src/components/Utilities/ReleaseManagement/MultiplesUtilEpisode.tsx rename src/components/Utilities/ReleaseManagement/{MultiplesUtilList.tsx => SeriesList.tsx} (63%) create mode 100644 src/pages/utilities/ReleaseManagement.tsx delete mode 100644 src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx diff --git a/src/components/Utilities/ReleaseManagement/Episode.tsx b/src/components/Utilities/ReleaseManagement/Episode.tsx new file mode 100644 index 000000000..a87d96fdd --- /dev/null +++ b/src/components/Utilities/ReleaseManagement/Episode.tsx @@ -0,0 +1,175 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { mdiOpenInNew } from '@mdi/js'; +import { Icon } from '@mdi/react'; +import { countBy, flatMap, forEach, map, toNumber } from 'lodash'; + +import FileInfo from '@/components/FileInfo'; +import Select from '@/components/Input/Select'; +import { getEpisodePrefix } from '@/core/utilities/getEpisodePrefix'; + +import type { ReleaseManagementOptionsType } from '@/components/Utilities/constants'; +import type { EpisodeType } from '@/core/types/api/episode'; + +type Props = { + type: 'multiples' | 'duplicates'; + episode: EpisodeType | undefined; + setFileOptions: (options: ReleaseManagementOptionsType) => void; +}; + +const Episode = ({ episode, setFileOptions, type }: Props) => { + const [options, setOptions] = useState( + () => { + const tempOptions: ReleaseManagementOptionsType = {}; + if (!episode) return tempOptions; + + if (type === 'multiples') { + forEach(episode.Files, (file) => { + if (file.IsVariation) tempOptions[file.ID] = 'variation'; + else tempOptions[file.ID] = 'keep'; + }); + return tempOptions; + } + + forEach( + flatMap(episode.Files, file => file.Locations), + (location) => { + tempOptions[location.ID] = 'keep'; + }, + ); + return tempOptions; + }, + ); + + const optionCounts = useMemo(() => countBy(options), [options]); + + useEffect(() => { + setFileOptions(options); + }, [options, setFileOptions]); + + if (!episode) return null; + + const handleOptionChange = (fileId: number, value: 'keep' | 'variation' | 'delete') => { + setOptions(tempOptions => ( + { ...tempOptions, [fileId]: value } + )); + }; + + return ( + <> +
+ {`${getEpisodePrefix(episode.AniDB?.Type)}${episode.AniDB?.EpisodeNumber} - ${episode.Name}`} +
+ {optionCounts.keep ?? 0} +  Kept |  + {type === 'multiples' && ( + <> + {optionCounts.variation ?? 0} +  Variation |  + + )} + {optionCounts.delete ?? 0} +  Delete +
+
+ + {type === 'duplicates' && flatMap(episode.Files, file => + map(file.Locations, (location, index) => { + const absolutePath = location.AbsolutePath ?? '??'; + const fileName = absolutePath.split(/[/\\]+/).pop(); + const folderPath = absolutePath.slice(0, absolutePath.replaceAll('\\', '/').lastIndexOf('/') + 1); + + return ( +
+
+
+
+
File Name
+ {fileName} +
+
+
Location
+ {folderPath} +
+
+
+ +
+ + + {file.AniDB?.ID && ( +
+
+ + {file.AniDB.ID} + (AniDB) + + +
+ )} +
+
+ ); + }))} + + {type === 'multiples' && episode.Files?.map(file => ( +
+ + +
+ + + {file.AniDB?.ID && ( + +
+ ))} + + ); +}; + +export default Episode; diff --git a/src/components/Utilities/ReleaseManagement/MultiplesUtilEpisode.tsx b/src/components/Utilities/ReleaseManagement/MultiplesUtilEpisode.tsx deleted file mode 100644 index 6039efd19..000000000 --- a/src/components/Utilities/ReleaseManagement/MultiplesUtilEpisode.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { mdiOpenInNew } from '@mdi/js'; -import { Icon } from '@mdi/react'; -import { countBy, forEach } from 'lodash'; - -import FileInfo from '@/components/FileInfo'; -import Select from '@/components/Input/Select'; -import { getEpisodePrefix } from '@/core/utilities/getEpisodePrefix'; - -import type { MultipleFileOptionsType } from '@/components/Utilities/constants'; -import type { EpisodeType } from '@/core/types/api/episode'; - -type Props = { - episode: EpisodeType | undefined; - setFileOptions: (options: MultipleFileOptionsType) => void; -}; - -const MultiplesUtilEpisode = ({ episode, setFileOptions }: Props) => { - const [options, setOptions] = useState( - () => { - const tempOptions: MultipleFileOptionsType = {}; - if (!episode) return tempOptions; - - forEach(episode.Files, (file) => { - if (file.IsVariation) tempOptions[file.ID] = 'variation'; - else tempOptions[file.ID] = 'keep'; - }); - return tempOptions; - }, - ); - - const optionCounts = useMemo(() => countBy(options), [options]); - - useEffect(() => { - setFileOptions(options); - }, [options, setFileOptions]); - - if (!episode) return null; - - const handleOptionChange = (fileId: number, value: 'keep' | 'variation' | 'delete') => { - setOptions(tempOptions => ( - { ...tempOptions, [fileId]: value } - )); - }; - - return ( - <> -
- {`${getEpisodePrefix(episode.AniDB?.Type)}${episode.AniDB?.EpisodeNumber} - ${episode.Name}`} -
- {optionCounts.keep ?? 0} -  Kept |  - {optionCounts.variation ?? 0} -  Variation |  - {optionCounts.delete ?? 0} -  Delete -
-
- - {episode.Files?.map(file => ( -
- - -
- - - {file.AniDB?.ID && ( - -
- ))} - - ); -}; - -export default MultiplesUtilEpisode; diff --git a/src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx b/src/components/Utilities/ReleaseManagement/SeriesList.tsx similarity index 63% rename from src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx rename to src/components/Utilities/ReleaseManagement/SeriesList.tsx index 0361e2eec..d0de8c38f 100644 --- a/src/components/Utilities/ReleaseManagement/MultiplesUtilList.tsx +++ b/src/components/Utilities/ReleaseManagement/SeriesList.tsx @@ -1,20 +1,21 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { mdiLoading, mdiOpenInNew } from '@mdi/js'; import { Icon } from '@mdi/react'; +import { forEach } from 'lodash'; import UtilitiesTable from '@/components/Utilities/UtilitiesTable'; import { - useSeriesEpisodesWithMultipleReleases, - useSeriesWithMultipleReleases, + useReleaseManagementSeries, + useReleaseManagementSeriesEpisodes, } from '@/core/react-query/release-management/queries'; import { getEpisodePrefix } from '@/core/utilities/getEpisodePrefix'; import useFlattenListResult from '@/hooks/useFlattenListResult'; import type { UtilityHeaderType } from '@/components/Utilities/constants'; import type { EpisodeType } from '@/core/types/api/episode'; -import type { SeriesWithMultipleReleasesType } from '@/core/types/api/series'; +import type { ReleaseManagementSeriesType } from '@/core/types/api/series'; -const seriesColumns: UtilityHeaderType[] = [ +const seriesColumns: UtilityHeaderType[] = [ { id: 'series', name: 'Series (AniDB ID)', @@ -57,58 +58,76 @@ const seriesColumns: UtilityHeaderType[] = [ }, ]; -const episodeColumns: UtilityHeaderType[] = [ - { - id: 'episode', - name: 'Episode Name', - className: 'line-clamp-1 grow basis-0 overflow-hidden', - item: episode => ( -
- - {getEpisodePrefix(episode.AniDB?.Type)} - {episode.AniDB?.EpisodeNumber} -  -  - {episode.Name} - -
- ( - {episode.IDs.AniDB} - ) -
- event.stopPropagation()} - > - - +const episodeNameColumn: UtilityHeaderType = { + id: 'episode', + name: 'Episode Name', + className: 'line-clamp-1 grow basis-0 overflow-hidden', + item: episode => ( +
+ + {getEpisodePrefix(episode.AniDB?.Type)} + {episode.AniDB?.EpisodeNumber} +  -  + {episode.Name} + +
+ ( + {episode.IDs.AniDB} + )
- ), + event.stopPropagation()} + > + + +
+ ), +}; + +const multiplesEpisodeFileCountColumn: UtilityHeaderType = { + id: 'file-count', + name: 'File Count', + className: 'w-28', + item: (episode) => { + const count = episode.Files?.length ?? 0; + return ( + <> + {count} + {count === 1 ? ' File' : ' Files'} + + ); }, - { - id: 'file-count', - name: 'File Count', - className: 'w-28', - item: (episode) => { - const count = episode.Files?.length ?? 0; - return ( - <> - {count} - {count === 1 ? ' File' : ' Files'} - - ); - }, +}; + +const duplicatesEpisodeFileCountColumn: UtilityHeaderType = { + id: 'duplicate-count', + name: 'Duplicate Count', + className: 'w-40', + item: (episode) => { + let count = 0; + forEach(episode.Files, (file) => { + if (file.Locations.length > 1) count += 1; + }); + return ( + <> + {count} + {count === 1 ? ' Duplicate' : ' Duplicates'} + + ); }, -]; +}; type Props = { + type: 'multiples' | 'duplicates'; ignoreVariations: boolean; onlyFinishedSeries: boolean; setSelectedEpisode: (episode: EpisodeType) => void; @@ -116,15 +135,16 @@ type Props = { setSeriesCount: (count: number) => void; }; -const MultiplesUtilList = ( - { ignoreVariations, onlyFinishedSeries, setSelectedEpisode, setSelectedSeriesId, setSeriesCount }: Props, +const SeriesList = ( + { ignoreVariations, onlyFinishedSeries, setSelectedEpisode, setSelectedSeriesId, setSeriesCount, type }: Props, ) => { const [selectedSeries, setSelectedSeries] = useState(0); - const seriesQuery = useSeriesWithMultipleReleases({ ignoreVariations, onlyFinishedSeries, pageSize: 25 }); + const seriesQuery = useReleaseManagementSeries(type, { ignoreVariations, onlyFinishedSeries, pageSize: 25 }); const [series, seriesCount] = useFlattenListResult(seriesQuery.data); - const episodesQuery = useSeriesEpisodesWithMultipleReleases( + const episodesQuery = useReleaseManagementSeriesEpisodes( + type, selectedSeries, { ignoreVariations, includeDataFrom: ['AniDB'], includeAbsolutePaths: true, pageSize: 25 }, selectedSeries > 0, @@ -141,6 +161,11 @@ const MultiplesUtilList = ( setSelectedSeries(0); }, [seriesQuery.data]); + const episodeColumns = useMemo(() => [ + episodeNameColumn, + type === 'multiples' ? multiplesEpisodeFileCountColumn : duplicatesEpisodeFileCountColumn, + ], [type]); + return ( <>
@@ -152,7 +177,8 @@ const MultiplesUtilList = ( {!seriesQuery.isPending && seriesCount === 0 && (
- No series with multiple files! + No series with + {type === 'multiples' ? ' multiple releases!' : ' duplicate files!'}
)} @@ -196,4 +222,4 @@ const MultiplesUtilList = ( ); }; -export default MultiplesUtilList; +export default SeriesList; diff --git a/src/components/Utilities/ReleaseManagement/Title.tsx b/src/components/Utilities/ReleaseManagement/Title.tsx index 4300cd9fa..1fdcb7089 100644 --- a/src/components/Utilities/ReleaseManagement/Title.tsx +++ b/src/components/Utilities/ReleaseManagement/Title.tsx @@ -19,8 +19,8 @@ const Title = () => ( Release Management - {/*
|
*/} - {/* */} +
|
+
); diff --git a/src/components/Utilities/constants.tsx b/src/components/Utilities/constants.tsx index 476a8c2cd..4bad515c9 100644 --- a/src/components/Utilities/constants.tsx +++ b/src/components/Utilities/constants.tsx @@ -6,16 +6,16 @@ import { dayjs } from '@/core/util'; import type { EpisodeType } from '@/core/types/api/episode'; import type { FileType } from '@/core/types/api/file'; -import type { SeriesType, SeriesWithMultipleReleasesType } from '@/core/types/api/series'; +import type { ReleaseManagementSeriesType, SeriesType } from '@/core/types/api/series'; -export type UtilityHeaderType = { +export type UtilityHeaderType = { id: string; name: string; className: string; item: (_: T) => React.ReactNode; }; -export type MultipleFileOptionsType = Record; +export type ReleaseManagementOptionsType = Record; export const criteriaMap = { importFolder: FileSortCriteriaEnum.ImportFolderName, diff --git a/src/core/react-query/file/mutations.ts b/src/core/react-query/file/mutations.ts index 90c4894ef..802187c9b 100644 --- a/src/core/react-query/file/mutations.ts +++ b/src/core/react-query/file/mutations.ts @@ -6,6 +6,7 @@ import { axios } from '@/core/axios'; import queryClient, { invalidateQueries } from '@/core/react-query/queryClient'; import type { + DeleteFileLocationRequestType, DeleteFileRequestType, DeleteFilesRequestType, IgnoreFileRequestType, @@ -68,6 +69,11 @@ export const useDeleteFileLinkMutation = () => ), }); +export const useDeleteFileLocationMutation = () => + useMutation({ + mutationFn: ({ locationId }: DeleteFileLocationRequestType) => axios.delete(`File/Location/${locationId}`), + }); + export const useIgnoreFileMutation = () => useMutation({ mutationFn: ({ fileId, ignore }: IgnoreFileRequestType) => diff --git a/src/core/react-query/file/types.ts b/src/core/react-query/file/types.ts index 2e039bfd7..c61d67b3e 100644 --- a/src/core/react-query/file/types.ts +++ b/src/core/react-query/file/types.ts @@ -8,6 +8,12 @@ export type DeleteFileRequestType = { removeFolder: boolean; }; +export type DeleteFileLocationRequestType = { + locationId: number; + deleteFile?: boolean; + deleteFolder?: boolean; +}; + export type IgnoreFileRequestType = { fileId: number; ignore: boolean; diff --git a/src/core/react-query/release-management/queries.ts b/src/core/react-query/release-management/queries.ts index 9d4b6e35e..1ad7de7db 100644 --- a/src/core/react-query/release-management/queries.ts +++ b/src/core/react-query/release-management/queries.ts @@ -3,19 +3,22 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { axios } from '@/core/axios'; import type { - SeriesEpisodesWithMultipleReleasesType, - SeriesWithMultipleReleasesRequestType, + ReleaseManagementSeriesEpisodesType, + ReleaseManagementSeriesRequestType, } from '@/core/react-query/release-management/types'; import type { ListResultType } from '@/core/types/api'; import type { EpisodeType } from '@/core/types/api/episode'; -import type { SeriesWithMultipleReleasesType } from '@/core/types/api/series'; +import type { ReleaseManagementSeriesType } from '@/core/types/api/series'; -export const useSeriesWithMultipleReleases = (params: SeriesWithMultipleReleasesRequestType) => - useInfiniteQuery>({ - queryKey: ['release-management', 'series', params], +export const useReleaseManagementSeries = ( + type: 'multiples' | 'duplicates', + params: ReleaseManagementSeriesRequestType, +) => + useInfiniteQuery>({ + queryKey: ['release-management', 'series', type, params], queryFn: ({ pageParam }) => axios.get( - 'ReleaseManagement/Series', + `ReleaseManagement/${type === 'multiples' ? 'MultipleReleases' : 'DuplicateFiles'}/Series`, { params: { ...params, @@ -31,16 +34,17 @@ export const useSeriesWithMultipleReleases = (params: SeriesWithMultipleReleases }, }); -export const useSeriesEpisodesWithMultipleReleases = ( +export const useReleaseManagementSeriesEpisodes = ( + type: 'multiples' | 'duplicates', seriesId: number, - params: SeriesEpisodesWithMultipleReleasesType, + params: ReleaseManagementSeriesEpisodesType, enabled = true, ) => useInfiniteQuery>({ - queryKey: ['release-management', 'series', 'episodes', seriesId, params], + queryKey: ['release-management', 'series', 'episodes', type, seriesId, params], queryFn: ({ pageParam }) => axios.get( - `ReleaseManagement/Series/${seriesId}`, + `ReleaseManagement/${type === 'multiples' ? 'MultipleReleases' : 'DuplicateFiles'}/Series/${seriesId}/Episodes`, { params: { ...params, diff --git a/src/core/react-query/release-management/types.ts b/src/core/react-query/release-management/types.ts index 262b71e92..17b4575e6 100644 --- a/src/core/react-query/release-management/types.ts +++ b/src/core/react-query/release-management/types.ts @@ -1,13 +1,13 @@ import type { PaginationType } from '@/core/types/api'; import type { DataSourceType } from '@/core/types/api/common'; -export type SeriesWithMultipleReleasesRequestType = { +export type ReleaseManagementSeriesRequestType = { ignoreVariations?: boolean; includeDataFrom?: DataSourceType[]; onlyFinishedSeries?: boolean; } & PaginationType; -export type SeriesEpisodesWithMultipleReleasesType = { +export type ReleaseManagementSeriesEpisodesType = { includeDataFrom?: DataSourceType[]; includeFiles?: boolean; includeMediaInfo?: boolean; diff --git a/src/core/router/index.tsx b/src/core/router/index.tsx index 0cc0a0c53..d9db4e36e 100644 --- a/src/core/router/index.tsx +++ b/src/core/router/index.tsx @@ -41,7 +41,7 @@ import MetadataSitesSettings from '@/pages/settings/tabs/MetadataSitesSettings'; import UserManagementSettings from '@/pages/settings/tabs/UserManagementSettings'; import UnsupportedPage from '@/pages/unsupported/UnsupportedPage'; import FileSearch from '@/pages/utilities/FileSearch'; -import MultiplesUtil from '@/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil'; +import ReleaseManagement from '@/pages/utilities/ReleaseManagement'; import Renamer from '@/pages/utilities/Renamer'; import SeriesWithoutFilesUtility from '@/pages/utilities/SeriesWithoutFilesUtility'; import IgnoredFilesTab from '@/pages/utilities/UnrecognizedUtilityTabs/IgnoredFilesTab'; @@ -91,7 +91,8 @@ const router = sentryCreateBrowserRouter( } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/core/types/api/file.ts b/src/core/types/api/file.ts index 6676d853f..0acf23146 100644 --- a/src/core/types/api/file.ts +++ b/src/core/types/api/file.ts @@ -9,6 +9,8 @@ type XRefsType = { }; type FileTypeLocation = { + ID: number; + FileID: number; ImportFolderID: number; RelativePath: string; AbsolutePath?: string; diff --git a/src/core/types/api/series.ts b/src/core/types/api/series.ts index acde6fff3..243590d7d 100644 --- a/src/core/types/api/series.ts +++ b/src/core/types/api/series.ts @@ -19,7 +19,7 @@ export type SeriesType = { }; }; -export type SeriesWithMultipleReleasesType = { +export type ReleaseManagementSeriesType = { EpisodeCount: number; } & SeriesType; diff --git a/src/pages/utilities/ReleaseManagement.tsx b/src/pages/utilities/ReleaseManagement.tsx new file mode 100644 index 000000000..9e5b9c3f6 --- /dev/null +++ b/src/pages/utilities/ReleaseManagement.tsx @@ -0,0 +1,210 @@ +import React, { useEffect, useState } from 'react'; +import { mdiCloseCircleOutline, mdiFileDocumentMultipleOutline, mdiRefresh, mdiSelectMultiple } from '@mdi/js'; +import { Icon } from '@mdi/react'; +import cx from 'classnames'; +import { map, toNumber } from 'lodash'; +import { useToggle } from 'usehooks-ts'; + +import Button from '@/components/Input/Button'; +import Checkbox from '@/components/Input/Checkbox'; +import ShokoPanel from '@/components/Panels/ShokoPanel'; +import toast from '@/components/Toast'; +import TransitionDiv from '@/components/TransitionDiv'; +import ItemCount from '@/components/Utilities/ItemCount'; +import Episode from '@/components/Utilities/ReleaseManagement/Episode'; +import QuickSelectModal from '@/components/Utilities/ReleaseManagement/QuickSelectModal'; +import SeriesList from '@/components/Utilities/ReleaseManagement/SeriesList'; +import Title from '@/components/Utilities/ReleaseManagement/Title'; +import MenuButton from '@/components/Utilities/Unrecognized/MenuButton'; +import { + useDeleteFileLocationMutation, + useDeleteFileMutation, + useMarkVariationMutation, +} from '@/core/react-query/file/mutations'; +import { invalidateQueries, resetQueries } from '@/core/react-query/queryClient'; +import useEventCallback from '@/hooks/useEventCallback'; + +import type { ReleaseManagementOptionsType } from '@/components/Utilities/constants'; +import type { EpisodeType } from '@/core/types/api/episode'; +import type { AxiosResponse } from 'axios'; + +type Props = { + type: 'multiples' | 'duplicates'; +}; + +const ReleaseManagement = ({ type }: Props) => { + const [ignoreVariations, toggleIgnoreVariations] = useToggle(true); + const [onlyFinishedSeries, toggleOnlyFinishedSeries] = useToggle(false); + const [seriesCount, setSeriesCount] = useState(0); + const [selectedSeries, setSelectedSeries] = useState(0); + const [selectedEpisode, setSelectedEpisode] = useState(); + const [operationsPending, setOperationsPending] = useState(false); + const [fileOptions, setFileOptions] = useState({}); + const [showQuickSelectModal, toggleShowQuickSelectModal] = useToggle(false); + + useEffect(() => () => { + setSelectedSeries(0); + }, []); + + const { mutateAsync: deleteFile } = useDeleteFileMutation(); + const { mutateAsync: markVariation } = useMarkVariationMutation(); + const { mutateAsync: deleteFileLocation } = useDeleteFileLocationMutation(); + + const handleCheckboxChange = (checkboxType: 'variations' | 'series') => { + if (checkboxType === 'variations') toggleIgnoreVariations(); + if (checkboxType === 'series') toggleOnlyFinishedSeries(); + }; + + const confirmChanges = useEventCallback(() => { + setOperationsPending(true); + + let operations: (Promise> | null)[]; + + if (type === 'multiples') { + operations = map(fileOptions, (option, id) => { + if (!selectedEpisode) return null; + + const file = selectedEpisode.Files!.find(item => item.ID === toNumber(id))!; + if (!file) return null; + if (option === 'delete') return deleteFile({ fileId: file.ID, removeFolder: false }); + if (option === 'variation' && !file.IsVariation) return markVariation({ fileId: file.ID, variation: true }); + if (option === 'keep' && file.IsVariation) return markVariation({ fileId: file.ID, variation: false }); + return null; + }); + } else { + operations = map(fileOptions, (option, id) => { + if (!selectedEpisode || option !== 'delete') return null; + return deleteFileLocation({ locationId: toNumber(id) }); + }); + } + + Promise.all(operations) + .then(() => toast.success('Successful!')) + .catch(() => toast.error('One or more operations failed!')) + .finally(() => { + setOperationsPending(false); + resetQueries(['release-management']); + setSelectedEpisode(undefined); + }); + }); + + return ( + <> + {`Utilities > ${type === 'multiples' ? 'Multiple Releases' : 'Duplicate Files'} | Shoko`} +
+ } options={}> +
+
+ invalidateQueries(['release-management', 'series'])} + icon={mdiRefresh} + name="Refresh" + /> + + {type === 'multiples' && ( + handleCheckboxChange('variations')} + label="Ignore Variations" + labelRight + /> + )} + + handleCheckboxChange('series')} + label="Only Finished Series" + labelRight + /> +
+ + {/* TODO: Add support for auto-delete */} + {/* {!selectedEpisode && ( */} + {/* */} + {/* )} */} + + {(type === 'multiples' && !selectedEpisode) && ( + + )} + + {selectedEpisode && ( +
+ + +
+ )} +
+
+ +
+ + + + + + + +
+ + +
+ + ); +}; + +export default ReleaseManagement; diff --git a/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx b/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx deleted file mode 100644 index 1ce9855e7..000000000 --- a/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import React, { useState } from 'react'; -import { mdiCloseCircleOutline, mdiFileDocumentMultipleOutline, mdiRefresh, mdiSelectMultiple } from '@mdi/js'; -import { Icon } from '@mdi/react'; -import cx from 'classnames'; -import { map, toNumber } from 'lodash'; -import { useToggle } from 'usehooks-ts'; - -import Button from '@/components/Input/Button'; -import Checkbox from '@/components/Input/Checkbox'; -import ShokoPanel from '@/components/Panels/ShokoPanel'; -import toast from '@/components/Toast'; -import TransitionDiv from '@/components/TransitionDiv'; -import ItemCount from '@/components/Utilities/ItemCount'; -import MultiplesUtilEpisode from '@/components/Utilities/ReleaseManagement/MultiplesUtilEpisode'; -import MultiplesUtilList from '@/components/Utilities/ReleaseManagement/MultiplesUtilList'; -import QuickSelectModal from '@/components/Utilities/ReleaseManagement/QuickSelectModal'; -import Title from '@/components/Utilities/ReleaseManagement/Title'; -import MenuButton from '@/components/Utilities/Unrecognized/MenuButton'; -import { useDeleteFileMutation, useMarkVariationMutation } from '@/core/react-query/file/mutations'; -import { invalidateQueries, resetQueries } from '@/core/react-query/queryClient'; -import useEventCallback from '@/hooks/useEventCallback'; - -import type { MultipleFileOptionsType } from '@/components/Utilities/constants'; -import type { EpisodeType } from '@/core/types/api/episode'; - -const MultiplesUtil = () => { - const [ignoreVariations, toggleIgnoreVariations] = useToggle(true); - const [onlyFinishedSeries, toggleOnlyFinishedSeries] = useToggle(false); - const [seriesCount, setSeriesCount] = useState(0); - const [selectedSeries, setSelectedSeries] = useState(0); - const [selectedEpisode, setSelectedEpisode] = useState(); - const [operationsPending, setOperationsPending] = useState(false); - const [fileOptions, setFileOptions] = useState({}); - const [showQuickSelectModal, toggleShowQuickSelectModal] = useToggle(false); - - const { mutateAsync: deleteFile } = useDeleteFileMutation(); - const { mutateAsync: markVariation } = useMarkVariationMutation(); - - const handleCheckboxChange = (type: 'variations' | 'series') => { - if (type === 'variations') toggleIgnoreVariations(); - if (type === 'series') toggleOnlyFinishedSeries(); - }; - - const confirmChanges = useEventCallback(() => { - setOperationsPending(true); - - const operations = map(fileOptions, (option, id) => { - if (!selectedEpisode) return null; - - const file = selectedEpisode.Files!.find(item => item.ID === toNumber(id))!; - if (!file) return null; - if (option === 'delete') return deleteFile({ fileId: file.ID, removeFolder: false }); - if (option === 'variation' && !file.IsVariation) return markVariation({ fileId: file.ID, variation: true }); - if (option === 'keep' && file.IsVariation) return markVariation({ fileId: file.ID, variation: false }); - return null; - }); - - Promise.all(operations) - .then(() => toast.success('Successful!')) - .catch(() => toast.error('One or more operations failed!')) - .finally(() => { - setOperationsPending(false); - resetQueries(['release-management']); - setSelectedEpisode(undefined); - }); - }); - - return ( -
- } options={}> -
-
- invalidateQueries(['release-management', 'series'])} - icon={mdiRefresh} - name="Refresh" - /> - - handleCheckboxChange('variations')} - label="Ignore Variations" - labelRight - /> - - handleCheckboxChange('series')} - label="Only Finished Series" - labelRight - /> -
- - {/* TODO: Add support for auto-delete */} - {/* {!selectedEpisode && ( */} - {/* */} - {/* )} */} - - {!selectedEpisode && ( - - )} - - {selectedEpisode && ( -
- - -
- )} -
-
- -
- - - - - - - -
- - -
- ); -}; - -export default MultiplesUtil;