diff --git a/src/components/Input/Checkbox.tsx b/src/components/Input/Checkbox.tsx index 45c7b391b..80f776886 100644 --- a/src/components/Input/Checkbox.tsx +++ b/src/components/Input/Checkbox.tsx @@ -67,7 +67,7 @@ const Checkbox = memo((props: Props) => { )} {labelRight && ( - + {label} )} diff --git a/src/components/Input/Select.tsx b/src/components/Input/Select.tsx index 65dee5f7c..8f6980e50 100644 --- a/src/components/Input/Select.tsx +++ b/src/components/Input/Select.tsx @@ -43,7 +43,7 @@ function Select(props: Props) { id={id} value={value} onChange={onChange} - className="w-full appearance-none rounded-lg border border-panel-border bg-panel-input px-4 py-2 transition ease-in-out focus:shadow-none focus:outline-none focus:ring-2 focus:ring-inset focus:ring-panel-icon-action" + className="w-full appearance-none rounded-lg border border-panel-border bg-panel-input py-2 pl-4 pr-8 transition ease-in-out focus:shadow-none focus:outline-none focus:ring-2 focus:ring-inset focus:ring-panel-icon-action" > {children} diff --git a/src/components/Input/SelectEpisodeList.tsx b/src/components/Input/SelectEpisodeList.tsx index dd1c2065b..ada90a3f1 100644 --- a/src/components/Input/SelectEpisodeList.tsx +++ b/src/components/Input/SelectEpisodeList.tsx @@ -7,6 +7,7 @@ import cx from 'classnames'; import { find, toInteger } from 'lodash'; import { EpisodeTypeEnum } from '@/core/types/api/episode'; +import getEpisodePrefix from '@/core/utilities/getEpisodePrefix'; import Input from './Input'; @@ -56,24 +57,6 @@ type Props = { rowIdx: number; }; -const getPrefix = (type: EpisodeTypeEnum) => { - switch (type) { - case EpisodeTypeEnum.Special: - return 'S'; - case EpisodeTypeEnum.ThemeSong: - return 'C'; - case EpisodeTypeEnum.Trailer: - return 'T'; - case EpisodeTypeEnum.Other: - return 'O'; - case EpisodeTypeEnum.Parody: - return 'P'; - case EpisodeTypeEnum.Normal: - default: - return ''; - } -}; - const SelectOption = (option: Option & { divider: boolean }) => ( (
- {getPrefix(option.type) + option.number} + {getEpisodePrefix(option.type) + option.number}
|
{option.label}
diff --git a/src/components/Utilities/ReleaseManagement/EpisodeList.tsx b/src/components/Utilities/ReleaseManagement/EpisodeList.tsx index 6542c71ad..51a671422 100644 --- a/src/components/Utilities/ReleaseManagement/EpisodeList.tsx +++ b/src/components/Utilities/ReleaseManagement/EpisodeList.tsx @@ -5,6 +5,7 @@ import { Icon } from '@mdi/react'; import UtilitiesTable from '@/components/Utilities/UtilitiesTable'; import { useSeriesEpisodesWithMultipleReleases } 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'; @@ -15,7 +16,7 @@ const columns: UtilityHeaderType[] = [ id: 'episode', name: 'Episode Name', className: 'line-clamp-1 grow basis-0 overflow-hidden', - item: episode => `${episode.AniDB?.EpisodeNumber} - ${episode.Name}`, + item: episode => `${getEpisodePrefix(episode.AniDB?.Type)}${episode.AniDB?.EpisodeNumber} - ${episode.Name}`, }, { id: 'file-count', @@ -33,11 +34,11 @@ const columns: UtilityHeaderType[] = [ }, ]; -const EpisodeList = ({ seriesId }: { seriesId: number }) => { +const EpisodeList = ({ ignoreVariations, seriesId }: { ignoreVariations: boolean, seriesId: number }) => { const navigate = useNavigate(); const episodesQuery = useSeriesEpisodesWithMultipleReleases( seriesId, - { includeDataFrom: ['AniDB'], includeAbsolutePaths: true, pageSize: 25 }, + { ignoreVariations, includeDataFrom: ['AniDB'], includeAbsolutePaths: true, pageSize: 25 }, seriesId > 0, ); const [episodes, episodeCount] = useFlattenListResult(episodesQuery.data); diff --git a/src/components/Utilities/Unrecognized/MenuButton.tsx b/src/components/Utilities/Unrecognized/MenuButton.tsx index 979820f35..bcd61f978 100644 --- a/src/components/Utilities/Unrecognized/MenuButton.tsx +++ b/src/components/Utilities/Unrecognized/MenuButton.tsx @@ -13,7 +13,11 @@ const MenuButton = React.memo(( disabled?: boolean; }, ) => ( - diff --git a/src/core/react-query/file/mutations.ts b/src/core/react-query/file/mutations.ts index b86e62971..22a3e8c97 100644 --- a/src/core/react-query/file/mutations.ts +++ b/src/core/react-query/file/mutations.ts @@ -8,6 +8,7 @@ import type { IgnoreFileRequestType, LinkManyFilesToOneEpisodeRequestType, LinkOneFileToManyEpisodesRequestType, + MarkVariationRequestType, } from '@/core/react-query/file/types'; export const useDeleteFileMutation = () => @@ -58,3 +59,9 @@ export const useRescanFileMutation = () => useMutation({ mutationFn: (fileId: number) => axios.post(`File/${fileId}/Rescan`), }); + +export const useMarkVariationMutation = () => + useMutation({ + mutationFn: ({ fileId, variation }: MarkVariationRequestType) => + axios.put(`File/${fileId}/Variation`, undefined, { params: { value: variation } }), + }); diff --git a/src/core/react-query/file/types.ts b/src/core/react-query/file/types.ts index 4edf61207..b4169a1da 100644 --- a/src/core/react-query/file/types.ts +++ b/src/core/react-query/file/types.ts @@ -17,3 +17,8 @@ export type LinkManyFilesToOneEpisodeRequestType = { episodeID: number; fileIDs: number[]; }; + +export type MarkVariationRequestType = { + fileId: number; + variation: boolean; +}; diff --git a/src/core/utilities/getEpisodePrefix.ts b/src/core/utilities/getEpisodePrefix.ts new file mode 100644 index 000000000..c46925d39 --- /dev/null +++ b/src/core/utilities/getEpisodePrefix.ts @@ -0,0 +1,21 @@ +import { EpisodeTypeEnum } from '@/core/types/api/episode'; + +const getEpisodePrefix = (type?: EpisodeTypeEnum) => { + switch (type) { + case EpisodeTypeEnum.Special: + return 'S'; + case EpisodeTypeEnum.ThemeSong: + return 'C'; + case EpisodeTypeEnum.Trailer: + return 'T'; + case EpisodeTypeEnum.Other: + return 'O'; + case EpisodeTypeEnum.Parody: + return 'P'; + case EpisodeTypeEnum.Normal: + default: + return ''; + } +}; + +export default getEpisodePrefix; diff --git a/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx b/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx index 1c1a2a492..dfb20c3a2 100644 --- a/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx +++ b/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtil.tsx @@ -3,6 +3,7 @@ import { mdiFileDocumentMultipleOutline, mdiLoading, mdiOpenInNew, mdiRefresh } import { Icon } from '@mdi/react'; import Button from '@/components/Input/Button'; +import Checkbox from '@/components/Input/Checkbox'; import ShokoPanel from '@/components/Panels/ShokoPanel'; import ItemCount from '@/components/Utilities/ItemCount'; import EpisodeList from '@/components/Utilities/ReleaseManagement/EpisodeList'; @@ -57,23 +58,48 @@ const columns: UtilityHeaderType[] = [ }, ]; -const Menu = () => ( -
- invalidateQueries(['release-management', 'series'])} icon={mdiRefresh} name="Refresh" /> -
-); - const MultiplesUtil = () => { - const seriesQuery = useSeriesWithMultipleReleases({ pageSize: 25 }); + const [selectedSeries, setSelectedSeries] = useState(0); + const [ignoreVariations, setIgnoreVariations] = useState(true); + const [onlyFinishedSeries, setOnlyFinishedSeries] = useState(true); + + const seriesQuery = useSeriesWithMultipleReleases({ ignoreVariations, onlyFinishedSeries, pageSize: 25 }); const [series, seriesCount] = useFlattenListResult(seriesQuery.data); - const [selectedSeries, setSelectedSeries] = useState(0); + const handleCheckboxChange = (type: 'variations' | 'series', checked: boolean) => { + setSelectedSeries(0); + if (type === 'variations') setIgnoreVariations(checked); + if (type === 'series') setOnlyFinishedSeries(checked); + }; return (
} options={}>
- +
+ invalidateQueries(['release-management', 'series'])} + icon={mdiRefresh} + name="Refresh" + /> + + handleCheckboxChange('variations', event.target.checked)} + label="Ignore Variations" + labelRight + /> + + handleCheckboxChange('series', event.target.checked)} + label="Only Finished Series" + labelRight + /> +
+
- +
); diff --git a/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtilEpisode.tsx b/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtilEpisode.tsx index 251d1c53a..86ab0ea95 100644 --- a/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtilEpisode.tsx +++ b/src/pages/utilities/ReleaseManagementUtilityTabs/MultiplesUtilEpisode.tsx @@ -2,13 +2,18 @@ import React, { useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { mdiCloseCircleOutline, mdiFileDocumentMultipleOutline, mdiOpenInNew } from '@mdi/js'; import { Icon } from '@mdi/react'; -import { countBy, forEach } from 'lodash'; +import { countBy, forEach, map, toNumber } from 'lodash'; import FileInfo from '@/components/FileInfo'; import Button from '@/components/Input/Button'; import Select from '@/components/Input/Select'; import ShokoPanel from '@/components/Panels/ShokoPanel'; +import toast from '@/components/Toast'; import Title from '@/components/Utilities/ReleaseManagement/Title'; +import { useDeleteFileMutation, useMarkVariationMutation } from '@/core/react-query/file/mutations'; +import { invalidateQueries } from '@/core/react-query/queryClient'; +import getEpisodePrefix from '@/core/utilities/getEpisodePrefix'; +import useEventCallback from '@/hooks/useEventCallback'; import type { EpisodeType } from '@/core/types/api/episode'; @@ -27,6 +32,11 @@ const MultiplesUtilEpisode = () => { if (!locationState) navigate('../release-management', { replace: true }); const { episode } = locationState; + const { mutateAsync: deleteFile } = useDeleteFileMutation(); + const { mutateAsync: markVariation } = useMarkVariationMutation(); + + const [operationsPending, setOperationsPending] = useState(false); + const [ fileOptions, setFileOptions, @@ -47,6 +57,27 @@ const MultiplesUtilEpisode = () => { const optionCounts = useMemo(() => countBy(fileOptions), [fileOptions]); + const confirmChanges = useEventCallback(() => { + setOperationsPending(true); + + const operations = map(fileOptions, (option, id) => { + const file = episode.Files!.find(item => item.ID === toNumber(id))!; + 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); + invalidateQueries(['release-management', 'series']); + navigate('../release-management', { replace: true }); + }); + }); + return (
}> @@ -55,12 +86,17 @@ const MultiplesUtilEpisode = () => { - @@ -68,7 +104,7 @@ const MultiplesUtilEpisode = () => {
- {`${episode.AniDB?.EpisodeNumber} - ${episode.Name}`} + {`${getEpisodePrefix(episode.AniDB?.Type)}${episode.AniDB?.EpisodeNumber} - ${episode.Name}`}
{optionCounts.keep ?? 0}  Kept |  @@ -88,14 +124,13 @@ const MultiplesUtilEpisode = () => {
{file.AniDB?.ID && (