mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Option to select download client when multiple of the same type are configured
(cherry picked from commit 07f0fbf9a51d54e44681fd0f74df4e048bff561a)
This commit is contained in:
@@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
titleClassName,
|
titleClassName,
|
||||||
descriptionClassName,
|
descriptionClassName,
|
||||||
title,
|
title,
|
||||||
@@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<DescriptionListItemTitle
|
<DescriptionListItemTitle
|
||||||
className={titleClassName}
|
className={titleClassName}
|
||||||
>
|
>
|
||||||
@@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
DescriptionListItem.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
titleClassName: PropTypes.string,
|
titleClassName: PropTypes.string,
|
||||||
descriptionClassName: PropTypes.string,
|
descriptionClassName: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
enum DownloadProtocol {
|
||||||
|
Unknown = 'unknown',
|
||||||
|
Usenet = 'usenet',
|
||||||
|
Torrent = 'torrent',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadProtocol;
|
@@ -43,6 +43,7 @@ import {
|
|||||||
faChevronCircleRight as fasChevronCircleRight,
|
faChevronCircleRight as fasChevronCircleRight,
|
||||||
faChevronCircleUp as fasChevronCircleUp,
|
faChevronCircleUp as fasChevronCircleUp,
|
||||||
faCircle as fasCircle,
|
faCircle as fasCircle,
|
||||||
|
faCircleDown as fasCircleDown,
|
||||||
faCloud as fasCloud,
|
faCloud as fasCloud,
|
||||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||||
faCog as fasCog,
|
faCog as fasCog,
|
||||||
@@ -141,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
|||||||
export const CHECK_CIRCLE = fasCheckCircle;
|
export const CHECK_CIRCLE = fasCheckCircle;
|
||||||
export const CHECK_SQUARE = fasSquareCheck;
|
export const CHECK_SQUARE = fasSquareCheck;
|
||||||
export const CIRCLE = fasCircle;
|
export const CIRCLE = fasCircle;
|
||||||
|
export const CIRCLE_DOWN = fasCircleDown;
|
||||||
export const CIRCLE_OUTLINE = farCircle;
|
export const CIRCLE_OUTLINE = farCircle;
|
||||||
export const CLEAR = fasTrashAlt;
|
export const CLEAR = fasTrashAlt;
|
||||||
export const CLIPBOARD = fasCopy;
|
export const CLIPBOARD = fasCopy;
|
||||||
|
@@ -47,3 +47,42 @@ $hoverScale: 1.05;
|
|||||||
right: 0;
|
right: 0;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.downloadLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
color: var(--textColor);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manualDownloadContent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
height: 20.39px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 20.39px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--iconButtonHoverColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactiveIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 8px;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@@ -4,9 +4,13 @@ interface CssExports {
|
|||||||
'actions': string;
|
'actions': string;
|
||||||
'container': string;
|
'container': string;
|
||||||
'content': string;
|
'content': string;
|
||||||
|
'downloadIcon': string;
|
||||||
|
'downloadLink': string;
|
||||||
'indexerRow': string;
|
'indexerRow': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'infoRow': string;
|
'infoRow': string;
|
||||||
|
'interactiveIcon': string;
|
||||||
|
'manualDownloadContent': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
'titleRow': string;
|
'titleRow': string;
|
||||||
}
|
}
|
||||||
|
@@ -1,234 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
|
||||||
import CategoryLabel from 'Search/Table/CategoryLabel';
|
|
||||||
import Peers from 'Search/Table/Peers';
|
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './SearchIndexOverview.css';
|
|
||||||
|
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
|
||||||
|
|
||||||
function getContentHeight(rowHeight, isSmallScreen) {
|
|
||||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
|
||||||
|
|
||||||
return rowHeight - padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
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('AddedToDownloadClient');
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('AddToDownloadClient');
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchIndexOverview extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
infoUrl,
|
|
||||||
protocol,
|
|
||||||
downloadUrl,
|
|
||||||
magnetUrl,
|
|
||||||
categories,
|
|
||||||
seeders,
|
|
||||||
leechers,
|
|
||||||
indexerFlags,
|
|
||||||
size,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
indexer,
|
|
||||||
rowHeight,
|
|
||||||
isSmallScreen,
|
|
||||||
isGrabbed,
|
|
||||||
isGrabbing,
|
|
||||||
grabError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.info} style={{ height: contentHeight }}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
<Link
|
|
||||||
to={infoUrl}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<TextTruncate
|
|
||||||
line={2}
|
|
||||||
text={title}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={getDownloadKind(isGrabbed, grabError)}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadUrl || magnetUrl ?
|
|
||||||
<IconButton
|
|
||||||
name={icons.SAVE}
|
|
||||||
title={translate('Save')}
|
|
||||||
to={downloadUrl ?? magnetUrl}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={styles.indexerRow}>
|
|
||||||
{indexer}
|
|
||||||
</div>
|
|
||||||
<div className={styles.infoRow}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Label>
|
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<CategoryLabel
|
|
||||||
categories={categories}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexerFlags.length ?
|
|
||||||
indexerFlags
|
|
||||||
.sort((a, b) => a.localeCompare(b))
|
|
||||||
.map((flag, index) => {
|
|
||||||
return (
|
|
||||||
<Label key={index} kind={kinds.INFO}>
|
|
||||||
{titleCase(flag)}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}) :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchIndexOverview.propTypes = {
|
|
||||||
guid: PropTypes.string.isRequired,
|
|
||||||
categories: PropTypes.arrayOf(PropTypes.object).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,
|
|
||||||
downloadUrl: PropTypes.string,
|
|
||||||
magnetUrl: PropTypes.string,
|
|
||||||
indexerId: PropTypes.number.isRequired,
|
|
||||||
indexer: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
files: PropTypes.number,
|
|
||||||
grabs: PropTypes.number,
|
|
||||||
seeders: PropTypes.number,
|
|
||||||
leechers: PropTypes.number,
|
|
||||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
rowHeight: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchIndexOverview.defaultProps = {
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchIndexOverview;
|
|
262
frontend/src/Search/Mobile/SearchIndexOverview.tsx
Normal file
262
frontend/src/Search/Mobile/SearchIndexOverview.tsx
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||||
|
import { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
|
||||||
|
import CategoryLabel from 'Search/Table/CategoryLabel';
|
||||||
|
import Peers from 'Search/Table/Peers';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './SearchIndexOverview.css';
|
||||||
|
|
||||||
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
|
const columnPaddingSmallScreen = parseInt(
|
||||||
|
dimensions.movieIndexColumnPaddingSmallScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
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('AddedToDownloadClient');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddToDownloadClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchIndexOverviewProps {
|
||||||
|
guid: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
age: number;
|
||||||
|
ageHours: number;
|
||||||
|
ageMinutes: number;
|
||||||
|
publishDate: string;
|
||||||
|
title: string;
|
||||||
|
infoUrl: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
magnetUrl?: string;
|
||||||
|
indexerId: number;
|
||||||
|
indexer: string;
|
||||||
|
categories: IndexerCategory[];
|
||||||
|
size: number;
|
||||||
|
seeders?: number;
|
||||||
|
leechers?: number;
|
||||||
|
indexerFlags: string[];
|
||||||
|
isGrabbing: boolean;
|
||||||
|
isGrabbed: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
longDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
rowHeight: number;
|
||||||
|
isSmallScreen: boolean;
|
||||||
|
onGrabPress(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchIndexOverview(props: SearchIndexOverviewProps) {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
protocol,
|
||||||
|
categories,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishDate,
|
||||||
|
title,
|
||||||
|
infoUrl,
|
||||||
|
downloadUrl,
|
||||||
|
magnetUrl,
|
||||||
|
indexer,
|
||||||
|
size,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
indexerFlags = [],
|
||||||
|
isGrabbing = false,
|
||||||
|
isGrabbed = false,
|
||||||
|
grabError,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
rowHeight,
|
||||||
|
isSmallScreen,
|
||||||
|
onGrabPress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPressWrapper = useCallback(() => {
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
});
|
||||||
|
}, [guid, indexerId, onGrabPress]);
|
||||||
|
|
||||||
|
const onOverridePress = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(true);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const onOverrideModalClose = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(false);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const contentHeight = useMemo(() => {
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
|
||||||
|
return rowHeight - padding;
|
||||||
|
}, [rowHeight, isSmallScreen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.info} style={{ height: contentHeight }}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
<Link to={infoUrl} title={title}>
|
||||||
|
<TextTruncate line={2} text={title} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={getDownloadKind(isGrabbed, grabError)}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<Link
|
||||||
|
className={styles.manualDownloadContent}
|
||||||
|
title={translate('OverrideAndAddToDownloadClient')}
|
||||||
|
onPress={onOverridePress}
|
||||||
|
>
|
||||||
|
<div className={styles.manualDownloadContent}>
|
||||||
|
<Icon
|
||||||
|
className={styles.interactiveIcon}
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
size={11}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
className={styles.downloadIcon}
|
||||||
|
name={icons.CIRCLE_DOWN}
|
||||||
|
size={9}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadUrl || magnetUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
to={downloadUrl ?? magnetUrl}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.indexerRow}>{indexer}</div>
|
||||||
|
<div className={styles.infoRow}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
|
||||||
|
{protocol === 'torrent' && (
|
||||||
|
<Peers seeders={seeders} leechers={leechers} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Label>{formatBytes(size)}</Label>
|
||||||
|
|
||||||
|
<Label
|
||||||
|
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<CategoryLabel categories={categories} />
|
||||||
|
|
||||||
|
{indexerFlags.length
|
||||||
|
? indexerFlags
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map((flag, index) => {
|
||||||
|
return (
|
||||||
|
<Label key={index} kind={kinds.INFO}>
|
||||||
|
{titleCase(flag)}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onOverrideModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchIndexOverview;
|
@@ -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);
|
||||||
|
}
|
7
frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts
vendored
Normal file
7
frontend/src/Search/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts
vendored
Normal file
@@ -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;
|
17
frontend/src/Search/OverrideMatch/OverrideMatchData.css
Normal file
17
frontend/src/Search/OverrideMatch/OverrideMatchData.css
Normal file
@@ -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);
|
||||||
|
}
|
9
frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts
vendored
Normal file
9
frontend/src/Search/OverrideMatch/OverrideMatchData.css.d.ts
vendored
Normal file
@@ -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;
|
35
frontend/src/Search/OverrideMatch/OverrideMatchData.tsx
Normal file
35
frontend/src/Search/OverrideMatch/OverrideMatchData.tsx
Normal file
@@ -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;
|
45
frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx
Normal file
45
frontend/src/Search/OverrideMatch/OverrideMatchModal.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||||
|
|
||||||
|
interface OverrideMatchModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
indexerId: number;
|
||||||
|
guid: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideMatchModal(props: OverrideMatchModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
protocol,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||||
|
<OverrideMatchModalContent
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
11
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts
vendored
Normal file
11
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.css.d.ts
vendored
Normal file
@@ -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;
|
150
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx
Normal file
150
frontend/src/Search/OverrideMatch/OverrideMatchModalContent.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
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 { grabRelease } from 'Store/Actions/releaseActions';
|
||||||
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||||
|
import OverrideMatchData from './OverrideMatchData';
|
||||||
|
import styles from './OverrideMatchModalContent.css';
|
||||||
|
|
||||||
|
type SelectType = 'select' | 'downloadClient';
|
||||||
|
|
||||||
|
interface OverrideMatchModalContentProps {
|
||||||
|
indexerId: number;
|
||||||
|
title: string;
|
||||||
|
guid: string;
|
||||||
|
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 [downloadClientId, setDownloadClientId] = useState<number | null>(null);
|
||||||
|
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const previousIsGrabbing = usePrevious(isGrabbing);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectModalClose = useCallback(() => {
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onSelectDownloadClientPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('downloadClient');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onDownloadClientSelect = useCallback(
|
||||||
|
(downloadClientId: number) => {
|
||||||
|
setDownloadClientId(downloadClientId);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setDownloadClientId, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
grabRelease({
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
downloadClientId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [indexerId, guid, downloadClientId, 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>
|
||||||
|
{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}>{grabError}</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
error={grabError}
|
||||||
|
onPress={onGrabPress}
|
||||||
|
>
|
||||||
|
{translate('GrabRelease')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<SelectDownloadClientModal
|
||||||
|
isOpen={selectModalOpen === 'downloadClient'}
|
||||||
|
protocol={protocol}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onDownloadClientSelect={onDownloadClientSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OverrideMatchModalContent;
|
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
|
import { bulkGrabReleases, cancelFetchReleases, clearReleases, fetchReleases, setReleasesFilter, setReleasesSort, setReleasesTableOption } from 'Store/Actions/releaseActions';
|
||||||
|
import { fetchDownloadClients } from 'Store/Actions/Settings/downloadClients';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
|
import createReleaseClientSideCollectionItemsSelector from 'Store/Selectors/createReleaseClientSideCollectionItemsSelector';
|
||||||
import SearchIndex from './SearchIndex';
|
import SearchIndex from './SearchIndex';
|
||||||
@@ -55,12 +56,20 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
|
|
||||||
dispatchClearReleases() {
|
dispatchClearReleases() {
|
||||||
dispatch(clearReleases());
|
dispatch(clearReleases());
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchFetchDownloadClients() {
|
||||||
|
dispatch(fetchDownloadClients());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchIndexConnector extends Component {
|
class SearchIndexConnector extends Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchDownloadClients();
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.props.dispatchCancelFetchReleases();
|
this.props.dispatchCancelFetchReleases();
|
||||||
this.props.dispatchClearReleases();
|
this.props.dispatchClearReleases();
|
||||||
@@ -85,6 +94,7 @@ SearchIndexConnector.propTypes = {
|
|||||||
onBulkGrabPress: PropTypes.func.isRequired,
|
onBulkGrabPress: PropTypes.func.isRequired,
|
||||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||||
dispatchClearReleases: PropTypes.func.isRequired,
|
dispatchClearReleases: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchDownloadClients: PropTypes.func.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object)
|
items: PropTypes.arrayOf(PropTypes.object)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
|
|
||||||
function createReleaseSelector() {
|
function createReleaseSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
@@ -37,10 +36,6 @@ function createMapStateToProps() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchExecuteCommand: executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class SearchIndexItemConnector extends Component {
|
class SearchIndexItemConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -71,4 +66,4 @@ SearchIndexItemConnector.propTypes = {
|
|||||||
component: PropTypes.elementType.isRequired
|
component: PropTypes.elementType.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(SearchIndexItemConnector);
|
export default connect(createMapStateToProps, null)(SearchIndexItemConnector);
|
||||||
|
@@ -67,3 +67,33 @@
|
|||||||
|
|
||||||
color: var(--textColor);
|
color: var(--textColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manualDownloadContent {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 2px;
|
||||||
|
width: 22px;
|
||||||
|
height: 20.39px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 20.39px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--iconButtonHoverColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interactiveIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 0;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 7px;
|
||||||
|
left: 8px;
|
||||||
|
/* width: 100%; */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
@@ -6,12 +6,15 @@ interface CssExports {
|
|||||||
'category': string;
|
'category': string;
|
||||||
'cell': string;
|
'cell': string;
|
||||||
'checkInput': string;
|
'checkInput': string;
|
||||||
|
'downloadIcon': string;
|
||||||
'downloadLink': string;
|
'downloadLink': string;
|
||||||
'externalLinks': string;
|
'externalLinks': string;
|
||||||
'files': string;
|
'files': string;
|
||||||
'grabs': string;
|
'grabs': string;
|
||||||
'indexer': string;
|
'indexer': string;
|
||||||
'indexerFlags': string;
|
'indexerFlags': string;
|
||||||
|
'interactiveIcon': string;
|
||||||
|
'manualDownloadContent': string;
|
||||||
'peers': string;
|
'peers': string;
|
||||||
'protocol': string;
|
'protocol': string;
|
||||||
'size': string;
|
'size': string;
|
||||||
|
@@ -1,431 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CategoryLabel from './CategoryLabel';
|
|
||||||
import Peers from './Peers';
|
|
||||||
import ReleaseLinks from './ReleaseLinks';
|
|
||||||
import styles from './SearchIndexRow.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('AddedToDownloadClient');
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translate('AddToDownloadClient');
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchIndexRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isConfirmGrabModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
indexerId,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onGrabPress({
|
|
||||||
guid,
|
|
||||||
indexerId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
const {
|
|
||||||
downloadUrl,
|
|
||||||
fileName,
|
|
||||||
onSavePress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSavePress({
|
|
||||||
downloadUrl,
|
|
||||||
fileName
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
guid,
|
|
||||||
protocol,
|
|
||||||
downloadUrl,
|
|
||||||
magnetUrl,
|
|
||||||
categories,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
publishDate,
|
|
||||||
title,
|
|
||||||
infoUrl,
|
|
||||||
indexer,
|
|
||||||
size,
|
|
||||||
files,
|
|
||||||
grabs,
|
|
||||||
seeders,
|
|
||||||
leechers,
|
|
||||||
imdbId,
|
|
||||||
tmdbId,
|
|
||||||
tvdbId,
|
|
||||||
tvMazeId,
|
|
||||||
indexerFlags,
|
|
||||||
columns,
|
|
||||||
isGrabbing,
|
|
||||||
isGrabbed,
|
|
||||||
grabError,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
isSelected,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'select') {
|
|
||||||
return (
|
|
||||||
<VirtualTableSelectCell
|
|
||||||
inputClassName={styles.checkInput}
|
|
||||||
id={guid}
|
|
||||||
key={column.name}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={false}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'protocol') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'age') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
>
|
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'sortTitle') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
to={infoUrl}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{indexer}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'size') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'files') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{files}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'grabs') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{grabs}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'peers') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'category') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<CategoryLabel
|
|
||||||
categories={categories}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'indexerFlags') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!indexerFlags.length &&
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.FLAG}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('IndexerFlags')}
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
indexerFlags.map((flag, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
{titleCase(flag)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (column.name === 'actions') {
|
|
||||||
return (
|
|
||||||
<VirtualTableRowCell
|
|
||||||
key={column.name}
|
|
||||||
className={styles[column.name]}
|
|
||||||
>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={getDownloadKind(isGrabbed, grabError)}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadUrl ?
|
|
||||||
<IconButton
|
|
||||||
className={styles.downloadLink}
|
|
||||||
name={icons.SAVE}
|
|
||||||
title={translate('Save')}
|
|
||||||
onPress={this.onSavePress}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
magnetUrl ?
|
|
||||||
<IconButton
|
|
||||||
className={styles.downloadLink}
|
|
||||||
name={icons.MAGNET}
|
|
||||||
title={translate('Open')}
|
|
||||||
to={magnetUrl}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
imdbId || tmdbId || tvdbId || tvMazeId ? (
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
className={styles.externalLinks}
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={12}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('Links')}
|
|
||||||
body={
|
|
||||||
<ReleaseLinks
|
|
||||||
categories={categories}
|
|
||||||
imdbId={imdbId}
|
|
||||||
tmdbId={tmdbId}
|
|
||||||
tvdbId={tvdbId}
|
|
||||||
tvMazeId={tvMazeId}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
kind={kinds.INVERSE}
|
|
||||||
position={tooltipPositions.TOP}
|
|
||||||
/>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchIndexRow.propTypes = {
|
|
||||||
guid: PropTypes.string.isRequired,
|
|
||||||
categories: PropTypes.arrayOf(PropTypes.object).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,
|
|
||||||
fileName: PropTypes.string.isRequired,
|
|
||||||
infoUrl: PropTypes.string.isRequired,
|
|
||||||
downloadUrl: PropTypes.string,
|
|
||||||
magnetUrl: PropTypes.string,
|
|
||||||
indexerId: PropTypes.number.isRequired,
|
|
||||||
indexer: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
files: PropTypes.number,
|
|
||||||
grabs: PropTypes.number,
|
|
||||||
seeders: PropTypes.number,
|
|
||||||
leechers: PropTypes.number,
|
|
||||||
imdbId: PropTypes.number,
|
|
||||||
tmdbId: PropTypes.number,
|
|
||||||
tvdbId: PropTypes.number,
|
|
||||||
tvMazeId: PropTypes.number,
|
|
||||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.string,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
SearchIndexRow.defaultProps = {
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SearchIndexRow;
|
|
395
frontend/src/Search/Table/SearchIndexRow.tsx
Normal file
395
frontend/src/Search/Table/SearchIndexRow.tsx
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||||
|
import { IndexerCategory } from 'Indexer/Indexer';
|
||||||
|
import OverrideMatchModal from 'Search/OverrideMatch/OverrideMatchModal';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CategoryLabel from './CategoryLabel';
|
||||||
|
import Peers from './Peers';
|
||||||
|
import ReleaseLinks from './ReleaseLinks';
|
||||||
|
import styles from './SearchIndexRow.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('AddedToDownloadClient');
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translate('AddToDownloadClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchIndexRowProps {
|
||||||
|
guid: string;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
age: number;
|
||||||
|
ageHours: number;
|
||||||
|
ageMinutes: number;
|
||||||
|
publishDate: string;
|
||||||
|
title: string;
|
||||||
|
fileName: string;
|
||||||
|
infoUrl: string;
|
||||||
|
downloadUrl?: string;
|
||||||
|
magnetUrl?: string;
|
||||||
|
indexerId: number;
|
||||||
|
indexer: string;
|
||||||
|
categories: IndexerCategory[];
|
||||||
|
size: number;
|
||||||
|
files?: number;
|
||||||
|
grabs?: number;
|
||||||
|
seeders?: number;
|
||||||
|
leechers?: number;
|
||||||
|
imdbId?: string;
|
||||||
|
tmdbId?: number;
|
||||||
|
tvdbId?: number;
|
||||||
|
tvMazeId?: number;
|
||||||
|
indexerFlags: string[];
|
||||||
|
isGrabbing: boolean;
|
||||||
|
isGrabbed: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
longDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
columns: Column[];
|
||||||
|
isSelected?: boolean;
|
||||||
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
|
onGrabPress(...args: unknown[]): void;
|
||||||
|
onSavePress(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchIndexRow(props: SearchIndexRowProps) {
|
||||||
|
const {
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
protocol,
|
||||||
|
categories,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishDate,
|
||||||
|
title,
|
||||||
|
fileName,
|
||||||
|
infoUrl,
|
||||||
|
downloadUrl,
|
||||||
|
magnetUrl,
|
||||||
|
indexer,
|
||||||
|
size,
|
||||||
|
files,
|
||||||
|
grabs,
|
||||||
|
seeders,
|
||||||
|
leechers,
|
||||||
|
imdbId,
|
||||||
|
tmdbId,
|
||||||
|
tvdbId,
|
||||||
|
tvMazeId,
|
||||||
|
indexerFlags = [],
|
||||||
|
isGrabbing = false,
|
||||||
|
isGrabbed = false,
|
||||||
|
grabError,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
columns,
|
||||||
|
isSelected,
|
||||||
|
onSelectedChange,
|
||||||
|
onGrabPress,
|
||||||
|
onSavePress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPressWrapper = useCallback(() => {
|
||||||
|
onGrabPress({
|
||||||
|
guid,
|
||||||
|
indexerId,
|
||||||
|
});
|
||||||
|
}, [guid, indexerId, onGrabPress]);
|
||||||
|
|
||||||
|
const onSavePressWrapper = useCallback(() => {
|
||||||
|
onSavePress({
|
||||||
|
downloadUrl,
|
||||||
|
fileName,
|
||||||
|
});
|
||||||
|
}, [downloadUrl, fileName, onSavePress]);
|
||||||
|
|
||||||
|
const onOverridePress = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(true);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
const onOverrideModalClose = useCallback(() => {
|
||||||
|
setIsOverrideModalOpen(false);
|
||||||
|
}, [setIsOverrideModalOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'select') {
|
||||||
|
return (
|
||||||
|
<VirtualTableSelectCell
|
||||||
|
inputClassName={styles.checkInput}
|
||||||
|
id={guid}
|
||||||
|
key={name}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={false}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'protocol') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'age') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sortTitle') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<Link to={infoUrl} title={title}>
|
||||||
|
<div>{title}</div>
|
||||||
|
</Link>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{indexer}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'files') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{files}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'grabs') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{grabs}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'peers') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{protocol === 'torrent' && (
|
||||||
|
<Peers seeders={seeders} leechers={leechers} />
|
||||||
|
)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'category') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<CategoryLabel categories={categories} />
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexerFlags') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{!!indexerFlags.length && (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||||
|
title={translate('IndexerFlags')}
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{indexerFlags.map((flag, index) => {
|
||||||
|
return <li key={index}>{titleCase(flag)}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={getDownloadKind(isGrabbed, grabError)}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<Link
|
||||||
|
className={styles.manualDownloadContent}
|
||||||
|
title={translate('OverrideAndAddToDownloadClient')}
|
||||||
|
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>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.SAVE}
|
||||||
|
title={translate('Save')}
|
||||||
|
onPress={onSavePressWrapper}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{magnetUrl ? (
|
||||||
|
<IconButton
|
||||||
|
className={styles.downloadLink}
|
||||||
|
name={icons.MAGNET}
|
||||||
|
title={translate('Open')}
|
||||||
|
to={magnetUrl}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{imdbId || tmdbId || tvdbId || tvMazeId ? (
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.externalLinks}
|
||||||
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={12}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('Links')}
|
||||||
|
body={
|
||||||
|
<ReleaseLinks
|
||||||
|
categories={categories}
|
||||||
|
imdbId={imdbId}
|
||||||
|
tmdbId={tmdbId}
|
||||||
|
tvdbId={tvdbId}
|
||||||
|
tvMazeId={tvMazeId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
protocol={protocol}
|
||||||
|
isGrabbing={isGrabbing}
|
||||||
|
grabError={grabError}
|
||||||
|
onModalClose={onOverrideModalClose}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchIndexRow;
|
@@ -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 };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
@@ -7,10 +7,10 @@ function formatBytes(input) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesize(size, {
|
return `${filesize(size, {
|
||||||
base: 2,
|
base: 2,
|
||||||
round: 1
|
round: 1
|
||||||
});
|
})}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default formatBytes;
|
export default formatBytes;
|
||||||
|
@@ -143,6 +143,7 @@
|
|||||||
"DatabaseMigration": "Database Migration",
|
"DatabaseMigration": "Database Migration",
|
||||||
"Date": "Date",
|
"Date": "Date",
|
||||||
"Dates": "Dates",
|
"Dates": "Dates",
|
||||||
|
"Default": "Default",
|
||||||
"DefaultCategory": "Default Category",
|
"DefaultCategory": "Default Category",
|
||||||
"DefaultNameCopiedProfile": "{name} - Copy",
|
"DefaultNameCopiedProfile": "{name} - Copy",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
@@ -286,6 +287,7 @@
|
|||||||
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics, and updates",
|
"GeneralSettingsSummary": "Port, SSL, username/password, proxy, analytics, and updates",
|
||||||
"Genre": "Genre",
|
"Genre": "Genre",
|
||||||
"GoToApplication": "Go to application",
|
"GoToApplication": "Go to application",
|
||||||
|
"GrabRelease": "Grab Release",
|
||||||
"GrabReleases": "Grab Release(s)",
|
"GrabReleases": "Grab Release(s)",
|
||||||
"GrabTitle": "Grab Title",
|
"GrabTitle": "Grab Title",
|
||||||
"Grabbed": "Grabbed",
|
"Grabbed": "Grabbed",
|
||||||
@@ -448,6 +450,7 @@
|
|||||||
"ManageClients": "Manage Clients",
|
"ManageClients": "Manage Clients",
|
||||||
"ManageDownloadClients": "Manage Download Clients",
|
"ManageDownloadClients": "Manage Download Clients",
|
||||||
"Manual": "Manual",
|
"Manual": "Manual",
|
||||||
|
"ManualGrab": "Manual Grab",
|
||||||
"MappedCategories": "Mapped Categories",
|
"MappedCategories": "Mapped Categories",
|
||||||
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
|
"MappedDrivesRunningAsService": "Mapped network drives are not available when running as a Windows Service. Please see the FAQ for more information",
|
||||||
"MassEditor": "Mass Editor",
|
"MassEditor": "Mass Editor",
|
||||||
@@ -508,9 +511,12 @@
|
|||||||
"OnHealthIssueHelpText": "On Health Issue",
|
"OnHealthIssueHelpText": "On Health Issue",
|
||||||
"OnHealthRestored": "On Health Restored",
|
"OnHealthRestored": "On Health Restored",
|
||||||
"OnHealthRestoredHelpText": "On Health Restored",
|
"OnHealthRestoredHelpText": "On Health Restored",
|
||||||
|
"Open": "Open",
|
||||||
"OpenBrowserOnStart": "Open browser on start",
|
"OpenBrowserOnStart": "Open browser on start",
|
||||||
"OpenThisModal": "Open This Modal",
|
"OpenThisModal": "Open This Modal",
|
||||||
"Options": "Options",
|
"Options": "Options",
|
||||||
|
"OverrideAndAddToDownloadClient": "Override and add to download client",
|
||||||
|
"OverrideGrabModalTitle": "Override and Grab - {title}",
|
||||||
"PackSeedTime": "Pack Seed Time",
|
"PackSeedTime": "Pack Seed Time",
|
||||||
"PackSeedTimeHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
|
"PackSeedTimeHelpText": "The time a pack (season or discography) torrent should be seeded before stopping, empty is app's default",
|
||||||
"PackageVersion": "Package Version",
|
"PackageVersion": "Package Version",
|
||||||
@@ -527,6 +533,7 @@
|
|||||||
"PortNumber": "Port Number",
|
"PortNumber": "Port Number",
|
||||||
"Presets": "Presets",
|
"Presets": "Presets",
|
||||||
"Priority": "Priority",
|
"Priority": "Priority",
|
||||||
|
"PrioritySettings": "Priority: {priority}",
|
||||||
"Privacy": "Privacy",
|
"Privacy": "Privacy",
|
||||||
"Private": "Private",
|
"Private": "Private",
|
||||||
"Protocol": "Protocol",
|
"Protocol": "Protocol",
|
||||||
@@ -612,6 +619,7 @@
|
|||||||
"SeedTimeHelpText": "The time a torrent should be seeded before stopping, empty is app's default",
|
"SeedTimeHelpText": "The time a torrent should be seeded before stopping, empty is app's default",
|
||||||
"Seeders": "Seeders",
|
"Seeders": "Seeders",
|
||||||
"SelectAll": "Select All",
|
"SelectAll": "Select All",
|
||||||
|
"SelectDownloadClientModalTitle": "{modalTitle} - Select Download Client",
|
||||||
"SelectIndexers": "Select Indexers",
|
"SelectIndexers": "Select Indexers",
|
||||||
"SelectedCountOfCountReleases": "Selected {selectedCount} of {itemCount} releases",
|
"SelectedCountOfCountReleases": "Selected {selectedCount} of {itemCount} releases",
|
||||||
"SemiPrivate": "Semi-Private",
|
"SemiPrivate": "Semi-Private",
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
@@ -55,6 +56,9 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
return $"{Title}{extension}";
|
return $"{Title}{extension}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public int? DownloadClientId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReleaseResourceMapper
|
public static class ReleaseResourceMapper
|
||||||
|
@@ -74,7 +74,7 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, null);
|
await _downloadService.SendReportToClient(releaseInfo, source, host, indexerDef.Redirect, release.DownloadClientId);
|
||||||
}
|
}
|
||||||
catch (ReleaseDownloadException ex)
|
catch (ReleaseDownloadException ex)
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user