1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-11-09 04:22:30 +01:00

New: Add icons for search results to indicate if it has been previously grabbed, failed or is in the blacklist.

This commit is contained in:
Qstick 2020-09-11 22:17:36 -04:00
parent 2ee77aa0a4
commit 4ea9ded0de
8 changed files with 146 additions and 40 deletions

View File

@ -23,6 +23,7 @@ import {
faArrowCircleLeft as fasArrowCircleLeft, faArrowCircleLeft as fasArrowCircleLeft,
faArrowCircleRight as fasArrowCircleRight, faArrowCircleRight as fasArrowCircleRight,
faBackward as fasBackward, faBackward as fasBackward,
faBan as fasBan,
faBars as fasBars, faBars as fasBars,
faBolt as fasBolt, faBolt as fasBolt,
faBookmark as fasBookmark, faBookmark as fasBookmark,
@ -223,3 +224,4 @@ export const UNSAVED_SETTING = farDotCircle;
export const VIEW = fasEye; export const VIEW = fasEye;
export const WARNING = fasExclamationTriangle; export const WARNING = fasExclamationTriangle;
export const WIKI = fasBookReader; export const WIKI = fasBookReader;
export const BLACKLIST = fasBan;

View File

@ -6,7 +6,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { icons, sortDirections } from 'Helpers/Props'; import { icons, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import InteractiveSearchRow from './InteractiveSearchRow'; import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
import styles from './InteractiveSearchContent.css'; import styles from './InteractiveSearchContent.css';
const columns = [ const columns = [
@ -34,6 +34,13 @@ const columns = [
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{
name: 'history',
label: translate('History'),
isSortable: true,
fixedSortDirection: sortDirections.ASCENDING,
isVisible: true
},
{ {
name: 'size', name: 'size',
label: translate('Size'), label: translate('Size'),
@ -151,7 +158,7 @@ function InteractiveSearchContent(props) {
{ {
items.map((item) => { items.map((item) => {
return ( return (
<InteractiveSearchRow <InteractiveSearchRowConnector
key={item.guid} key={item.guid}
{...item} {...item}
searchPayload={searchPayload} searchPayload={searchPayload}

View File

@ -62,3 +62,13 @@
.title div { .title div {
@add-mixin truncate; @add-mixin truncate;
} }
.history {
composes: cell;
width: 75px;
}
.blacklist {
margin-left: 5px;
}

View File

@ -124,7 +124,10 @@ class InteractiveSearchRow extends Component {
isGrabbed, isGrabbed,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
grabError grabError,
historyGrabbedData,
historyFailedData,
blacklistData
} = this.props; } = this.props;
return ( return (
@ -157,6 +160,37 @@ class InteractiveSearchRow extends Component {
{indexer} {indexer}
</TableRowCell> </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 })}`}
/>
}
{
blacklistData?.date &&
<Icon
className={historyGrabbedData || historyFailedData ? styles.blacklist : ''}
name={icons.BLACKLIST}
kind={kinds.DANGER}
title={`${translate('Blacklisted')}: ${formatDateTime(blacklistData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
/>
}
</TableRowCell>
<TableRowCell className={styles.size}> <TableRowCell className={styles.size}>
{formatBytes(size)} {formatBytes(size)}
</TableRowCell> </TableRowCell>
@ -304,7 +338,10 @@ InteractiveSearchRow.propTypes = {
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
searchPayload: PropTypes.object.isRequired, searchPayload: PropTypes.object.isRequired,
onGrabPress: PropTypes.func.isRequired onGrabPress: PropTypes.func.isRequired,
historyFailedData: PropTypes.object,
historyGrabbedData: PropTypes.object,
blacklistData: PropTypes.object
}; };
InteractiveSearchRow.defaultProps = { InteractiveSearchRow.defaultProps = {

View File

@ -0,0 +1,62 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import InteractiveSearchRow from './InteractiveSearchRow';
function createMapStateToProps() {
return createSelector(
(state, { guid }) => guid,
(state) => state.movieHistory.items,
(state) => state.blacklist.items,
(guid, movieHistory, blacklist) => {
let blacklistData = {};
let historyFailedData = {};
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
if (historyGrabbedData) {
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
blacklistData = blacklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
}
return {
historyGrabbedData,
historyFailedData,
blacklistData
};
}
);
}
class InteractiveSearchRowConnector extends Component {
//
// Render
render() {
const {
historyGrabbedData,
historyFailedData,
blacklistData,
...otherProps
} = this.props;
return (
<InteractiveSearchRow
historyGrabbedData={historyGrabbedData}
historyFailedData={historyFailedData}
blacklistData={blacklistData}
{...otherProps}
/>
);
}
}
InteractiveSearchRowConnector.propTypes = {
historyGrabbedData: PropTypes.object,
historyFailedData: PropTypes.object,
blacklistData: PropTypes.object
};
export default connect(createMapStateToProps)(InteractiveSearchRowConnector);

View File

@ -5,11 +5,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import { fetchBlacklist } from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions'; import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions'; import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions'; import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions'; import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions'; import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchImportListSchema } from 'Store/Actions/settingsActions'; import { fetchImportListSchema } from 'Store/Actions/settingsActions';
@ -178,6 +180,12 @@ function createMapDispatchToProps(dispatch, props) {
dispatchClearMovieFiles() { dispatchClearMovieFiles() {
dispatch(clearMovieFiles()); dispatch(clearMovieFiles());
}, },
dispatchFetchMovieHistory({ movieId }) {
dispatch(fetchMovieHistory({ movieId }));
},
dispatchClearMovieHistory() {
dispatch(clearMovieHistory());
},
dispatchFetchMovieCredits({ movieId }) { dispatchFetchMovieCredits({ movieId }) {
dispatch(fetchMovieCredits({ movieId })); dispatch(fetchMovieCredits({ movieId }));
}, },
@ -213,6 +221,10 @@ function createMapDispatchToProps(dispatch, props) {
}, },
onGoToMovie(titleSlug) { onGoToMovie(titleSlug) {
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`)); dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
},
dispatchFetchBlacklist() {
// TODO: Allow for passing a movie id to fetch a single movie's blacklist data
dispatch(fetchBlacklist());
} }
}; };
} }
@ -266,15 +278,22 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id; const movieId = this.props.id;
this.props.dispatchFetchMovieFiles({ movieId }); this.props.dispatchFetchMovieFiles({ movieId });
this.props.dispatchFetchMovieHistory({ movieId });
this.props.dispatchFetchExtraFiles({ movieId }); this.props.dispatchFetchExtraFiles({ movieId });
this.props.dispatchFetchMovieCredits({ movieId }); this.props.dispatchFetchMovieCredits({ movieId });
this.props.dispatchFetchQueueDetails({ movieId }); this.props.dispatchFetchQueueDetails({ movieId });
this.props.dispatchFetchImportListSchema(); this.props.dispatchFetchImportListSchema();
this.props.dispatchFetchBlacklist();
}
repopulate = () => {
this.props.dispatchFetchBlacklist();
} }
unpopulate = () => { unpopulate = () => {
this.props.dispatchCancelFetchReleases(); this.props.dispatchCancelFetchReleases();
this.props.dispatchClearMovieFiles(); this.props.dispatchClearMovieFiles();
this.props.dispatchClearMovieHistory();
this.props.dispatchClearExtraFiles(); this.props.dispatchClearExtraFiles();
this.props.dispatchClearMovieCredits(); this.props.dispatchClearMovieCredits();
this.props.dispatchClearQueueDetails(); this.props.dispatchClearQueueDetails();
@ -331,6 +350,8 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
dispatchFetchMovieFiles: PropTypes.func.isRequired, dispatchFetchMovieFiles: PropTypes.func.isRequired,
dispatchClearMovieFiles: PropTypes.func.isRequired, dispatchClearMovieFiles: PropTypes.func.isRequired,
dispatchFetchMovieHistory: PropTypes.func.isRequired,
dispatchClearMovieHistory: PropTypes.func.isRequired,
dispatchFetchExtraFiles: PropTypes.func.isRequired, dispatchFetchExtraFiles: PropTypes.func.isRequired,
dispatchClearExtraFiles: PropTypes.func.isRequired, dispatchClearExtraFiles: PropTypes.func.isRequired,
dispatchFetchMovieCredits: PropTypes.func.isRequired, dispatchFetchMovieCredits: PropTypes.func.isRequired,
@ -342,6 +363,7 @@ MovieDetailsConnector.propTypes = {
dispatchClearQueueDetails: PropTypes.func.isRequired, dispatchClearQueueDetails: PropTypes.func.isRequired,
dispatchFetchImportListSchema: PropTypes.func.isRequired, dispatchFetchImportListSchema: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired, dispatchExecuteCommand: PropTypes.func.isRequired,
dispatchFetchBlacklist: PropTypes.func.isRequired,
onGoToMovie: PropTypes.func.isRequired onGoToMovie: PropTypes.func.isRequired
}; };

View File

@ -2,7 +2,7 @@ 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 { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions'; import { movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
import MovieHistoryTableContent from './MovieHistoryTableContent'; import MovieHistoryTableContent from './MovieHistoryTableContent';
function createMapStateToProps() { function createMapStateToProps() {
@ -15,44 +15,11 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
fetchMovieHistory,
clearMovieHistory,
movieHistoryMarkAsFailed movieHistoryMarkAsFailed
}; };
class MovieHistoryTableContentConnector extends Component { class MovieHistoryTableContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
movieId
} = this.props;
this.props.fetchMovieHistory({
movieId
});
}
componentDidUpdate(prevProps) {
const {
movieId
} = this.props;
// If the id has changed we need to clear the history
if (prevProps.movieId !== movieId) {
this.props.clearMovieHistory();
this.props.fetchMovieHistory({
movieId
});
}
}
componentWillUnmount() {
this.props.clearMovieHistory();
}
// //
// Listeners // Listeners
@ -82,8 +49,6 @@ class MovieHistoryTableContentConnector extends Component {
MovieHistoryTableContentConnector.propTypes = { MovieHistoryTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired, movieId: PropTypes.number.isRequired,
fetchMovieHistory: PropTypes.func.isRequired,
clearMovieHistory: PropTypes.func.isRequired,
movieHistoryMarkAsFailed: PropTypes.func.isRequired movieHistoryMarkAsFailed: PropTypes.func.isRequired
}; };

View File

@ -63,6 +63,7 @@
"BindAddress": "Bind Address", "BindAddress": "Bind Address",
"BindAddressHelpText": "Valid IP4 address or '*' for all interfaces", "BindAddressHelpText": "Valid IP4 address or '*' for all interfaces",
"Blacklist": "Blacklist", "Blacklist": "Blacklist",
"Blacklisted": "Blacklisted",
"BlacklistHelpText": "Prevents Radarr from automatically grabbing this movie again", "BlacklistHelpText": "Prevents Radarr from automatically grabbing this movie again",
"BlacklistRelease": "Blacklist Release", "BlacklistRelease": "Blacklist Release",
"Branch": "Branch", "Branch": "Branch",