mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-11-25 20:22:37 +01:00
Convert Queue to TypeScript
This commit is contained in:
parent
824ed0a369
commit
76650af9fd
@ -11,3 +11,7 @@
|
|||||||
border-color: var(--usenetColor);
|
border-color: var(--usenetColor);
|
||||||
background-color: var(--usenetColor);
|
background-color: var(--usenetColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unknown {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'torrent': string;
|
'torrent': string;
|
||||||
|
'unknown': string;
|
||||||
'usenet': string;
|
'usenet': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import styles from './ProtocolLabel.css';
|
|
||||||
|
|
||||||
function ProtocolLabel({ protocol }) {
|
|
||||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label className={styles[protocol]}>
|
|
||||||
{protocolName}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProtocolLabel.propTypes = {
|
|
||||||
protocol: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProtocolLabel;
|
|
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
16
frontend/src/Activity/Queue/ProtocolLabel.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import styles from './ProtocolLabel.css';
|
||||||
|
|
||||||
|
interface ProtocolLabelProps {
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
|
||||||
|
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||||
|
|
||||||
|
return <Label className={styles[protocol]}>{protocolName}</Label>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProtocolLabel;
|
@ -1,372 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import QueueFilterModal from './QueueFilterModal';
|
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
|
||||||
|
|
||||||
class Queue extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
isPendingSelected: false,
|
|
||||||
isConfirmRemoveModalOpen: false,
|
|
||||||
items: props.items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
if (this._shouldBlockRefresh) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
isEpisodesFetching
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
|
|
||||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
|
|
||||||
) {
|
|
||||||
this.setState((state) => {
|
|
||||||
return {
|
|
||||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
|
||||||
items
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextState = {};
|
|
||||||
|
|
||||||
if (prevProps.items !== items) {
|
|
||||||
nextState.items = items;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
|
||||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
|
||||||
nextState.isPendingSelected = isPendingSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(nextState)) {
|
|
||||||
this.setState(nextState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
|
||||||
this._shouldBlockRefresh = isOpen;
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabSelectedPress = () => {
|
|
||||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = () => {
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
|
||||||
this._shouldBlockRefresh = true;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = (payload) => {
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onConfirmRemoveModalClose = () => {
|
|
||||||
this._shouldBlockRefresh = false;
|
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isEpisodesFetching,
|
|
||||||
isEpisodesPopulated,
|
|
||||||
episodesError,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
count,
|
|
||||||
totalRecords,
|
|
||||||
isGrabbing,
|
|
||||||
isRemoving,
|
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
|
||||||
onRefreshPress,
|
|
||||||
onFilterSelect,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
selectedState,
|
|
||||||
isConfirmRemoveModalOpen,
|
|
||||||
isPendingSelected,
|
|
||||||
items
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
|
||||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
|
||||||
const hasError = error || episodesError;
|
|
||||||
const selectedIds = this.getSelectedIds();
|
|
||||||
const selectedCount = selectedIds.length;
|
|
||||||
const disableSelectedActions = selectedCount === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Queue')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label="Refresh"
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isRefreshing}
|
|
||||||
onPress={onRefreshPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('GrabSelected')}
|
|
||||||
iconName={icons.DOWNLOAD}
|
|
||||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={this.onGrabSelectedPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RemoveSelected')}
|
|
||||||
iconName={icons.REMOVE}
|
|
||||||
isDisabled={disableSelectedActions}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveSelectedPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection
|
|
||||||
alignContent={align.RIGHT}
|
|
||||||
>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
columns={columns}
|
|
||||||
maxPageSize={200}
|
|
||||||
{...otherProps}
|
|
||||||
optionsComponent={QueueOptionsConnector}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={QueueFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isRefreshing && !isAllPopulated ?
|
|
||||||
<LoadingIndicator /> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isRefreshing && hasError ?
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('QueueLoadError')}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !items.length ?
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{
|
|
||||||
selectedFilterKey !== 'all' && count > 0 ?
|
|
||||||
translate('QueueFilterHasNoItems') :
|
|
||||||
translate('QueueIsEmpty')
|
|
||||||
}
|
|
||||||
</Alert> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length ?
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
selectAll={true}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
{...otherProps}
|
|
||||||
optionsComponent={QueueOptionsConnector}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QueueRowConnector
|
|
||||||
key={item.id}
|
|
||||||
episodeId={item.episodeId}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isRefreshing}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
|
||||||
selectedCount={selectedCount}
|
|
||||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
return !!(item && item.downloadClientHasPostImportCategory);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
canIgnore={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
return !!(item && item.seriesId && item.episodeId);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
pending={isConfirmRemoveModalOpen && (
|
|
||||||
selectedIds.every((id) => {
|
|
||||||
const item = items.find((i) => i.id === id);
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
|
||||||
onModalClose={this.onConfirmRemoveModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Queue.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isEpisodesFetching: PropTypes.bool.isRequired,
|
|
||||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
|
||||||
episodesError: PropTypes.object,
|
|
||||||
columns: 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,
|
|
||||||
count: PropTypes.number.isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Queue.defaultProps = {
|
|
||||||
count: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Queue;
|
|
411
frontend/src/Activity/Queue/Queue.tsx
Normal file
411
frontend/src/Activity/Queue/Queue.tsx
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
import React, {
|
||||||
|
ReactElement,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||||
|
import {
|
||||||
|
clearQueue,
|
||||||
|
fetchQueue,
|
||||||
|
gotoQueuePage,
|
||||||
|
grabQueueItems,
|
||||||
|
removeQueueItems,
|
||||||
|
setQueueFilter,
|
||||||
|
setQueueSort,
|
||||||
|
setQueueTableOption,
|
||||||
|
} from 'Store/Actions/queueActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import QueueItem from 'typings/Queue';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
|
import QueueOptions from './QueueOptions';
|
||||||
|
import QueueRow from './QueueRow';
|
||||||
|
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||||
|
import createQueueStatusSelector from './Status/createQueueStatusSelector';
|
||||||
|
|
||||||
|
function Queue() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
isGrabbing,
|
||||||
|
isRemoving,
|
||||||
|
} = useSelector((state: AppState) => state.queue.paged);
|
||||||
|
|
||||||
|
const { count } = useSelector(createQueueStatusSelector());
|
||||||
|
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
||||||
|
useSelector(createEpisodesFetchingSelector());
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('queue'));
|
||||||
|
|
||||||
|
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldBlockRefresh = useRef(false);
|
||||||
|
const currentQueue = useRef<ReactElement | null>(null);
|
||||||
|
|
||||||
|
const [selectState, setSelectState] = useSelectState();
|
||||||
|
const { allSelected, allUnselected, selectedState } = selectState;
|
||||||
|
|
||||||
|
const selectedIds = useMemo(() => {
|
||||||
|
return getSelectedIds(selectedState);
|
||||||
|
}, [selectedState]);
|
||||||
|
|
||||||
|
const isPendingSelected = useMemo(() => {
|
||||||
|
return items.some((item) => {
|
||||||
|
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||||
|
});
|
||||||
|
}, [items, selectedIds]);
|
||||||
|
|
||||||
|
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const isRefreshing =
|
||||||
|
isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||||
|
const isAllPopulated =
|
||||||
|
isPopulated &&
|
||||||
|
(isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
||||||
|
const hasError = error || episodesError;
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
|
||||||
|
const handleSelectAllChange = useCallback(
|
||||||
|
({ value }: CheckInputChanged) => {
|
||||||
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectedChange = useCallback(
|
||||||
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||||
|
setSelectState({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
items,
|
||||||
|
id,
|
||||||
|
isSelected: value,
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[items, setSelectState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRefreshPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
|
||||||
|
shouldBlockRefresh.current = isOpen;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGrabSelectedPress = useCallback(() => {
|
||||||
|
dispatch(grabQueueItems({ ids: selectedIds }));
|
||||||
|
}, [selectedIds, dispatch]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedPress = useCallback(() => {
|
||||||
|
shouldBlockRefresh.current = true;
|
||||||
|
setIsConfirmRemoveModalOpen(true);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const handleRemoveSelectedConfirmed = useCallback(
|
||||||
|
(payload: RemovePressProps) => {
|
||||||
|
shouldBlockRefresh.current = false;
|
||||||
|
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
},
|
||||||
|
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleConfirmRemoveModalClose = useCallback(() => {
|
||||||
|
shouldBlockRefresh.current = false;
|
||||||
|
setIsConfirmRemoveModalOpen(false);
|
||||||
|
}, [setIsConfirmRemoveModalOpen]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoQueuePage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setQueueSort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setQueueTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoQueuePage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearQueue());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const episodeIds = selectUniqueIds<QueueItem, number | undefined>(
|
||||||
|
items,
|
||||||
|
'episodeId'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (episodeIds.length) {
|
||||||
|
dispatch(fetchEpisodes({ episodeIds }));
|
||||||
|
} else {
|
||||||
|
dispatch(clearEpisodes());
|
||||||
|
}
|
||||||
|
}, [items, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchQueue());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!shouldBlockRefresh.current) {
|
||||||
|
currentQueue.current = (
|
||||||
|
<PageContentBody>
|
||||||
|
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isRefreshing && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{selectedFilterKey !== 'all' && count > 0
|
||||||
|
? translate('QueueFilterHasNoItems')
|
||||||
|
: translate('QueueIsEmpty')}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && !!items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSelectAllChange={handleSelectAllChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<QueueRow
|
||||||
|
key={item.id}
|
||||||
|
episodeId={item.episodeId}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={handleSelectedChange}
|
||||||
|
onQueueRowModalOpenOrClose={
|
||||||
|
handleQueueRowModalOpenOrClose
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Queue')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Refresh"
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isRefreshing}
|
||||||
|
onPress={handleRefreshPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('GrabSelected')}
|
||||||
|
iconName={icons.DOWNLOAD}
|
||||||
|
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={handleGrabSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RemoveSelected')}
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={disableSelectedActions}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
maxPageSize={200}
|
||||||
|
optionsComponent={QueueOptions}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
{currentQueue.current}
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
selectedCount={selectedCount}
|
||||||
|
canChangeCategory={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.downloadClientHasPostImportCategory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
canIgnore={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.seriesId && item.episodeId);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isPending={
|
||||||
|
isConfirmRemoveModalOpen &&
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
item.status === 'delay' ||
|
||||||
|
item.status === 'downloadClientUnavailable'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onRemovePress={handleRemoveSelectedConfirmed}
|
||||||
|
onModalClose={handleConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Queue;
|
@ -1,203 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Queue from './Queue';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.episodes,
|
|
||||||
(state) => state.queue.options,
|
|
||||||
(state) => state.queue.paged,
|
|
||||||
(state) => state.queue.status.item,
|
|
||||||
createCustomFiltersSelector('queue'),
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
|
||||||
(episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
|
||||||
return {
|
|
||||||
count: options.includeUnknownSeriesItems ? status.totalCount : status.count,
|
|
||||||
isEpisodesFetching: episodes.isFetching,
|
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
|
||||||
episodesError: episodes.error,
|
|
||||||
customFilters,
|
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
|
||||||
...options,
|
|
||||||
...queue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...queueActions,
|
|
||||||
fetchEpisodes,
|
|
||||||
clearEpisodes,
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchQueue,
|
|
||||||
fetchQueueStatus,
|
|
||||||
gotoQueueFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchQueue();
|
|
||||||
} else {
|
|
||||||
gotoQueueFirstPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchQueueStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
|
||||||
const episodeIds = selectUniqueIds(this.props.items, 'episodeId');
|
|
||||||
|
|
||||||
if (episodeIds.length) {
|
|
||||||
this.props.fetchEpisodes({ episodeIds });
|
|
||||||
} else {
|
|
||||||
this.props.clearEpisodes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.props.includeUnknownSeriesItems !==
|
|
||||||
prevProps.includeUnknownSeriesItems
|
|
||||||
) {
|
|
||||||
this.repopulate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearQueue();
|
|
||||||
this.props.clearEpisodes();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchQueue();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoQueuePreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoQueueNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoQueueLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoQueuePage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setQueueSort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setQueueFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setQueueTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
this.props.executeCommand({
|
|
||||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onGrabSelectedPress = (ids) => {
|
|
||||||
this.props.grabQueueItems({ ids });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveSelectedPress = (payload) => {
|
|
||||||
this.props.removeQueueItems(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Queue
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
onRefreshPress={this.onRefreshPress}
|
|
||||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
|
||||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueConnector.propTypes = {
|
|
||||||
includeUnknownSeriesItems: PropTypes.bool.isRequired,
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchQueue: PropTypes.func.isRequired,
|
|
||||||
fetchQueueStatus: PropTypes.func.isRequired,
|
|
||||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoQueuePage: PropTypes.func.isRequired,
|
|
||||||
setQueueSort: PropTypes.func.isRequired,
|
|
||||||
setQueueFilter: PropTypes.func.isRequired,
|
|
||||||
setQueueTableOption: PropTypes.func.isRequired,
|
|
||||||
clearQueue: PropTypes.func.isRequired,
|
|
||||||
grabQueueItems: PropTypes.func.isRequired,
|
|
||||||
removeQueueItems: PropTypes.func.isRequired,
|
|
||||||
fetchEpisodes: PropTypes.func.isRequired,
|
|
||||||
clearEpisodes: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
|
||||||
);
|
|
@ -1,36 +1,49 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QueueStatus from './QueueStatus';
|
import QueueStatus from './QueueStatus';
|
||||||
import styles from './QueueDetails.css';
|
import styles from './QueueDetails.css';
|
||||||
|
|
||||||
function QueueDetails(props) {
|
interface QueueDetailsProps {
|
||||||
|
title: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
progressBar: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueDetails(props: QueueDetailsProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
status,
|
status,
|
||||||
trackedDownloadState,
|
trackedDownloadState = 'downloading',
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus = 'ok',
|
||||||
statusMessages,
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
progressBar
|
progressBar,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = (100 - sizeleft / size * 100);
|
const progress = 100 - (sizeleft / size) * 100;
|
||||||
const isDownloading = status === 'downloading';
|
const isDownloading = status === 'downloading';
|
||||||
const isPaused = status === 'paused';
|
const isPaused = status === 'paused';
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
const hasError = trackedDownloadStatus === 'error';
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
if (
|
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
|
||||||
(isDownloading || isPaused) &&
|
|
||||||
!hasWarning &&
|
|
||||||
!hasError
|
|
||||||
) {
|
|
||||||
const state = isPaused ? translate('Paused') : translate('Downloading');
|
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||||
|
|
||||||
if (progress < 5) {
|
if (progress < 5) {
|
||||||
@ -45,11 +58,9 @@ function QueueDetails(props) {
|
|||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
className={styles.progressBarContainer}
|
className={styles.progressBarContainer}
|
||||||
anchor={progressBar}
|
anchor={progressBar!}
|
||||||
title={`${state} - ${progress.toFixed(1)}%`}
|
title={`${state} - ${progress.toFixed(1)}%`}
|
||||||
body={
|
body={<div>{title}</div>}
|
||||||
<div>{title}</div>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -68,22 +79,4 @@ function QueueDetails(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueDetails.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
progressBar: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueDetails.defaultProps = {
|
|
||||||
trackedDownloadStatus: 'ok',
|
|
||||||
trackedDownloadState: 'downloading'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueDetails;
|
export default QueueDetails;
|
@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component, Fragment } from 'react';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class QueueOptions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
includeUnknownSeriesItems: props.includeUnknownSeriesItems
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
includeUnknownSeriesItems
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (includeUnknownSeriesItems !== prevProps.includeUnknownSeriesItems) {
|
|
||||||
this.setState({
|
|
||||||
includeUnknownSeriesItems
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOptionChange = ({ name, value }) => {
|
|
||||||
this.setState({
|
|
||||||
[name]: value
|
|
||||||
}, () => {
|
|
||||||
this.props.onOptionChange({
|
|
||||||
[name]: value
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
includeUnknownSeriesItems
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="includeUnknownSeriesItems"
|
|
||||||
value={includeUnknownSeriesItems}
|
|
||||||
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
|
||||||
onChange={this.onOptionChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueOptions.propTypes = {
|
|
||||||
includeUnknownSeriesItems: PropTypes.bool.isRequired,
|
|
||||||
onOptionChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueOptions;
|
|
46
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
46
frontend/src/Activity/Queue/QueueOptions.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { Fragment, useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||||
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function QueueOptions() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { includeUnknownSeriesItems } = useSelector(
|
||||||
|
(state: AppState) => state.queue.options
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionChange = useCallback(
|
||||||
|
({ name, value }: CheckInputChanged) => {
|
||||||
|
dispatch(
|
||||||
|
setQueueOption({
|
||||||
|
[name]: value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowUnknownSeriesItems')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeUnknownSeriesItems"
|
||||||
|
value={includeUnknownSeriesItems}
|
||||||
|
helpText={translate('ShowUnknownSeriesItemsHelpText')}
|
||||||
|
onChange={handleOptionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueOptions;
|
@ -1,19 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
|
||||||
import QueueOptions from './QueueOptions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.queue.options,
|
|
||||||
(options) => {
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
onOptionChange: setQueueOption
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
|
@ -1,481 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QueueStatusCell from './QueueStatusCell';
|
|
||||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
|
||||||
import TimeleftCell from './TimeleftCell';
|
|
||||||
import styles from './QueueRow.css';
|
|
||||||
|
|
||||||
class QueueRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isRemoveQueueItemModalOpen: false,
|
|
||||||
isInteractiveImportModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onRemoveQueueItemPress = () => {
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
|
|
||||||
const {
|
|
||||||
onRemoveQueueItemPress,
|
|
||||||
onQueueRowModalOpenOrClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onQueueRowModalOpenOrClose(false);
|
|
||||||
onRemoveQueueItemPress(blocklist, skipRedownload);
|
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemModalClose = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(false);
|
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportPress = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(true);
|
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onInteractiveImportModalClose = () => {
|
|
||||||
this.props.onQueueRowModalOpenOrClose(false);
|
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
downloadId,
|
|
||||||
title,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage,
|
|
||||||
series,
|
|
||||||
episode,
|
|
||||||
languages,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
protocol,
|
|
||||||
indexer,
|
|
||||||
outputPath,
|
|
||||||
downloadClient,
|
|
||||||
downloadClientHasPostImportCategory,
|
|
||||||
estimatedCompletionTime,
|
|
||||||
added,
|
|
||||||
timeleft,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
isGrabbing,
|
|
||||||
grabError,
|
|
||||||
isRemoving,
|
|
||||||
isSelected,
|
|
||||||
columns,
|
|
||||||
onSelectedChange,
|
|
||||||
onGrabPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isRemoveQueueItemModalOpen,
|
|
||||||
isInteractiveImportModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const progress = 100 - (sizeleft / size * 100);
|
|
||||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
|
||||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableSelectCell
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
|
||||||
return (
|
|
||||||
<QueueStatusCell
|
|
||||||
key={name}
|
|
||||||
sourceTitle={title}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'series.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
series ?
|
|
||||||
<SeriesTitleLink
|
|
||||||
titleSlug={series.titleSlug}
|
|
||||||
title={series.title}
|
|
||||||
/> :
|
|
||||||
title
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episode') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
episode ?
|
|
||||||
<SeasonEpisodeNumber
|
|
||||||
seasonNumber={episode.seasonNumber}
|
|
||||||
episodeNumber={episode.episodeNumber}
|
|
||||||
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
|
||||||
seriesType={series.seriesType}
|
|
||||||
alternateTitles={series.alternateTitles}
|
|
||||||
sceneSeasonNumber={episode.sceneSeasonNumber}
|
|
||||||
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
|
||||||
sceneAbsoluteEpisodeNumber={episode.sceneAbsoluteEpisodeNumber}
|
|
||||||
unverifiedSceneNumbering={episode.unverifiedSceneNumbering}
|
|
||||||
/> :
|
|
||||||
'-'
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episodes.title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
episode ?
|
|
||||||
<EpisodeTitleLink
|
|
||||||
episodeId={episode.id}
|
|
||||||
seriesId={series.id}
|
|
||||||
episodeFileId={episode.episodeFileId}
|
|
||||||
episodeTitle={episode.title}
|
|
||||||
showOpenSeriesButton={true}
|
|
||||||
/> :
|
|
||||||
'-'
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'episodes.airDateUtc') {
|
|
||||||
if (episode) {
|
|
||||||
return (
|
|
||||||
<RelativeDateCell
|
|
||||||
key={name}
|
|
||||||
date={episode.airDateUtc}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeLanguages
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
quality ?
|
|
||||||
<EpisodeQuality
|
|
||||||
quality={quality}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<EpisodeFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'protocol') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<ProtocolLabel
|
|
||||||
protocol={protocol}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{title}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'size') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'outputPath') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{outputPath}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'estimatedCompletionTime') {
|
|
||||||
return (
|
|
||||||
<TimeleftCell
|
|
||||||
key={name}
|
|
||||||
status={status}
|
|
||||||
estimatedCompletionTime={estimatedCompletionTime}
|
|
||||||
timeleft={timeleft}
|
|
||||||
size={size}
|
|
||||||
sizeleft={sizeleft}
|
|
||||||
showRelativeDates={showRelativeDates}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'progress') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.progress}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!progress &&
|
|
||||||
<ProgressBar
|
|
||||||
progress={progress}
|
|
||||||
title={`${progress.toFixed(1)}%`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'added') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCell
|
|
||||||
key={name}
|
|
||||||
date={added}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.actions}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
showInteractiveImport &&
|
|
||||||
<IconButton
|
|
||||||
name={icons.INTERACTIVE}
|
|
||||||
onPress={this.onInteractiveImportPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPending &&
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={icons.DOWNLOAD}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={onGrabPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<SpinnerIconButton
|
|
||||||
title={translate('RemoveFromQueue')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
isSpinning={isRemoving}
|
|
||||||
onPress={this.onRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<InteractiveImportModal
|
|
||||||
isOpen={isInteractiveImportModalOpen}
|
|
||||||
downloadId={downloadId}
|
|
||||||
title={title}
|
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RemoveQueueItemModal
|
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
|
||||||
sourceTitle={title}
|
|
||||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
|
||||||
canIgnore={!!series}
|
|
||||||
isPending={isPending}
|
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
|
||||||
onModalClose={this.onRemoveQueueItemModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string,
|
|
||||||
trackedDownloadState: PropTypes.string,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
series: PropTypes.object,
|
|
||||||
episode: PropTypes.object,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
protocol: PropTypes.string.isRequired,
|
|
||||||
indexer: PropTypes.string,
|
|
||||||
outputPath: PropTypes.string,
|
|
||||||
downloadClient: PropTypes.string,
|
|
||||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
added: PropTypes.string,
|
|
||||||
timeleft: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
sizeleft: PropTypes.number,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
|
||||||
grabError: PropTypes.object,
|
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
|
||||||
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueRow.defaultProps = {
|
|
||||||
customFormats: [],
|
|
||||||
isGrabbing: false,
|
|
||||||
isRemoving: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueRow;
|
|
411
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
411
frontend/src/Activity/Queue/QueueRow.tsx
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
|
import useEpisode from 'Episode/useEpisode';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
|
import useSeries from 'Series/useSeries';
|
||||||
|
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueueStatusCell from './QueueStatusCell';
|
||||||
|
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
|
||||||
|
import TimeleftCell from './TimeleftCell';
|
||||||
|
import styles from './QueueRow.css';
|
||||||
|
|
||||||
|
interface QueueRowProps {
|
||||||
|
id: number;
|
||||||
|
seriesId?: number;
|
||||||
|
episodeId?: number;
|
||||||
|
downloadId?: string;
|
||||||
|
title: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
indexer?: string;
|
||||||
|
outputPath?: string;
|
||||||
|
downloadClient?: string;
|
||||||
|
downloadClientHasPostImportCategory?: boolean;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
added?: string;
|
||||||
|
timeleft?: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
isGrabbing?: boolean;
|
||||||
|
grabError?: Error;
|
||||||
|
isRemoving?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
|
columns: Column[];
|
||||||
|
onSelectedChange: (options: SelectStateInputProps) => void;
|
||||||
|
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueRow(props: QueueRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
seriesId,
|
||||||
|
episodeId,
|
||||||
|
downloadId,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats = [],
|
||||||
|
customFormatScore,
|
||||||
|
protocol,
|
||||||
|
indexer,
|
||||||
|
outputPath,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientHasPostImportCategory,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
added,
|
||||||
|
timeleft,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
isGrabbing = false,
|
||||||
|
grabError,
|
||||||
|
isRemoving = false,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
|
onQueueRowModalOpenOrClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const series = useSeries(seriesId);
|
||||||
|
const episode = useEpisode(episodeId, 'episodes');
|
||||||
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handleGrabPress = useCallback(() => {
|
||||||
|
dispatch(grabQueueItem({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
const handleInteractiveImportPress = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(true);
|
||||||
|
setIsInteractiveImportModalOpen(true);
|
||||||
|
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleInteractiveImportModalClose = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
setIsInteractiveImportModalOpen(false);
|
||||||
|
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemPress = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(true);
|
||||||
|
setIsRemoveQueueItemModalOpen(true);
|
||||||
|
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemModalConfirmed = useCallback(
|
||||||
|
(payload: RemovePressProps) => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
dispatch(removeQueueItem({ id, ...payload }));
|
||||||
|
setIsRemoveQueueItemModalOpen(false);
|
||||||
|
},
|
||||||
|
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveQueueItemModalClose = useCallback(() => {
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
setIsRemoveQueueItemModalOpen(false);
|
||||||
|
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
|
||||||
|
|
||||||
|
const progress = 100 - (sizeleft / size) * 100;
|
||||||
|
const showInteractiveImport =
|
||||||
|
status === 'completed' && trackedDownloadStatus === 'warning';
|
||||||
|
const isPending =
|
||||||
|
status === 'delay' || status === 'downloadClientUnavailable';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<QueueStatusCell
|
||||||
|
key={name}
|
||||||
|
sourceTitle={title}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'series.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{series ? (
|
||||||
|
<SeriesTitleLink
|
||||||
|
titleSlug={series.titleSlug}
|
||||||
|
title={series.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episode') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{episode ? (
|
||||||
|
<SeasonEpisodeNumber
|
||||||
|
seasonNumber={episode.seasonNumber}
|
||||||
|
episodeNumber={episode.episodeNumber}
|
||||||
|
absoluteEpisodeNumber={episode.absoluteEpisodeNumber}
|
||||||
|
seriesType={series?.seriesType}
|
||||||
|
alternateTitles={series?.alternateTitles}
|
||||||
|
sceneSeasonNumber={episode.sceneSeasonNumber}
|
||||||
|
sceneEpisodeNumber={episode.sceneEpisodeNumber}
|
||||||
|
sceneAbsoluteEpisodeNumber={
|
||||||
|
episode.sceneAbsoluteEpisodeNumber
|
||||||
|
}
|
||||||
|
unverifiedSceneNumbering={episode.unverifiedSceneNumbering}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodes.title') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{series && episode ? (
|
||||||
|
<EpisodeTitleLink
|
||||||
|
episodeId={episode.id}
|
||||||
|
seriesId={series.id}
|
||||||
|
episodeTitle={episode.title}
|
||||||
|
episodeEntity="episodes"
|
||||||
|
showOpenSeriesButton={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'episodes.airDateUtc') {
|
||||||
|
if (episode) {
|
||||||
|
return <RelativeDateCell key={name} date={episode.airDateUtc} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TableRowCell key={name}>-</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{quality ? <EpisodeQuality quality={quality} /> : null}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<EpisodeFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'protocol') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<ProtocolLabel protocol={protocol} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return <TableRowCell key={name}>{indexer}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'title') {
|
||||||
|
return <TableRowCell key={name}>{title}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'outputPath') {
|
||||||
|
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'estimatedCompletionTime') {
|
||||||
|
return (
|
||||||
|
<TimeleftCell
|
||||||
|
key={name}
|
||||||
|
status={status}
|
||||||
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
|
timeleft={timeleft}
|
||||||
|
size={size}
|
||||||
|
sizeleft={sizeleft}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'progress') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.progress}>
|
||||||
|
{!!progress && (
|
||||||
|
<ProgressBar
|
||||||
|
progress={progress}
|
||||||
|
title={`${progress.toFixed(1)}%`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'added') {
|
||||||
|
return <RelativeDateCell key={name} date={added} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.actions}>
|
||||||
|
{showInteractiveImport ? (
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={handleInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isPending ? (
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={handleGrabPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<SpinnerIconButton
|
||||||
|
title={translate('RemoveFromQueue')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={handleRemoveQueueItemPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
downloadId={downloadId}
|
||||||
|
modalTitle={title}
|
||||||
|
onModalClose={handleInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RemoveQueueItemModal
|
||||||
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
|
sourceTitle={title}
|
||||||
|
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||||
|
canIgnore={!!series}
|
||||||
|
isPending={isPending}
|
||||||
|
onRemovePress={handleRemoveQueueItemModalConfirmed}
|
||||||
|
onModalClose={handleRemoveQueueItemModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueRow;
|
@ -1,70 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
|
||||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import QueueRow from './QueueRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSeriesSelector(),
|
|
||||||
createEpisodeSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(series, episode, uiSettings) => {
|
|
||||||
const result = {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
|
|
||||||
result.series = series;
|
|
||||||
result.episode = episode;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
grabQueueItem,
|
|
||||||
removeQueueItem
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onGrabPress = () => {
|
|
||||||
this.props.grabQueueItem({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveQueueItemPress = (payload) => {
|
|
||||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QueueRow
|
|
||||||
{...this.props}
|
|
||||||
onGrabPress={this.onGrabPress}
|
|
||||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
episode: PropTypes.object,
|
|
||||||
grabQueueItem: PropTypes.func.isRequired,
|
|
||||||
removeQueueItem: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
|
@ -1,51 +1,59 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './QueueStatus.css';
|
import styles from './QueueStatus.css';
|
||||||
|
|
||||||
function getDetailedPopoverBody(statusMessages) {
|
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{statusMessages.map(({ title, messages }) => {
|
||||||
statusMessages.map(({ title, messages }) => {
|
return (
|
||||||
return (
|
<div
|
||||||
<div
|
key={title}
|
||||||
key={title}
|
className={messages.length ? undefined : styles.noMessages}
|
||||||
className={messages.length ? undefined: styles.noMessages}
|
>
|
||||||
>
|
{title}
|
||||||
{title}
|
<ul>
|
||||||
<ul>
|
{messages.map((message) => {
|
||||||
{
|
return <li key={message}>{message}</li>;
|
||||||
messages.map((message) => {
|
})}
|
||||||
return (
|
</ul>
|
||||||
<li key={message}>
|
</div>
|
||||||
{message}
|
);
|
||||||
</li>
|
})}
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function QueueStatus(props) {
|
interface QueueStatusProps {
|
||||||
|
sourceTitle: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
position: TooltipPosition;
|
||||||
|
canFlip?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatus(props: QueueStatusProps) {
|
||||||
const {
|
const {
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
status,
|
status,
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus = 'ok',
|
||||||
trackedDownloadState,
|
trackedDownloadState = 'downloading',
|
||||||
statusMessages,
|
statusMessages = [],
|
||||||
errorMessage,
|
errorMessage,
|
||||||
position,
|
position,
|
||||||
canFlip
|
canFlip = false,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
@ -115,7 +123,8 @@ function QueueStatus(props) {
|
|||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
const warningMessage =
|
||||||
|
errorMessage || translate('CheckDownloadClientForDetails');
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
title = translate('DownloadWarning', { warningMessage });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,35 +142,23 @@ function QueueStatus(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
anchor={
|
anchor={<Icon name={iconName} kind={iconKind} />}
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={title}
|
title={title}
|
||||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
body={
|
||||||
|
hasWarning || hasError
|
||||||
|
? getDetailedPopoverBody(statusMessages)
|
||||||
|
: sourceTitle
|
||||||
|
}
|
||||||
position={position}
|
position={position}
|
||||||
canFlip={canFlip}
|
canFlip={canFlip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueStatus.propTypes = {
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string,
|
|
||||||
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
|
||||||
canFlip: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueStatus.defaultProps = {
|
QueueStatus.defaultProps = {
|
||||||
trackedDownloadStatus: 'ok',
|
trackedDownloadStatus: 'ok',
|
||||||
trackedDownloadState: 'downloading',
|
trackedDownloadState: 'downloading',
|
||||||
canFlip: false
|
canFlip: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueueStatus;
|
export default QueueStatus;
|
@ -1,47 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import { tooltipPositions } from 'Helpers/Props';
|
|
||||||
import QueueStatus from './QueueStatus';
|
|
||||||
import styles from './QueueStatusCell.css';
|
|
||||||
|
|
||||||
function QueueStatusCell(props) {
|
|
||||||
const {
|
|
||||||
sourceTitle,
|
|
||||||
status,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
trackedDownloadState,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell className={styles.status}>
|
|
||||||
<QueueStatus
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueStatusCell.propTypes = {
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
QueueStatusCell.defaultProps = {
|
|
||||||
trackedDownloadStatus: 'ok',
|
|
||||||
trackedDownloadState: 'downloading'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QueueStatusCell;
|
|
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
45
frontend/src/Activity/Queue/QueueStatusCell.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
import QueueStatus from './QueueStatus';
|
||||||
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
|
interface QueueStatusCellProps {
|
||||||
|
sourceTitle: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadStatus?: QueueTrackedDownloadStatus;
|
||||||
|
trackedDownloadState?: QueueTrackedDownloadState;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatusCell(props: QueueStatusCellProps) {
|
||||||
|
const {
|
||||||
|
sourceTitle,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus = 'ok',
|
||||||
|
trackedDownloadState = 'downloading',
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.status}>
|
||||||
|
<QueueStatus
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatusCell;
|
@ -12,7 +12,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './RemoveQueueItemModal.css';
|
import styles from './RemoveQueueItemModal.css';
|
||||||
|
|
||||||
interface RemovePressProps {
|
export interface RemovePressProps {
|
||||||
remove: boolean;
|
remove: boolean;
|
||||||
changeCategory: boolean;
|
changeCategory: boolean;
|
||||||
blocklist: boolean;
|
blocklist: boolean;
|
||||||
@ -21,7 +21,7 @@ interface RemovePressProps {
|
|||||||
|
|
||||||
interface RemoveQueueItemModalProps {
|
interface RemoveQueueItemModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
sourceTitle: string;
|
sourceTitle?: string;
|
||||||
canChangeCategory: boolean;
|
canChangeCategory: boolean;
|
||||||
canIgnore: boolean;
|
canIgnore: boolean;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
@ -39,7 +39,7 @@ type BlocklistMethod =
|
|||||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
sourceTitle,
|
sourceTitle = '',
|
||||||
canIgnore,
|
canIgnore,
|
||||||
canChangeCategory,
|
canChangeCategory,
|
||||||
isPending,
|
isPending,
|
||||||
|
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
37
frontend/src/Activity/Queue/Status/QueueStatus.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||||
|
import createQueueStatusSelector from './createQueueStatusSelector';
|
||||||
|
|
||||||
|
function QueueStatus() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isConnected, isReconnecting } = useSelector(
|
||||||
|
(state: AppState) => state.app
|
||||||
|
);
|
||||||
|
const { isPopulated, count, errors, warnings } = useSelector(
|
||||||
|
createQueueStatusSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasReconnecting = usePrevious(isReconnecting);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPopulated) {
|
||||||
|
dispatch(fetchQueueStatus());
|
||||||
|
}
|
||||||
|
}, [isPopulated, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isConnected && wasReconnecting) {
|
||||||
|
dispatch(fetchQueueStatus());
|
||||||
|
}
|
||||||
|
}, [isConnected, wasReconnecting, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatus;
|
@ -1,76 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
|
||||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app,
|
|
||||||
(state) => state.queue.status,
|
|
||||||
(state) => state.queue.options.includeUnknownSeriesItems,
|
|
||||||
(app, status, includeUnknownSeriesItems) => {
|
|
||||||
const {
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
unknownErrors,
|
|
||||||
unknownWarnings,
|
|
||||||
count,
|
|
||||||
totalCount
|
|
||||||
} = status.item;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isConnected: app.isConnected,
|
|
||||||
isReconnecting: app.isReconnecting,
|
|
||||||
isPopulated: status.isPopulated,
|
|
||||||
...status.item,
|
|
||||||
count: includeUnknownSeriesItems ? totalCount : count,
|
|
||||||
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
|
|
||||||
warnings: includeUnknownSeriesItems ? warnings || unknownWarnings : warnings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchQueueStatus
|
|
||||||
};
|
|
||||||
|
|
||||||
class QueueStatusConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (!this.props.isPopulated) {
|
|
||||||
this.props.fetchQueueStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
|
||||||
this.props.fetchQueueStatus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<PageSidebarStatus
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueueStatusConnector.propTypes = {
|
|
||||||
isConnected: PropTypes.bool.isRequired,
|
|
||||||
isReconnecting: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
fetchQueueStatus: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
|
@ -0,0 +1,32 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createQueueStatusSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.status.isPopulated,
|
||||||
|
(state: AppState) => state.queue.status.item,
|
||||||
|
(state: AppState) => state.queue.options.includeUnknownSeriesItems,
|
||||||
|
(isPopulated, status, includeUnknownSeriesItems) => {
|
||||||
|
const {
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
unknownErrors,
|
||||||
|
unknownWarnings,
|
||||||
|
count,
|
||||||
|
totalCount,
|
||||||
|
} = status;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...status,
|
||||||
|
isPopulated,
|
||||||
|
count: includeUnknownSeriesItems ? totalCount : count,
|
||||||
|
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
|
||||||
|
warnings: includeUnknownSeriesItems
|
||||||
|
? warnings || unknownWarnings
|
||||||
|
: warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createQueueStatusSelector;
|
@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
@ -11,7 +10,18 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './TimeleftCell.css';
|
import styles from './TimeleftCell.css';
|
||||||
|
|
||||||
function TimeleftCell(props) {
|
interface TimeleftCellProps {
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
timeleft?: string;
|
||||||
|
status: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
showRelativeDates: boolean;
|
||||||
|
shortDateFormat: string;
|
||||||
|
timeFormat: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TimeleftCell(props: TimeleftCellProps) {
|
||||||
const {
|
const {
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
timeleft,
|
timeleft,
|
||||||
@ -20,16 +30,18 @@ function TimeleftCell(props) {
|
|||||||
sizeleft,
|
sizeleft,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat
|
timeFormat,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (status === 'delay') {
|
if (status === 'delay') {
|
||||||
const date = getRelativeDate({
|
const date = getRelativeDate({
|
||||||
date: estimatedCompletionTime,
|
date: estimatedCompletionTime,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
});
|
});
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
@ -47,9 +59,11 @@ function TimeleftCell(props) {
|
|||||||
const date = getRelativeDate({
|
const date = getRelativeDate({
|
||||||
date: estimatedCompletionTime,
|
date: estimatedCompletionTime,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
const time = formatTime(estimatedCompletionTime, timeFormat, {
|
||||||
|
includeMinuteZero: true,
|
||||||
});
|
});
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
@ -64,11 +78,7 @@ function TimeleftCell(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||||
return (
|
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
|
||||||
<TableRowCell className={styles.timeleft}>
|
|
||||||
-
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSize = formatBytes(size);
|
const totalSize = formatBytes(size);
|
||||||
@ -84,15 +94,4 @@ function TimeleftCell(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeleftCell.propTypes = {
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
timeleft: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TimeleftCell;
|
export default TimeleftCell;
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { Redirect, Route } from 'react-router-dom';
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||||
import History from 'Activity/History/History';
|
import History from 'Activity/History/History';
|
||||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
import Queue from 'Activity/Queue/Queue';
|
||||||
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
|
import AddNewSeriesConnector from 'AddSeries/AddNewSeries/AddNewSeriesConnector';
|
||||||
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
|
import ImportSeries from 'AddSeries/ImportSeries/ImportSeries';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
@ -130,7 +130,7 @@ function AppRoutes(props) {
|
|||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/activity/queue"
|
path="/activity/queue"
|
||||||
component={QueueConnector}
|
component={Queue}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
@ -46,6 +46,8 @@ export interface CustomFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
isConnected: boolean;
|
||||||
|
isReconnecting: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
@ -3,15 +3,29 @@ import AppSectionState, {
|
|||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
Error,
|
Error,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
} from './AppSectionState';
|
} from './AppSectionState';
|
||||||
|
|
||||||
|
export interface QueueStatus {
|
||||||
|
totalCount: number;
|
||||||
|
count: number;
|
||||||
|
unknownCount: number;
|
||||||
|
errors: boolean;
|
||||||
|
warnings: boolean;
|
||||||
|
unknownErrors: boolean;
|
||||||
|
unknownWarnings: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuePagedAppState
|
export interface QueuePagedAppState
|
||||||
extends AppSectionState<Queue>,
|
extends AppSectionState<Queue>,
|
||||||
AppSectionFilterState<Queue> {
|
AppSectionFilterState<Queue>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
@ -19,9 +33,12 @@ export interface QueuePagedAppState
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface QueueAppState {
|
interface QueueAppState {
|
||||||
status: AppSectionItemState<Queue>;
|
status: AppSectionItemState<QueueStatus>;
|
||||||
details: QueueDetailsAppState;
|
details: QueueDetailsAppState;
|
||||||
paged: QueuePagedAppState;
|
paged: QueuePagedAppState;
|
||||||
|
options: {
|
||||||
|
includeUnknownSeriesItems: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default QueueAppState;
|
export default QueueAppState;
|
||||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import QueueStatusConnector from 'Activity/Queue/Status/QueueStatusConnector';
|
import QueueStatus from 'Activity/Queue/Status/QueueStatus';
|
||||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@ -50,7 +50,7 @@ const links = [
|
|||||||
{
|
{
|
||||||
title: () => translate('Queue'),
|
title: () => translate('Queue'),
|
||||||
to: '/activity/queue',
|
to: '/activity/queue',
|
||||||
statusComponent: QueueStatusConnector
|
statusComponent: QueueStatus
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: () => translate('History'),
|
title: () => translate('History'),
|
||||||
|
@ -9,7 +9,7 @@ export type EpisodeEntities =
|
|||||||
| 'cutoffUnmet'
|
| 'cutoffUnmet'
|
||||||
| 'missing';
|
| 'missing';
|
||||||
|
|
||||||
function createEpisodeSelector(episodeId: number) {
|
function createEpisodeSelector(episodeId?: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.episodes.items,
|
(state: AppState) => state.episodes.items,
|
||||||
(episodes) => {
|
(episodes) => {
|
||||||
@ -18,7 +18,7 @@ function createEpisodeSelector(episodeId: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCalendarEpisodeSelector(episodeId: number) {
|
function createCalendarEpisodeSelector(episodeId?: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.calendar.items,
|
(state: AppState) => state.calendar.items,
|
||||||
(episodes) => {
|
(episodes) => {
|
||||||
@ -27,7 +27,10 @@ function createCalendarEpisodeSelector(episodeId: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEpisode(episodeId: number, episodeEntity: EpisodeEntities) {
|
function useEpisode(
|
||||||
|
episodeId: number | undefined,
|
||||||
|
episodeEntity: EpisodeEntities
|
||||||
|
) {
|
||||||
let selector = createEpisodeSelector;
|
let selector = createEpisodeSelector;
|
||||||
|
|
||||||
switch (episodeEntity) {
|
switch (episodeEntity) {
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
enum TooltipPosition {
|
type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
|
||||||
Top = 'top',
|
|
||||||
Right = 'right',
|
|
||||||
Bottom = 'bottom',
|
|
||||||
Left = 'left',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TooltipPosition;
|
export default TooltipPosition;
|
||||||
|
@ -137,14 +137,13 @@ function getInfoRowProps(
|
|||||||
date: formatDateTime(previousAiring, longDateFormat, timeFormat),
|
date: formatDateTime(previousAiring, longDateFormat, timeFormat),
|
||||||
}),
|
}),
|
||||||
iconName: icons.CALENDAR,
|
iconName: icons.CALENDAR,
|
||||||
label:
|
label: getRelativeDate({
|
||||||
getRelativeDate({
|
date: previousAiring,
|
||||||
date: previousAiring,
|
shortDateFormat,
|
||||||
shortDateFormat,
|
showRelativeDates,
|
||||||
showRelativeDates,
|
timeFormat,
|
||||||
timeFormat,
|
timeForToday: true,
|
||||||
timeForToday: true,
|
}),
|
||||||
}) ?? '',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
|
||||||
import SeasonDetails from 'Series/Index/Select/SeasonPass/SeasonDetails';
|
import SeasonDetails from 'Series/Index/Select/SeasonPass/SeasonDetails';
|
||||||
import { Season } from 'Series/Series';
|
import { Season } from 'Series/Series';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@ -33,7 +32,7 @@ function SeasonsCell(props: SeriesStatusCellProps) {
|
|||||||
anchor={seasonCount}
|
anchor={seasonCount}
|
||||||
title={translate('SeasonDetails')}
|
title={translate('SeasonDetails')}
|
||||||
body={<SeasonDetails seriesId={seriesId} seasons={seasons} />}
|
body={<SeasonDetails seriesId={seriesId} seasons={seasons} />}
|
||||||
position={TooltipPosition.Left}
|
position="left"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
seasonCount
|
seasonCount
|
||||||
|
@ -27,7 +27,7 @@ function getRelativeDate({
|
|||||||
includeTime = false,
|
includeTime = false,
|
||||||
}: GetRelativeDateOptions) {
|
}: GetRelativeDateOptions) {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return null;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((includeTime || timeForToday) && !timeFormat) {
|
if ((includeTime || timeForToday) && !timeFormat) {
|
@ -1,4 +1,5 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
import { QualityModel } from 'Quality/Quality';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
@ -7,6 +8,7 @@ export type QueueTrackedDownloadStatus = 'ok' | 'warning' | 'error';
|
|||||||
|
|
||||||
export type QueueTrackedDownloadState =
|
export type QueueTrackedDownloadState =
|
||||||
| 'downloading'
|
| 'downloading'
|
||||||
|
| 'importBlocked'
|
||||||
| 'importPending'
|
| 'importPending'
|
||||||
| 'importing'
|
| 'importing'
|
||||||
| 'imported'
|
| 'imported'
|
||||||
@ -23,6 +25,7 @@ interface Queue extends ModelBase {
|
|||||||
languages: Language[];
|
languages: Language[];
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
customFormats: CustomFormat[];
|
customFormats: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
size: number;
|
size: number;
|
||||||
title: string;
|
title: string;
|
||||||
sizeleft: number;
|
sizeleft: number;
|
||||||
@ -35,13 +38,14 @@ interface Queue extends ModelBase {
|
|||||||
statusMessages: StatusMessage[];
|
statusMessages: StatusMessage[];
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
downloadId: string;
|
downloadId: string;
|
||||||
protocol: string;
|
protocol: DownloadProtocol;
|
||||||
downloadClient: string;
|
downloadClient: string;
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
episodeHasFile: boolean;
|
episodeHasFile: boolean;
|
||||||
seriesId?: number;
|
seriesId?: number;
|
||||||
episodeId?: number;
|
episodeId?: number;
|
||||||
seasonNumber?: number;
|
seasonNumber?: number;
|
||||||
|
downloadClientHasPostImportCategory: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Queue;
|
export default Queue;
|
||||||
|
Loading…
Reference in New Issue
Block a user