New: Option to override release and grab New: Option to select download client when multiple of the same type are configured (cherry picked from commit 07f0fbf9a51d54e44681fd0f74df4e048bff561a)pull/9066/head
parent
07b69e665d
commit
9b4f80535e
@ -0,0 +1,7 @@
|
||||
enum DownloadProtocol {
|
||||
Unknown = 'unknown',
|
||||
Usenet = 'usenet',
|
||||
Torrent = 'torrent',
|
||||
}
|
||||
|
||||
export default DownloadProtocol;
|
@ -1,365 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed, grabError) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadQueue');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadQueue');
|
||||
}
|
||||
|
||||
class InteractiveSearchRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmGrabModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId
|
||||
});
|
||||
};
|
||||
|
||||
onConfirmGrabPress = () => {
|
||||
this.setState({ isConfirmGrabModalOpen: true });
|
||||
};
|
||||
|
||||
onGrabConfirm = () => {
|
||||
this.setState({ isConfirmGrabModalOpen: false });
|
||||
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
searchPayload,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
...searchPayload
|
||||
});
|
||||
};
|
||||
|
||||
onGrabCancel = () => {
|
||||
this.setState({ isConfirmGrabModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
protocol,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
indexer,
|
||||
size,
|
||||
seeders,
|
||||
leechers,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
indexerFlags,
|
||||
rejections,
|
||||
downloadAllowed,
|
||||
isGrabbing,
|
||||
isGrabbed,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.age}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{
|
||||
!!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}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexer}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{
|
||||
historyGrabbedData?.date && !historyFailedData?.date &&
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DEFAULT}
|
||||
title={`${translate('Grabbed')}: ${formatDateTime(historyGrabbedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
historyFailedData?.date &&
|
||||
<Icon
|
||||
className={styles.failed}
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Failed')}: ${formatDateTime(historyFailedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
blocklistData?.date &&
|
||||
<Icon
|
||||
className={historyGrabbedData || historyFailedData ? styles.blocklist : ''}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Blocklisted')}: ${formatDateTime(blocklistData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.size}>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.peers}>
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
<Peers
|
||||
seeders={seeders}
|
||||
leechers={leechers}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.language}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormat}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
{customFormatScore > 0 && `+${customFormatScore}`}
|
||||
{customFormatScore < 0 && customFormatScore}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{
|
||||
!!indexerFlags.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.FLAG}
|
||||
kind={kinds.PRIMARY}
|
||||
/>
|
||||
}
|
||||
title={translate('IndexerFlags')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
indexerFlags.map((flag, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{flag}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('GrabRelease')}
|
||||
message={translate('GrabReleaseMessageText', [title])}
|
||||
confirmLabel={translate('Grab')}
|
||||
onConfirm={this.onGrabConfirm}
|
||||
onCancel={this.onGrabCancel}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchRow.propTypes = {
|
||||
guid: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
age: PropTypes.number.isRequired,
|
||||
ageHours: PropTypes.number.isRequired,
|
||||
ageMinutes: PropTypes.number.isRequired,
|
||||
publishDate: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
infoUrl: PropTypes.string.isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
indexer: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadAllowed: PropTypes.bool.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
historyFailedData: PropTypes.object,
|
||||
historyGrabbedData: PropTypes.object,
|
||||
blocklistData: PropTypes.object
|
||||
};
|
||||
|
||||
InteractiveSearchRow.defaultProps = {
|
||||
rejections: [],
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
};
|
||||
|
||||
export default InteractiveSearchRow;
|
@ -0,0 +1,378 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import MovieBlocklist from 'typings/MovieBlocklist';
|
||||
import MovieHistory from 'typings/MovieHistory';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
function getDownloadIcon(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddToDownloadQueue');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddedToDownloadQueue');
|
||||
}
|
||||
|
||||
interface InteractiveSearchRowProps {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
infoUrl: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
size: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mappedMovieId?: number;
|
||||
rejections: string[];
|
||||
indexerFlags: string[];
|
||||
episodeRequested: boolean;
|
||||
downloadAllowed: boolean;
|
||||
isDaily: boolean;
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
historyFailedData?: MovieHistory;
|
||||
historyGrabbedData?: MovieHistory;
|
||||
blocklistData?: MovieBlocklist;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
searchPayload: object;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
protocol,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
indexer,
|
||||
size,
|
||||
seeders,
|
||||
leechers,
|
||||
quality,
|
||||
languages,
|
||||
customFormatScore,
|
||||
customFormats,
|
||||
mappedMovieId,
|
||||
rejections = [],
|
||||
indexerFlags = [],
|
||||
downloadAllowed,
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData,
|
||||
searchPayload,
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
const onGrabPressWrapper = useCallback(() => {
|
||||
if (downloadAllowed) {
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirmGrabModalOpen(true);
|
||||
}, [
|
||||
guid,
|
||||
indexerId,
|
||||
downloadAllowed,
|
||||
onGrabPress,
|
||||
setIsConfirmGrabModalOpen,
|
||||
]);
|
||||
|
||||
const onGrabConfirm = useCallback(() => {
|
||||
setIsConfirmGrabModalOpen(false);
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
...searchPayload,
|
||||
});
|
||||
}, [guid, indexerId, searchPayload, onGrabPress, setIsConfirmGrabModalOpen]);
|
||||
|
||||
const onGrabCancel = useCallback(() => {
|
||||
setIsConfirmGrabModalOpen(false);
|
||||
}, [setIsConfirmGrabModalOpen]);
|
||||
|
||||
const onOverridePress = useCallback(() => {
|
||||
setIsOverrideModalOpen(true);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
const onOverrideModalClose = useCallback(() => {
|
||||
setIsOverrideModalOpen(false);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.age}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||
includeSeconds: true,
|
||||
})}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPressWrapper}
|
||||
/>
|
||||
|
||||
<Link
|
||||
className={styles.manualDownloadContent}
|
||||
title={translate('OverrideAndAddToDownloadQueue')}
|
||||
onPress={onOverridePress}
|
||||
>
|
||||
<div className={styles.manualDownloadContent}>
|
||||
<Icon
|
||||
className={styles.interactiveIcon}
|
||||
name={icons.INTERACTIVE}
|
||||
size={12}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={styles.downloadIcon}
|
||||
name={icons.CIRCLE_DOWN}
|
||||
size={10}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{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}</li>;
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<Link to={infoUrl} title={title}>
|
||||
<div>{title}</div>
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{historyGrabbedData?.date && !historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DEFAULT}
|
||||
title={`${translate('Grabbed')}: ${formatDateTime(
|
||||
historyGrabbedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Failed')}: ${formatDateTime(
|
||||
historyFailedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{blocklistData?.date ? (
|
||||
<Icon
|
||||
className={
|
||||
historyGrabbedData || historyFailedData ? styles.blocklist : ''
|
||||
}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Blocklisted')}: ${formatDateTime(
|
||||
blocklistData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.size}>{formatBytes(size)}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.peers}>
|
||||
{protocol === 'torrent' ? (
|
||||
<Peers seeders={seeders} leechers={leechers} />
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.languages}>
|
||||
<MovieLanguage languages={languages} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<MovieQuality quality={quality} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormat}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{indexerFlags.length ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={
|
||||
<ul>
|
||||
{indexerFlags.map((flag, index) => {
|
||||
return <li key={index}>{flag}</li>;
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('GrabRelease')}
|
||||
message={translate('GrabReleaseMessageText', { title })}
|
||||
confirmLabel={translate('Grab')}
|
||||
onConfirm={onGrabConfirm}
|
||||
onCancel={onGrabCancel}
|
||||
/>
|
||||
|
||||
<OverrideMatchModal
|
||||
isOpen={isOverrideModalOpen}
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
movieId={mappedMovieId}
|
||||
languages={languages}
|
||||
quality={quality}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onOverrideModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveSearchRow;
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
|
||||
|
||||
interface SelectDownloadClientModalProps {
|
||||
isOpen: boolean;
|
||||
protocol: DownloadProtocol;
|
||||
modalTitle: string;
|
||||
onDownloadClientSelect(downloadClientId: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
|
||||
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||
<SelectDownloadClientModalContent
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientModal;
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientRow from './SelectDownloadClientRow';
|
||||
|
||||
interface SelectDownloadClientModalContentProps {
|
||||
protocol: DownloadProtocol;
|
||||
modalTitle: string;
|
||||
onDownloadClientSelect(downloadClientId: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectDownloadClientModalContent(
|
||||
props: SelectDownloadClientModalContentProps
|
||||
) {
|
||||
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('SelectDownloadClientModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('DownloadClientsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
<Form>
|
||||
{items.map((downloadClient) => {
|
||||
const { id, name, priority } = downloadClient;
|
||||
|
||||
return (
|
||||
<SelectDownloadClientRow
|
||||
key={id}
|
||||
id={id}
|
||||
name={name}
|
||||
priority={priority}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientModalContent;
|
@ -0,0 +1,6 @@
|
||||
.downloadClient {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'downloadClient': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectDownloadClientRow.css';
|
||||
|
||||
interface SelectSeasonRowProps {
|
||||
id: number;
|
||||
name: string;
|
||||
priority: number;
|
||||
onDownloadClientSelect(downloadClientId: number): unknown;
|
||||
}
|
||||
|
||||
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
|
||||
const { id, name, priority, onDownloadClientSelect } = props;
|
||||
|
||||
const onSeasonSelectWrapper = useCallback(() => {
|
||||
onDownloadClientSelect(id);
|
||||
}, [id, onDownloadClientSelect]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.downloadClient}
|
||||
component="div"
|
||||
onPress={onSeasonSelectWrapper}
|
||||
>
|
||||
<div>{name}</div>
|
||||
<div>{translate('PrioritySettings', { priority })}</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientRow;
|
@ -0,0 +1,17 @@
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
margin: -2px 0;
|
||||
width: 100%;
|
||||
outline: 2px dashed var(--dangerColor);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.optional {
|
||||
outline: 2px dashed var(--gray);
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'link': string;
|
||||
'optional': string;
|
||||
'placeholder': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,35 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './OverrideMatchData.css';
|
||||
|
||||
interface OverrideMatchDataProps {
|
||||
value?: string | number | JSX.Element | JSX.Element[];
|
||||
isDisabled?: boolean;
|
||||
isOptional?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function OverrideMatchData(props: OverrideMatchDataProps) {
|
||||
const { value, isDisabled = false, isOptional, onPress } = props;
|
||||
|
||||
return (
|
||||
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
|
||||
{(value == null || (Array.isArray(value) && value.length === 0)) &&
|
||||
!isDisabled ? (
|
||||
<span
|
||||
className={classNames(
|
||||
styles.placeholder,
|
||||
isOptional && styles.optional
|
||||
)}
|
||||
>
|
||||
|
||||
</span>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchData;
|
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||
|
||||
interface OverrideMatchModalProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
indexerId: number;
|
||||
guid: string;
|
||||
movieId?: number;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function OverrideMatchModal(props: OverrideMatchModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
indexerId,
|
||||
guid,
|
||||
movieId,
|
||||
languages,
|
||||
quality,
|
||||
protocol,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||
<OverrideMatchModalContent
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
movieId={movieId}
|
||||
languages={languages}
|
||||
quality={quality}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchModal;
|
@ -0,0 +1,49 @@
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-right: 20px;
|
||||
color: var(--dangerColor);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttons': string;
|
||||
'error': string;
|
||||
'footer': string;
|
||||
'item': string;
|
||||
'label': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,299 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import Language from 'Language/Language';
|
||||
import Movie from 'Movie/Movie';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import { createMovieSelectorForHook } from 'Store/Selectors/createMovieSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||
import OverrideMatchData from './OverrideMatchData';
|
||||
import styles from './OverrideMatchModalContent.css';
|
||||
|
||||
type SelectType =
|
||||
| 'select'
|
||||
| 'movie'
|
||||
| 'quality'
|
||||
| 'language'
|
||||
| 'downloadClient';
|
||||
|
||||
interface OverrideMatchModalContentProps {
|
||||
indexerId: number;
|
||||
title: string;
|
||||
guid: string;
|
||||
movieId?: number;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
||||
const modalTitle = translate('ManualGrab');
|
||||
const {
|
||||
indexerId,
|
||||
title,
|
||||
guid,
|
||||
protocol,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const [movieId, setMovieId] = useState(props.movieId);
|
||||
const [languages, setLanguages] = useState(props.languages);
|
||||
const [quality, setQuality] = useState(props.quality);
|
||||
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
);
|
||||
const previousIsGrabbing = usePrevious(isGrabbing);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const movie: Movie | undefined = useSelector(
|
||||
createMovieSelectorForHook(movieId)
|
||||
);
|
||||
const { items: downloadClients } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
const onSelectModalClose = useCallback(() => {
|
||||
setSelectModalOpen(null);
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSelectMoviePress = useCallback(() => {
|
||||
setSelectModalOpen('movie');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onMovieSelect = useCallback(
|
||||
(m: Movie) => {
|
||||
setMovieId(m.id);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setMovieId, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectQualityPress = useCallback(() => {
|
||||
setSelectModalOpen('quality');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onQualitySelect = useCallback(
|
||||
(quality: QualityModel) => {
|
||||
setQuality(quality);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setQuality, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectLanguagesPress = useCallback(() => {
|
||||
setSelectModalOpen('language');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onLanguagesSelect = useCallback(
|
||||
(languages: Language[]) => {
|
||||
setLanguages(languages);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setLanguages, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectDownloadClientPress = useCallback(() => {
|
||||
setSelectModalOpen('downloadClient');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onDownloadClientSelect = useCallback(
|
||||
(downloadClientId: number) => {
|
||||
setDownloadClientId(downloadClientId);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setDownloadClientId, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onGrabPress = useCallback(() => {
|
||||
if (!movieId) {
|
||||
setError(translate('OverrideGrabNoMovie'));
|
||||
return;
|
||||
} else if (!quality) {
|
||||
setError(translate('OverrideGrabNoQuality'));
|
||||
return;
|
||||
} else if (!languages.length) {
|
||||
setError(translate('OverrideGrabNoLanguage'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
grabRelease({
|
||||
indexerId,
|
||||
guid,
|
||||
movieId,
|
||||
quality,
|
||||
languages,
|
||||
downloadClientId,
|
||||
shouldOverride: true,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
indexerId,
|
||||
guid,
|
||||
movieId,
|
||||
quality,
|
||||
languages,
|
||||
downloadClientId,
|
||||
setError,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGrabbing && previousIsGrabbing) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isGrabbing, previousIsGrabbing, onModalClose]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch(fetchDownloadClients());
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('OverrideGrabModalTitle', { title })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('Movie')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={movie?.title}
|
||||
onPress={onSelectMoviePress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('Quality')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
<MovieQuality className={styles.label} quality={quality} />
|
||||
}
|
||||
onPress={onSelectQualityPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('Languages')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
<MovieLanguage
|
||||
className={styles.label}
|
||||
languages={languages}
|
||||
/>
|
||||
}
|
||||
onPress={onSelectLanguagesPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{downloadClients.length > 1 ? (
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('DownloadClient')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
downloadClients.find(
|
||||
(downloadClient) => downloadClient.id === downloadClientId
|
||||
)?.name ?? translate('Default')
|
||||
}
|
||||
onPress={onSelectDownloadClientPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.error}>{error || grabError}</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isGrabbing}
|
||||
error={grabError}
|
||||
onPress={onGrabPress}
|
||||
>
|
||||
{translate('GrabRelease')}
|
||||
</SpinnerErrorButton>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectMovieModal
|
||||
isOpen={selectModalOpen === 'movie'}
|
||||
modalTitle={modalTitle}
|
||||
onMovieSelect={onMovieSelect}
|
||||
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}
|
||||
/>
|
||||
|
||||
<SelectDownloadClientModal
|
||||
isOpen={selectModalOpen === 'downloadClient'}
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchModalContent;
|
@ -0,0 +1,22 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
|
||||
export default function createEnabledDownloadClientsSelector(
|
||||
protocol: DownloadProtocol
|
||||
) {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||
(downloadClients: DownloadClientAppState) => {
|
||||
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||
|
||||
const clients = items.filter(
|
||||
(item) => item.protocol === protocol && item.enable
|
||||
);
|
||||
|
||||
return { isFetching, isPopulated, error, items: clients };
|
||||
}
|
||||
);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface MovieBlocklist extends ModelBase {
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default MovieBlocklist;
|
@ -0,0 +1,7 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface MovieHistory extends ModelBase {
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default MovieHistory;
|
Loading…
Reference in new issue