Sonarr/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx

630 lines
18 KiB
TypeScript

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import Episode from 'Episode/Episode';
import EpisodeFormats from 'Episode/EpisodeFormats';
import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import getReleaseTypeName from 'Episode/getReleaseTypeName';
import IndexerFlags from 'Episode/IndexerFlags';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series';
import {
reprocessInteractiveImportItems,
updateInteractiveImportItem,
} from 'Store/Actions/interactiveImportActions';
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
type SelectType =
| 'series'
| 'season'
| 'episode'
| 'releaseGroup'
| 'quality'
| 'language'
| 'indexerFlags'
| 'releaseType';
type SelectedChangeProps = SelectStateInputProps & {
hasEpisodeFileId: boolean;
};
interface InteractiveImportRowProps {
id: number;
allowSeriesChange: boolean;
relativePath: string;
series?: Series;
seasonNumber?: number;
episodes?: Episode[];
releaseGroup?: string;
quality?: QualityModel;
languages?: Language[];
size: number;
releaseType: ReleaseType;
customFormats?: object[];
customFormatScore?: number;
indexerFlags: number;
rejections: Rejection[];
columns: Column[];
episodeFileId?: number;
isReprocessing?: boolean;
isSelected?: boolean;
modalTitle: string;
onSelectedChange(result: SelectedChangeProps): void;
onValidRowChange(id: number, isValid: boolean): void;
}
function InteractiveImportRow(props: InteractiveImportRowProps) {
const {
id,
allowSeriesChange,
relativePath,
series,
seasonNumber,
episodes = [],
quality,
languages,
releaseGroup,
size,
releaseType,
customFormats,
customFormatScore,
indexerFlags,
rejections,
isReprocessing,
isSelected,
modalTitle,
episodeFileId,
columns,
onSelectedChange,
onValidRowChange,
} = props;
const dispatch = useDispatch();
const isSeriesColumnVisible = useMemo(
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
[columns]
);
const isIndexerFlagsColumnVisible = useMemo(
() => columns.find((c) => c.name === 'indexerFlags')?.isVisible ?? false,
[columns]
);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
);
useEffect(
() => {
if (
allowSeriesChange &&
series &&
seasonNumber != null &&
episodes.length &&
quality &&
languages &&
size > 0
) {
onSelectedChange({
id,
hasEpisodeFileId: !!episodeFileId,
value: true,
shiftKey: false,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
const isValid = !!(
series &&
seasonNumber != null &&
episodes.length &&
quality &&
languages
);
if (isSelected && !isValid) {
onValidRowChange(id, false);
} else {
onValidRowChange(id, true);
}
}, [
id,
series,
seasonNumber,
episodes,
quality,
languages,
isSelected,
onValidRowChange,
]);
const onSelectedChangeWrapper = useCallback(
(result: SelectedChangeProps) => {
onSelectedChange({
...result,
hasEpisodeFileId: !!episodeFileId,
});
},
[episodeFileId, onSelectedChange]
);
const selectRowAfterChange = useCallback(() => {
if (!isSelected) {
onSelectedChange({
id,
hasEpisodeFileId: !!episodeFileId,
value: true,
shiftKey: false,
});
}
}, [id, episodeFileId, isSelected, onSelectedChange]);
const onSelectModalClose = useCallback(() => {
setSelectModalOpen(null);
}, [setSelectModalOpen]);
const onSelectSeriesPress = useCallback(() => {
setSelectModalOpen('series');
}, [setSelectModalOpen]);
const onSeriesSelect = useCallback(
(series: Series) => {
dispatch(
updateInteractiveImportItem({
id,
series,
seasonNumber: undefined,
episodes: [],
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectSeasonPress = useCallback(() => {
setSelectModalOpen('season');
}, [setSelectModalOpen]);
const onSeasonSelect = useCallback(
(seasonNumber: number) => {
dispatch(
updateInteractiveImportItem({
id,
seasonNumber,
episodes: [],
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectEpisodePress = useCallback(() => {
setSelectModalOpen('episode');
}, [setSelectModalOpen]);
const onEpisodesSelect = useCallback(
(selectedEpisodes: SelectedEpisode[]) => {
dispatch(
updateInteractiveImportItem({
id,
episodes: selectedEpisodes[0].episodes,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectReleaseGroupPress = useCallback(() => {
setSelectModalOpen('releaseGroup');
}, [setSelectModalOpen]);
const onReleaseGroupSelect = useCallback(
(releaseGroup: string) => {
dispatch(
updateInteractiveImportItem({
id,
releaseGroup,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectQualityPress = useCallback(() => {
setSelectModalOpen('quality');
}, [setSelectModalOpen]);
const onQualitySelect = useCallback(
(quality: QualityModel) => {
dispatch(
updateInteractiveImportItem({
id,
quality,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectLanguagePress = useCallback(() => {
setSelectModalOpen('language');
}, [setSelectModalOpen]);
const onLanguagesSelect = useCallback(
(languages: Language[]) => {
dispatch(
updateInteractiveImportItem({
id,
languages,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectReleaseTypePress = useCallback(() => {
setSelectModalOpen('releaseType');
}, [setSelectModalOpen]);
const onReleaseTypeSelect = useCallback(
(releaseType: ReleaseType) => {
dispatch(
updateInteractiveImportItem({
id,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectIndexerFlagsPress = useCallback(() => {
setSelectModalOpen('indexerFlags');
}, [setSelectModalOpen]);
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItem({
id,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const seriesTitle = series ? series.title : '';
const isAnime = series?.seriesType === 'anime';
const episodeInfo = episodes.map((episode) => {
return (
<div key={episode.id}>
{episode.episodeNumber}
{isAnime && episode.absoluteEpisodeNumber != null
? ` (${episode.absoluteEpisodeNumber})`
: ''}
{` - ${episode.title}`}
</div>
);
});
const requiresSeasonNumber = isNaN(Number(seasonNumber));
const showSeriesPlaceholder = isSelected && !series;
const showSeasonNumberPlaceholder =
isSelected && !!series && requiresSeasonNumber && !isReprocessing;
const showEpisodeNumbersPlaceholder =
isSelected && Number.isInteger(seasonNumber) && !episodes.length;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages;
const showIndexerFlagsPlaceholder = isSelected && !indexerFlags;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
/>
<TableRowCell className={styles.relativePath} title={relativePath}>
{relativePath}
</TableRowCell>
{isSeriesColumnVisible ? (
<TableRowCellButton
isDisabled={!allowSeriesChange}
title={
allowSeriesChange ? translate('ClickToChangeSeries') : undefined
}
onPress={onSelectSeriesPress}
>
{showSeriesPlaceholder ? (
<InteractiveImportRowCellPlaceholder />
) : (
seriesTitle
)}
</TableRowCellButton>
) : null}
<TableRowCellButton
isDisabled={!series}
title={series ? translate('ClickToChangeSeason') : undefined}
onPress={onSelectSeasonPress}
>
{showSeasonNumberPlaceholder ? (
<InteractiveImportRowCellPlaceholder />
) : (
seasonNumber
)}
{isReprocessing && seasonNumber == null ? (
<LoadingIndicator className={styles.reprocessing} size={20} />
) : null}
</TableRowCellButton>
<TableRowCellButton
isDisabled={!series || requiresSeasonNumber}
title={
series && !requiresSeasonNumber
? translate('ClickToChangeEpisode')
: undefined
}
onPress={onSelectEpisodePress}
>
{showEpisodeNumbersPlaceholder ? (
<InteractiveImportRowCellPlaceholder />
) : (
episodeInfo
)}
</TableRowCellButton>
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={onSelectReleaseGroupPress}
>
{showReleaseGroupPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
releaseGroup
)}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title={translate('ClickToChangeQuality')}
onPress={onSelectQualityPress}
>
{showQualityPlaceholder && <InteractiveImportRowCellPlaceholder />}
{!showQualityPlaceholder && !!quality && (
<EpisodeQuality className={styles.label} quality={quality} />
)}
</TableRowCellButton>
<TableRowCellButton
className={styles.languages}
title={translate('ClickToChangeLanguage')}
onPress={onSelectLanguagePress}
>
{showLanguagePlaceholder && <InteractiveImportRowCellPlaceholder />}
{!showLanguagePlaceholder && !!languages && (
<EpisodeLanguages className={styles.label} languages={languages} />
)}
</TableRowCellButton>
<TableRowCell>{formatBytes(size)}</TableRowCell>
<TableRowCellButton
title={translate('ClickToChangeReleaseType')}
onPress={onSelectReleaseTypePress}
>
{getReleaseTypeName(releaseType)}
</TableRowCellButton>
<TableRowCell>
{customFormats?.length ? (
<Popover
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
title={translate('CustomFormats')}
body={
<div className={styles.customFormatTooltip}>
<EpisodeFormats formats={customFormats} />
</div>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
{isIndexerFlagsColumnVisible ? (
<TableRowCellButton
title={translate('ClickToChangeIndexerFlags')}
onPress={onSelectIndexerFlagsPress}
>
{showIndexerFlagsPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
<>
{indexerFlags ? (
<Popover
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
title={translate('IndexerFlags')}
body={<IndexerFlags indexerFlags={indexerFlags} />}
position={tooltipPositions.LEFT}
/>
) : null}
</>
)}
</TableRowCellButton>
) : null}
<TableRowCell>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection.reason}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
canFlip={false}
/>
) : null}
</TableRowCell>
<SelectSeriesModal
isOpen={selectModalOpen === 'series'}
modalTitle={modalTitle}
onSeriesSelect={onSeriesSelect}
onModalClose={onSelectModalClose}
/>
<SelectSeasonModal
isOpen={selectModalOpen === 'season'}
seriesId={series?.id}
modalTitle={modalTitle}
onSeasonSelect={onSeasonSelect}
onModalClose={onSelectModalClose}
/>
<SelectEpisodeModal
isOpen={selectModalOpen === 'episode'}
selectedIds={[id]}
seriesId={series?.id}
isAnime={isAnime}
seasonNumber={seasonNumber}
selectedDetails={relativePath}
modalTitle={modalTitle}
onEpisodesSelect={onEpisodesSelect}
onModalClose={onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === 'releaseGroup'}
releaseGroup={releaseGroup ?? ''}
modalTitle={modalTitle}
onReleaseGroupSelect={onReleaseGroupSelect}
onModalClose={onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === 'quality'}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
modalTitle={modalTitle}
onQualitySelect={onQualitySelect}
onModalClose={onSelectModalClose}
/>
<SelectLanguageModal
isOpen={selectModalOpen === 'language'}
languageIds={languages ? languages.map((l) => l.id) : []}
modalTitle={modalTitle}
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
<SelectReleaseTypeModal
isOpen={selectModalOpen === 'releaseType'}
releaseType={releaseType ?? 'unknown'}
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={indexerFlags ?? 0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
/>
</TableRow>
);
}
export default InteractiveImportRow;