mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-01-10 23:29:53 +02:00
Convert Interactive Search to TypeScript
This commit is contained in:
parent
546e9fd1d0
commit
0a0e03dca0
@ -8,6 +8,7 @@ import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import PathsAppState from './PathsAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import ReleasesAppState from './ReleasesAppState';
|
||||
import RootFolderAppState from './RootFolderAppState';
|
||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
@ -72,6 +73,7 @@ interface AppState {
|
||||
parse: ParseAppState;
|
||||
paths: PathsAppState;
|
||||
queue: QueueAppState;
|
||||
releases: ReleasesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
series: SeriesAppState;
|
||||
seriesIndex: SeriesIndexAppState;
|
||||
|
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Release from 'typings/Release';
|
||||
|
||||
interface ReleasesAppState
|
||||
extends AppSectionState<Release>,
|
||||
AppSectionFilterState<Release> {}
|
||||
|
||||
export default ReleasesAppState;
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
@ -9,6 +10,7 @@ interface Column {
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
fixedSortDirection?: SortDirection;
|
||||
isVisible: boolean;
|
||||
isModifiable?: boolean;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import EpisodeSearch from './EpisodeSearch';
|
||||
|
||||
@ -65,7 +65,7 @@ class EpisodeSearchConnector extends Component {
|
||||
|
||||
if (this.state.isInteractiveSearchOpen) {
|
||||
return (
|
||||
<InteractiveSearchConnector
|
||||
<InteractiveSearch
|
||||
type="episode"
|
||||
searchPayload={{ episodeId }}
|
||||
/>
|
||||
|
@ -1,234 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languageWeight',
|
||||
label: () => translate('Languages'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections')
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function InteractiveSearch(props) {
|
||||
const {
|
||||
searchPayload,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalReleasesCount,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
type,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onSortPress,
|
||||
onFilterSelect,
|
||||
onGrabPress
|
||||
} = props;
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
filterModalConnectorComponentProps={{ type }}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>
|
||||
{
|
||||
errorMessage ?
|
||||
<Fragment>
|
||||
{translate('InteractiveSearchResultsSeriesFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
|
||||
</Fragment> :
|
||||
translate('EpisodeSearchResultsLoadError')
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !totalReleasesCount ?
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoResultsFound')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!totalReleasesCount && isPopulated && !items.length ?
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('AllResultsAreHiddenByTheAppliedFilter')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onGrabPress={onGrabPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
totalReleasesCount !== items.length && !!items.length ?
|
||||
<div className={styles.filteredMessage}>
|
||||
{translate('SomeResultsAreHiddenByTheAppliedFilter')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearch.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalReleasesCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearch;
|
247
frontend/src/InteractiveSearch/InteractiveSearch.tsx
Normal file
247
frontend/src/InteractiveSearch/InteractiveSearch.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import ReleasesAppState from 'App/State/ReleasesAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType';
|
||||
import {
|
||||
fetchReleases,
|
||||
grabRelease,
|
||||
setEpisodeReleasesFilter,
|
||||
setReleasesSort,
|
||||
setSeasonReleasesFilter,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModal from './InteractiveSearchFilterModal';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languageWeight',
|
||||
label: () => translate('Languages'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections'),
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
interface InteractiveSearchProps {
|
||||
type: InteractiveSearchType;
|
||||
searchPayload: object;
|
||||
}
|
||||
|
||||
function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
totalItems,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
}: ReleasesAppState & ClientSideCollectionAppState = useSelector(
|
||||
createClientSideCollectionSelector('releases', `releases.${type}`)
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string) => {
|
||||
const action =
|
||||
type === 'episode' ? setEpisodeReleasesFilter : setSeasonReleasesFilter;
|
||||
|
||||
dispatch(action({ selectedFilterKey }));
|
||||
},
|
||||
[type, dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string, sortDirection: SortDirection) => {
|
||||
dispatch(setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleGrabPress = useCallback(
|
||||
(payload: object) => {
|
||||
dispatch(grabRelease(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// If search results are not yet isPopulated fetch them,
|
||||
// otherwise re-show the existing props.
|
||||
|
||||
if (!isPopulated) {
|
||||
dispatch(fetchReleases(searchPayload));
|
||||
}
|
||||
}, [isPopulated, searchPayload, dispatch]);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModal}
|
||||
filterModalConnectorComponentProps={{ type }}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<div>
|
||||
{errorMessage ? (
|
||||
<>
|
||||
{translate('InteractiveSearchResultsSeriesFailedErrorMessage', {
|
||||
message:
|
||||
errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1),
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
translate('EpisodeSearchResultsLoadError')
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && isPopulated && !totalItems ? (
|
||||
<Alert kind={kinds.INFO}>{translate('NoResultsFound')}</Alert>
|
||||
) : null}
|
||||
|
||||
{!!totalItems && isPopulated && !items.length ? (
|
||||
<Alert kind={kinds.WARNING}>
|
||||
{translate('AllResultsAreHiddenByTheAppliedFilter')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
onGrabPress={handleGrabPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{totalItems !== items.length && !!items.length ? (
|
||||
<div className={styles.filteredMessage}>
|
||||
{translate('SomeResultsAreHiddenByTheAppliedFilter')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveSearch;
|
@ -1,95 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import InteractiveSearch from './InteractiveSearch';
|
||||
|
||||
function createMapStateToProps(appState, { type }) {
|
||||
return createSelector(
|
||||
(state) => state.releases.items.length,
|
||||
createClientSideCollectionSelector('releases', `releases.${type}`),
|
||||
createUISettingsSelector(),
|
||||
(totalReleasesCount, releases, uiSettings) => {
|
||||
return {
|
||||
totalReleasesCount,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
...releases
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchReleases(payload) {
|
||||
dispatch(releaseActions.fetchReleases(payload));
|
||||
},
|
||||
|
||||
onSortPress(sortKey, sortDirection) {
|
||||
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
const action = props.type === 'episode' ?
|
||||
releaseActions.setEpisodeReleasesFilter :
|
||||
releaseActions.setSeasonReleasesFilter;
|
||||
|
||||
dispatch(action({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
onGrabPress(payload) {
|
||||
dispatch(releaseActions.grabRelease(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class InteractiveSearchConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
searchPayload,
|
||||
isPopulated,
|
||||
dispatchFetchReleases
|
||||
} = this.props;
|
||||
|
||||
// If search results are not yet isPopulated fetch them,
|
||||
// otherwise re-show the existing props.
|
||||
|
||||
if (!isPopulated) {
|
||||
dispatchFetchReleases(searchPayload);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchReleases,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
||||
<InteractiveSearch
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchConnector.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isPopulated: PropTypes.bool,
|
||||
dispatchFetchReleases: PropTypes.func
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
@ -0,0 +1,65 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import InteractiveSearchType from 'InteractiveSearch/InteractiveSearchType';
|
||||
import {
|
||||
setEpisodeReleasesFilter,
|
||||
setSeasonReleasesFilter,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
|
||||
function createReleasesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.releases.items,
|
||||
(releases) => {
|
||||
return releases;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.releases.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface InteractiveSearchFilterModalProps {
|
||||
isOpen: boolean;
|
||||
type: InteractiveSearchType;
|
||||
}
|
||||
|
||||
export default function InteractiveSearchFilterModal({
|
||||
type,
|
||||
...otherProps
|
||||
}: InteractiveSearchFilterModalProps) {
|
||||
const sectionItems = useSelector(createReleasesSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'releases';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
const action =
|
||||
type === 'episode' ? setEpisodeReleasesFilter : setSeasonReleasesFilter;
|
||||
|
||||
dispatch(action(payload));
|
||||
},
|
||||
[type, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...otherProps}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setEpisodeReleasesFilter, setSeasonReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.releases.items,
|
||||
(state) => state.releases.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'releases'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchSetFilter(payload) {
|
||||
const action = props.type === 'episode' ?
|
||||
setEpisodeReleasesFilter:
|
||||
setSeasonReleasesFilter;
|
||||
|
||||
dispatch(action(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
@ -8,15 +9,13 @@ 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 EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import Release from 'typings/Release';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@ -24,7 +23,6 @@ import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||
import Peers from './Peers';
|
||||
import ReleaseEpisode from './ReleaseEpisode';
|
||||
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
@ -72,43 +70,7 @@ function getDownloadTooltip(
|
||||
return translate('AddToDownloadQueue');
|
||||
}
|
||||
|
||||
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;
|
||||
sceneMapping?: object;
|
||||
seasonNumber?: number;
|
||||
episodeNumbers?: number[];
|
||||
absoluteEpisodeNumbers?: number[];
|
||||
mappedSeriesId?: number;
|
||||
mappedSeasonNumber?: number;
|
||||
mappedEpisodeNumbers?: number[];
|
||||
mappedAbsoluteEpisodeNumbers?: number[];
|
||||
mappedEpisodeInfo: ReleaseEpisode[];
|
||||
indexerFlags: number;
|
||||
rejections: string[];
|
||||
episodeRequested: boolean;
|
||||
downloadAllowed: boolean;
|
||||
isDaily: boolean;
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
interface InteractiveSearchRowProps extends Release {
|
||||
searchPayload: object;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
}
|
||||
@ -148,13 +110,15 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
isDaily,
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
searchPayload,
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const { longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
|
3
frontend/src/InteractiveSearch/InteractiveSearchType.ts
Normal file
3
frontend/src/InteractiveSearch/InteractiveSearchType.ts
Normal file
@ -0,0 +1,3 @@
|
||||
type InteractiveSearchType = 'episode' | 'season';
|
||||
|
||||
export default InteractiveSearchType;
|
@ -2,9 +2,9 @@ import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ReleaseEpisode from 'InteractiveSearch/ReleaseEpisode';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import { ReleaseEpisode } from 'typings/Release';
|
||||
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||
|
||||
interface OverrideMatchModalProps {
|
||||
|
@ -18,7 +18,6 @@ import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import ReleaseEpisode from 'InteractiveSearch/ReleaseEpisode';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
@ -26,6 +25,7 @@ import { grabRelease } from 'Store/Actions/releaseActions';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector';
|
||||
import { ReleaseEpisode } from 'typings/Release';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||
import OverrideMatchData from './OverrideMatchData';
|
||||
|
@ -1,9 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function getKind(seeders) {
|
||||
function getKind(seeders: number = 0) {
|
||||
if (seeders > 50) {
|
||||
return kinds.PRIMARY;
|
||||
}
|
||||
@ -19,7 +18,7 @@ function getKind(seeders) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
function getPeersTooltipPart(peers, peersUnit) {
|
||||
function getPeersTooltipPart(peersUnit: string, peers?: number) {
|
||||
if (peers == null) {
|
||||
return `Unknown ${peersUnit}s`;
|
||||
}
|
||||
@ -31,27 +30,27 @@ function getPeersTooltipPart(peers, peersUnit) {
|
||||
return `${peers} ${peersUnit}s`;
|
||||
}
|
||||
|
||||
function Peers(props) {
|
||||
const {
|
||||
seeders,
|
||||
leechers
|
||||
} = props;
|
||||
interface PeersProps {
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
}
|
||||
|
||||
function Peers(props: PeersProps) {
|
||||
const { seeders, leechers } = props;
|
||||
|
||||
const kind = getKind(seeders);
|
||||
|
||||
return (
|
||||
<Label
|
||||
kind={kind}
|
||||
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(leechers, 'leecher')}`}
|
||||
title={`${getPeersTooltipPart('seeder', seeders)}, ${getPeersTooltipPart(
|
||||
'leecher',
|
||||
leechers
|
||||
)}`}
|
||||
>
|
||||
{seeders == null ? '-' : seeders} / {leechers == null ? '-' : leechers}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
Peers.propTypes = {
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number
|
||||
};
|
||||
|
||||
export default Peers;
|
@ -1,10 +0,0 @@
|
||||
interface ReleaseEpisode {
|
||||
id: number;
|
||||
episodeFileId: number;
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
absoluteEpisodeNumber?: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default ReleaseEpisode;
|
@ -5,7 +5,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
|
||||
import formatSeason from 'Season/formatSeason';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
@ -31,7 +31,7 @@ function SeasonInteractiveSearchModalContent(
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
<InteractiveSearchConnector
|
||||
<InteractiveSearch
|
||||
type="season"
|
||||
searchPayload={{
|
||||
seriesId,
|
||||
|
53
frontend/src/typings/Release.ts
Normal file
53
frontend/src/typings/Release.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
export interface ReleaseEpisode {
|
||||
id: number;
|
||||
episodeFileId: number;
|
||||
seasonNumber: number;
|
||||
episodeNumber: number;
|
||||
absoluteEpisodeNumber?: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface Release {
|
||||
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;
|
||||
sceneMapping?: object;
|
||||
seasonNumber?: number;
|
||||
episodeNumbers?: number[];
|
||||
absoluteEpisodeNumbers?: number[];
|
||||
mappedSeriesId?: number;
|
||||
mappedSeasonNumber?: number;
|
||||
mappedEpisodeNumbers?: number[];
|
||||
mappedAbsoluteEpisodeNumbers?: number[];
|
||||
mappedEpisodeInfo: ReleaseEpisode[];
|
||||
indexerFlags: number;
|
||||
rejections: string[];
|
||||
episodeRequested: boolean;
|
||||
downloadAllowed: boolean;
|
||||
isDaily: boolean;
|
||||
|
||||
isGrabbing?: boolean;
|
||||
isGrabbed?: boolean;
|
||||
grabError?: string;
|
||||
}
|
||||
|
||||
export default Release;
|
Loading…
Reference in New Issue
Block a user