parent
546e9fd1d0
commit
0a0e03dca0
@ -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,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;
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
|||||||
|
type InteractiveSearchType = 'episode' | 'season';
|
||||||
|
|
||||||
|
export default InteractiveSearchType;
|
@ -1,10 +0,0 @@
|
|||||||
interface ReleaseEpisode {
|
|
||||||
id: number;
|
|
||||||
episodeFileId: number;
|
|
||||||
seasonNumber: number;
|
|
||||||
episodeNumber: number;
|
|
||||||
absoluteEpisodeNumber?: number;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReleaseEpisode;
|
|
@ -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