Co-authored-by: Mark McDowall <markus.mcd5@gmail.com>pull/7150/head
parent
4548dcdf97
commit
041fdd3929
@ -0,0 +1,13 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Episode from 'Episode/Episode';
|
||||
|
||||
interface WantedCutoffUnmetAppState extends AppSectionState<Episode> {}
|
||||
|
||||
interface WantedMissingAppState extends AppSectionState<Episode> {}
|
||||
|
||||
interface WantedAppState {
|
||||
cutoffUnmet: WantedCutoffUnmetAppState;
|
||||
missing: WantedMissingAppState;
|
||||
}
|
||||
|
||||
export default WantedAppState;
|
@ -1,60 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EpisodeDetailsModalContentConnector from './EpisodeDetailsModalContentConnector';
|
||||
|
||||
class EpisodeDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
closeOnBackgroundClick: props.selectedTab !== 'search'
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTabChange = (isSearch) => {
|
||||
this.setState({ closeOnBackgroundClick: !isSearch });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={this.state.closeOnBackgroundClick}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EpisodeDetailsModalContentConnector
|
||||
{...otherProps}
|
||||
onTabChange={this.onTabChange}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeDetailsModal.propTypes = {
|
||||
selectedTab: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeDetailsModal;
|
@ -0,0 +1,52 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
|
||||
import { EpisodeEntities } from 'Episode/useEpisode';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EpisodeDetailsModalContent from './EpisodeDetailsModalContent';
|
||||
|
||||
interface EpisodeDetailsModalProps {
|
||||
isOpen: boolean;
|
||||
episodeId: number;
|
||||
episodeEntity: EpisodeEntities;
|
||||
seriesId: number;
|
||||
episodeTitle: string;
|
||||
isSaving?: boolean;
|
||||
showOpenSeriesButton?: boolean;
|
||||
selectedTab?: EpisodeDetailsTab;
|
||||
startInteractiveSearch?: boolean;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function EpisodeDetailsModal(props: EpisodeDetailsModalProps) {
|
||||
const { selectedTab, isOpen, onModalClose, ...otherProps } = props;
|
||||
|
||||
const [closeOnBackgroundClick, setCloseOnBackgroundClick] = useState(
|
||||
selectedTab !== 'search'
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(isSearch: boolean) => {
|
||||
setCloseOnBackgroundClick(!isSearch);
|
||||
},
|
||||
[setCloseOnBackgroundClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={closeOnBackgroundClick}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EpisodeDetailsModalContent
|
||||
{...otherProps}
|
||||
selectedTab={selectedTab}
|
||||
onTabChange={handleTabChange}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeDetailsModal;
|
@ -1,222 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeHistoryConnector from './History/EpisodeHistoryConnector';
|
||||
import EpisodeSearchConnector from './Search/EpisodeSearchConnector';
|
||||
import SeasonEpisodeNumber from './SeasonEpisodeNumber';
|
||||
import EpisodeSummaryConnector from './Summary/EpisodeSummaryConnector';
|
||||
import styles from './EpisodeDetailsModalContent.css';
|
||||
|
||||
const tabs = [
|
||||
'details',
|
||||
'history',
|
||||
'search'
|
||||
];
|
||||
|
||||
class EpisodeDetailsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
selectedTab: props.selectedTab
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTabSelect = (index, lastIndex) => {
|
||||
const selectedTab = tabs[index];
|
||||
this.props.onTabChange(selectedTab === 'search');
|
||||
this.setState({ selectedTab });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
episodeId,
|
||||
episodeEntity,
|
||||
episodeFileId,
|
||||
seriesId,
|
||||
seriesTitle,
|
||||
titleSlug,
|
||||
seriesMonitored,
|
||||
seriesType,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
absoluteEpisodeNumber,
|
||||
episodeTitle,
|
||||
airDate,
|
||||
monitored,
|
||||
isSaving,
|
||||
showOpenSeriesButton,
|
||||
startInteractiveSearch,
|
||||
onMonitorEpisodePress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const seriesLink = `/series/${titleSlug}`;
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
<MonitorToggleButton
|
||||
className={styles.toggleButton}
|
||||
id={episodeId}
|
||||
monitored={monitored}
|
||||
size={18}
|
||||
isDisabled={!seriesMonitored}
|
||||
isSaving={isSaving}
|
||||
onPress={onMonitorEpisodePress}
|
||||
/>
|
||||
|
||||
<span className={styles.seriesTitle}>
|
||||
{seriesTitle}
|
||||
</span>
|
||||
|
||||
<span className={styles.separator}>-</span>
|
||||
|
||||
<SeasonEpisodeNumber
|
||||
seasonNumber={seasonNumber}
|
||||
episodeNumber={episodeNumber}
|
||||
absoluteEpisodeNumber={absoluteEpisodeNumber}
|
||||
airDate={airDate}
|
||||
seriesType={seriesType}
|
||||
/>
|
||||
|
||||
<span className={styles.separator}>-</span>
|
||||
|
||||
{episodeTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
selectedIndex={tabs.indexOf(this.state.selectedTab)}
|
||||
onSelect={this.onTabSelect}
|
||||
>
|
||||
<TabList
|
||||
className={styles.tabList}
|
||||
>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Details')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('History')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Search')}
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<div className={styles.tabContent}>
|
||||
<EpisodeSummaryConnector
|
||||
episodeId={episodeId}
|
||||
episodeEntity={episodeEntity}
|
||||
episodeFileId={episodeFileId}
|
||||
seriesId={seriesId}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<div className={styles.tabContent}>
|
||||
<EpisodeHistoryConnector
|
||||
episodeId={episodeId}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
{/* Don't wrap in tabContent so we not have a top margin */}
|
||||
<EpisodeSearchConnector
|
||||
episodeId={episodeId}
|
||||
startInteractiveSearch={startInteractiveSearch}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
showOpenSeriesButton &&
|
||||
<Button
|
||||
className={styles.openSeriesButton}
|
||||
to={seriesLink}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('OpenSeries')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeDetailsModalContent.propTypes = {
|
||||
episodeId: PropTypes.number.isRequired,
|
||||
episodeEntity: PropTypes.string.isRequired,
|
||||
episodeFileId: PropTypes.number,
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
seriesTitle: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
seriesMonitored: PropTypes.bool.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
episodeNumber: PropTypes.number.isRequired,
|
||||
absoluteEpisodeNumber: PropTypes.number,
|
||||
airDate: PropTypes.string.isRequired,
|
||||
episodeTitle: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
showOpenSeriesButton: PropTypes.bool,
|
||||
selectedTab: PropTypes.string.isRequired,
|
||||
startInteractiveSearch: PropTypes.bool.isRequired,
|
||||
onMonitorEpisodePress: PropTypes.func.isRequired,
|
||||
onTabChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
EpisodeDetailsModalContent.defaultProps = {
|
||||
selectedTab: 'details',
|
||||
episodeEntity: episodeEntities.EPISODES,
|
||||
startInteractiveSearch: false
|
||||
};
|
||||
|
||||
export default EpisodeDetailsModalContent;
|
@ -0,0 +1,204 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Episode from 'Episode/Episode';
|
||||
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import useEpisode, { EpisodeEntities } from 'Episode/useEpisode';
|
||||
import Series from 'Series/Series';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
|
||||
import {
|
||||
cancelFetchReleases,
|
||||
clearReleases,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeHistoryConnector from './History/EpisodeHistoryConnector';
|
||||
import EpisodeSearchConnector from './Search/EpisodeSearchConnector';
|
||||
import SeasonEpisodeNumber from './SeasonEpisodeNumber';
|
||||
import EpisodeSummary from './Summary/EpisodeSummary';
|
||||
import styles from './EpisodeDetailsModalContent.css';
|
||||
|
||||
const TABS: EpisodeDetailsTab[] = ['details', 'history', 'search'];
|
||||
|
||||
export interface EpisodeDetailsModalContentProps {
|
||||
episodeId: number;
|
||||
episodeEntity: EpisodeEntities;
|
||||
seriesId: number;
|
||||
episodeTitle: string;
|
||||
isSaving?: boolean;
|
||||
showOpenSeriesButton?: boolean;
|
||||
selectedTab?: EpisodeDetailsTab;
|
||||
startInteractiveSearch?: boolean;
|
||||
onTabChange(isSearch: boolean): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
||||
const {
|
||||
episodeId,
|
||||
episodeEntity = episodeEntities.EPISODES,
|
||||
seriesId,
|
||||
episodeTitle,
|
||||
isSaving = false,
|
||||
showOpenSeriesButton = false,
|
||||
startInteractiveSearch = false,
|
||||
selectedTab = 'details',
|
||||
onTabChange,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [currentlySelectedTab, setCurrentlySelectedTab] = useState(selectedTab);
|
||||
|
||||
const {
|
||||
title: seriesTitle,
|
||||
titleSlug,
|
||||
monitored: seriesMonitored,
|
||||
seriesType,
|
||||
} = useSeries(seriesId) as Series;
|
||||
|
||||
const {
|
||||
episodeFileId,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
absoluteEpisodeNumber,
|
||||
airDate,
|
||||
monitored,
|
||||
} = useEpisode(episodeId, episodeEntity) as Episode;
|
||||
|
||||
const handleTabSelect = useCallback(
|
||||
(selectedIndex: number) => {
|
||||
const tab = TABS[selectedIndex];
|
||||
onTabChange(tab === 'search');
|
||||
setCurrentlySelectedTab(tab);
|
||||
},
|
||||
[onTabChange]
|
||||
);
|
||||
|
||||
const handleMonitorEpisodePress = useCallback(
|
||||
(monitored: boolean) => {
|
||||
dispatch(
|
||||
toggleEpisodeMonitored({
|
||||
episodeEntity,
|
||||
episodeId,
|
||||
monitored,
|
||||
})
|
||||
);
|
||||
},
|
||||
[episodeEntity, episodeId, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clear pending releases here, so we can reshow the search
|
||||
// results even after switching tabs.
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
const seriesLink = `/series/${titleSlug}`;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
<MonitorToggleButton
|
||||
id={episodeId}
|
||||
monitored={monitored}
|
||||
size={18}
|
||||
isDisabled={!seriesMonitored}
|
||||
isSaving={isSaving}
|
||||
onPress={handleMonitorEpisodePress}
|
||||
/>
|
||||
|
||||
<span className={styles.seriesTitle}>{seriesTitle}</span>
|
||||
|
||||
<span className={styles.separator}>-</span>
|
||||
|
||||
<SeasonEpisodeNumber
|
||||
seasonNumber={seasonNumber}
|
||||
episodeNumber={episodeNumber}
|
||||
absoluteEpisodeNumber={absoluteEpisodeNumber}
|
||||
airDate={airDate}
|
||||
seriesType={seriesType}
|
||||
/>
|
||||
|
||||
<span className={styles.separator}>-</span>
|
||||
|
||||
{episodeTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Tabs
|
||||
className={styles.tabs}
|
||||
selectedIndex={TABS.indexOf(currentlySelectedTab)}
|
||||
onSelect={handleTabSelect}
|
||||
>
|
||||
<TabList className={styles.tabList}>
|
||||
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||
{translate('Details')}
|
||||
</Tab>
|
||||
|
||||
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||
{translate('History')}
|
||||
</Tab>
|
||||
|
||||
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||
{translate('Search')}
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<div className={styles.tabContent}>
|
||||
<EpisodeSummary
|
||||
episodeId={episodeId}
|
||||
episodeEntity={episodeEntity}
|
||||
episodeFileId={episodeFileId}
|
||||
seriesId={seriesId}
|
||||
/>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<div className={styles.tabContent}>
|
||||
<EpisodeHistoryConnector episodeId={episodeId} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
{/* Don't wrap in tabContent so we not have a top margin */}
|
||||
<EpisodeSearchConnector
|
||||
episodeId={episodeId}
|
||||
startInteractiveSearch={startInteractiveSearch}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{showOpenSeriesButton && (
|
||||
<Button
|
||||
className={styles.openSeriesButton}
|
||||
to={seriesLink}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('OpenSeries')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeDetailsModalContent;
|
@ -1,101 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||
import EpisodeDetailsModalContent from './EpisodeDetailsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createEpisodeSelector(),
|
||||
createSeriesSelector(),
|
||||
(episode, series) => {
|
||||
const {
|
||||
title: seriesTitle,
|
||||
titleSlug,
|
||||
monitored: seriesMonitored,
|
||||
seriesType
|
||||
} = series;
|
||||
|
||||
return {
|
||||
seriesTitle,
|
||||
titleSlug,
|
||||
seriesMonitored,
|
||||
seriesType,
|
||||
...episode
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchCancelFetchReleases() {
|
||||
dispatch(cancelFetchReleases());
|
||||
},
|
||||
|
||||
dispatchClearReleases() {
|
||||
dispatch(clearReleases());
|
||||
},
|
||||
|
||||
onMonitorEpisodePress(monitored) {
|
||||
const {
|
||||
episodeId,
|
||||
episodeEntity
|
||||
} = props;
|
||||
|
||||
dispatch(toggleEpisodeMonitored({
|
||||
episodeEntity,
|
||||
episodeId,
|
||||
monitored
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EpisodeDetailsModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentWillUnmount() {
|
||||
// Clear pending releases here, so we can reshow the search
|
||||
// results even after switching tabs.
|
||||
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearReleases();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchCancelFetchReleases,
|
||||
dispatchClearReleases,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EpisodeDetailsModalContent {...otherProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeDetailsModalContentConnector.propTypes = {
|
||||
episodeId: PropTypes.number.isRequired,
|
||||
episodeEntity: PropTypes.string.isRequired,
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
EpisodeDetailsModalContentConnector.defaultProps = {
|
||||
episodeEntity: episodeEntities.EPISODES
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeDetailsModalContentConnector);
|
@ -0,0 +1,3 @@
|
||||
type EpisodeDetailsTab = 'details' | 'history' | 'search';
|
||||
|
||||
export default EpisodeDetailsTab;
|
@ -1,33 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function EpisodeFormats({ formats }) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
formats.map((format) => {
|
||||
return (
|
||||
<Label
|
||||
key={format.id}
|
||||
kind={kinds.INFO}
|
||||
>
|
||||
{format.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
EpisodeFormats.propTypes = {
|
||||
formats: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
EpisodeFormats.defaultProps = {
|
||||
formats: []
|
||||
};
|
||||
|
||||
export default EpisodeFormats;
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
interface EpisodeFormatsProps {
|
||||
formats: CustomFormat[];
|
||||
}
|
||||
|
||||
function EpisodeFormats({ formats }: EpisodeFormatsProps) {
|
||||
return (
|
||||
<div>
|
||||
{formats.map(({ id, name }) => (
|
||||
<Label key={id} kind={kinds.INFO}>
|
||||
{name}
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeFormats;
|
@ -1,86 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeDetailsModal from './EpisodeDetailsModal';
|
||||
import styles from './EpisodeSearchCell.css';
|
||||
|
||||
class EpisodeSearchCell extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onManualSearchPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
episodeId,
|
||||
seriesId,
|
||||
episodeTitle,
|
||||
isSearching,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.episodeSearchCell}>
|
||||
<SpinnerIconButton
|
||||
name={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
onPress={onSearchPress}
|
||||
title={translate('AutomaticSearch')}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onManualSearchPress}
|
||||
title={translate('InteractiveSearch')}
|
||||
/>
|
||||
|
||||
<EpisodeDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
episodeId={episodeId}
|
||||
seriesId={seriesId}
|
||||
episodeTitle={episodeTitle}
|
||||
selectedTab="search"
|
||||
startInteractiveSearch={true}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
{...otherProps}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeSearchCell.propTypes = {
|
||||
episodeId: PropTypes.number.isRequired,
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
episodeTitle: PropTypes.string.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeSearchCell;
|
@ -0,0 +1,75 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { EPISODE_SEARCH } from 'Commands/commandNames';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { EpisodeEntities } from 'Episode/useEpisode';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeDetailsModal from './EpisodeDetailsModal';
|
||||
import styles from './EpisodeSearchCell.css';
|
||||
|
||||
interface EpisodeSearchCellProps {
|
||||
episodeId: number;
|
||||
episodeEntity: EpisodeEntities;
|
||||
seriesId: number;
|
||||
episodeTitle: string;
|
||||
}
|
||||
|
||||
function EpisodeSearchCell(props: EpisodeSearchCellProps) {
|
||||
const { episodeId, episodeEntity, seriesId, episodeTitle } = props;
|
||||
|
||||
const executingCommands = useSelector(createExecutingCommandsSelector());
|
||||
const isSearching = executingCommands.some(({ name, body }) => {
|
||||
const { episodeIds = [] } = body;
|
||||
return name === EPISODE_SEARCH && episodeIds.indexOf(episodeId) > -1;
|
||||
});
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isDetailsModalOpen, setDetailsModalOpen, setDetailsModalClosed] =
|
||||
useModalOpenState(false);
|
||||
|
||||
const handleSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: EPISODE_SEARCH,
|
||||
episodeIds: [episodeId],
|
||||
})
|
||||
);
|
||||
}, [episodeId, dispatch]);
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.episodeSearchCell}>
|
||||
<SpinnerIconButton
|
||||
name={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
title={translate('AutomaticSearch')}
|
||||
onPress={handleSearchPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('InteractiveSearch')}
|
||||
onPress={setDetailsModalOpen}
|
||||
/>
|
||||
|
||||
<EpisodeDetailsModal
|
||||
isOpen={isDetailsModalOpen}
|
||||
episodeId={episodeId}
|
||||
episodeEntity={episodeEntity}
|
||||
seriesId={seriesId}
|
||||
episodeTitle={episodeTitle}
|
||||
selectedTab="search"
|
||||
startInteractiveSearch={true}
|
||||
onModalClose={setDetailsModalClosed}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeSearchCell;
|
@ -1,50 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import EpisodeSearchCell from './EpisodeSearchCell';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { episodeId }) => episodeId,
|
||||
(state, { sceneSeasonNumber }) => sceneSeasonNumber,
|
||||
createSeriesSelector(),
|
||||
createCommandsSelector(),
|
||||
(episodeId, sceneSeasonNumber, series, commands) => {
|
||||
const isSearching = commands.some((command) => {
|
||||
const episodeSearch = command.name === commandNames.EPISODE_SEARCH;
|
||||
|
||||
if (!episodeSearch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
isCommandExecuting(command) &&
|
||||
command.body.episodeIds.indexOf(episodeId) > -1
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
seriesMonitored: series.monitored,
|
||||
seriesType: series.seriesType,
|
||||
isSearching
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onSearchPress(name, path) {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.EPISODE_SEARCH,
|
||||
episodeIds: [props.episodeId]
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSearchCell);
|
@ -1,53 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||
import EpisodeStatus from './EpisodeStatus';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createEpisodeSelector(),
|
||||
createQueueItemSelector(),
|
||||
createEpisodeFileSelector(),
|
||||
(episode, queueItem, episodeFile) => {
|
||||
const result = _.pick(episode, [
|
||||
'airDateUtc',
|
||||
'monitored',
|
||||
'grabbed'
|
||||
]);
|
||||
|
||||
result.queueItem = queueItem;
|
||||
result.episodeFile = episodeFile;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
};
|
||||
|
||||
class EpisodeStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EpisodeStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeStatusConnector.propTypes = {
|
||||
episodeId: PropTypes.number.isRequired,
|
||||
episodeFileId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeStatusConnector);
|
@ -1,130 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SceneInfo.css';
|
||||
|
||||
function SceneInfo(props) {
|
||||
const {
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
sceneSeasonNumber,
|
||||
sceneEpisodeNumber,
|
||||
sceneAbsoluteEpisodeNumber,
|
||||
alternateTitles,
|
||||
seriesType
|
||||
} = props;
|
||||
|
||||
const reducedAlternateTitles = alternateTitles.map((alternateTitle) => {
|
||||
let suffix = '';
|
||||
|
||||
const altSceneSeasonNumber = sceneSeasonNumber === undefined ? seasonNumber : sceneSeasonNumber;
|
||||
const altSceneEpisodeNumber = sceneEpisodeNumber === undefined ? episodeNumber : sceneEpisodeNumber;
|
||||
|
||||
const mappingSeasonNumber = alternateTitle.sceneOrigin === 'tvdb' ? seasonNumber : altSceneSeasonNumber;
|
||||
const altSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined) ? alternateTitle.sceneSeasonNumber : mappingSeasonNumber;
|
||||
const altEpisodeNumber = alternateTitle.sceneOrigin === 'tvdb' ? episodeNumber : altSceneEpisodeNumber;
|
||||
|
||||
if (altEpisodeNumber !== altSceneEpisodeNumber) {
|
||||
suffix = `S${padNumber(altSeasonNumber, 2)}E${padNumber(altEpisodeNumber, 2)}`;
|
||||
} else if (altSeasonNumber !== altSceneSeasonNumber) {
|
||||
suffix = `S${padNumber(altSeasonNumber, 2)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
alternateTitle,
|
||||
title: alternateTitle.title,
|
||||
suffix,
|
||||
comment: alternateTitle.comment
|
||||
};
|
||||
});
|
||||
|
||||
const groupedAlternateTitles = _.map(_.groupBy(reducedAlternateTitles, (item) => `${item.title} ${item.suffix}`), (group) => {
|
||||
return {
|
||||
title: group[0].title,
|
||||
suffix: group[0].suffix,
|
||||
comment: _.uniq(group.map((item) => item.comment)).join('/')
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
{
|
||||
sceneSeasonNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Season')}
|
||||
data={sceneSeasonNumber}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
sceneEpisodeNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Episode')}
|
||||
data={sceneEpisodeNumber}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Absolute')}
|
||||
data={sceneAbsoluteEpisodeNumber}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!alternateTitles.length &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={groupedAlternateTitles.length === 1 ? translate('Title') : translate('Titles')}
|
||||
data={
|
||||
<div>
|
||||
{
|
||||
groupedAlternateTitles.map(({ title, suffix, comment }) => {
|
||||
return (
|
||||
<div
|
||||
key={`${title} ${suffix}`}
|
||||
>
|
||||
{title}
|
||||
{
|
||||
suffix &&
|
||||
<span> ({suffix})</span>
|
||||
}
|
||||
{
|
||||
comment &&
|
||||
<span className={styles.comment}> {comment}</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
SceneInfo.propTypes = {
|
||||
seasonNumber: PropTypes.number,
|
||||
episodeNumber: PropTypes.number,
|
||||
sceneSeasonNumber: PropTypes.number,
|
||||
sceneEpisodeNumber: PropTypes.number,
|
||||
sceneAbsoluteEpisodeNumber: PropTypes.number,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seriesType: PropTypes.string
|
||||
};
|
||||
|
||||
export default SceneInfo;
|
@ -0,0 +1,168 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import { AlternateTitle } from 'Series/Series';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SceneInfo.css';
|
||||
|
||||
interface SceneInfoProps {
|
||||
seasonNumber?: number;
|
||||
episodeNumber?: number;
|
||||
sceneSeasonNumber?: number;
|
||||
sceneEpisodeNumber?: number;
|
||||
sceneAbsoluteEpisodeNumber?: number;
|
||||
alternateTitles: AlternateTitle[];
|
||||
seriesType?: string;
|
||||
}
|
||||
|
||||
function SceneInfo(props: SceneInfoProps) {
|
||||
const {
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
sceneSeasonNumber,
|
||||
sceneEpisodeNumber,
|
||||
sceneAbsoluteEpisodeNumber,
|
||||
alternateTitles,
|
||||
seriesType,
|
||||
} = props;
|
||||
|
||||
const groupedAlternateTitles = useMemo(() => {
|
||||
const reducedAlternateTitles = alternateTitles.map((alternateTitle) => {
|
||||
let suffix = '';
|
||||
|
||||
const altSceneSeasonNumber =
|
||||
sceneSeasonNumber === undefined ? seasonNumber : sceneSeasonNumber;
|
||||
const altSceneEpisodeNumber =
|
||||
sceneEpisodeNumber === undefined ? episodeNumber : sceneEpisodeNumber;
|
||||
|
||||
const mappingSeasonNumber =
|
||||
alternateTitle.sceneOrigin === 'tvdb'
|
||||
? seasonNumber
|
||||
: altSceneSeasonNumber;
|
||||
const altSeasonNumber =
|
||||
alternateTitle.sceneSeasonNumber !== -1 &&
|
||||
alternateTitle.sceneSeasonNumber !== undefined
|
||||
? alternateTitle.sceneSeasonNumber
|
||||
: mappingSeasonNumber;
|
||||
const altEpisodeNumber =
|
||||
alternateTitle.sceneOrigin === 'tvdb'
|
||||
? episodeNumber
|
||||
: altSceneEpisodeNumber;
|
||||
|
||||
if (altEpisodeNumber !== altSceneEpisodeNumber) {
|
||||
suffix = `S${padNumber(altSeasonNumber as number, 2)}E${padNumber(
|
||||
altEpisodeNumber as number,
|
||||
2
|
||||
)}`;
|
||||
} else if (altSeasonNumber !== altSceneSeasonNumber) {
|
||||
suffix = `S${padNumber(altSeasonNumber as number, 2)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
alternateTitle,
|
||||
title: alternateTitle.title,
|
||||
suffix,
|
||||
comment: alternateTitle.comment,
|
||||
};
|
||||
});
|
||||
|
||||
return Object.values(
|
||||
reducedAlternateTitles.reduce(
|
||||
(
|
||||
acc: Record<
|
||||
string,
|
||||
{ title: string; suffix: string; comment: string }
|
||||
>,
|
||||
alternateTitle
|
||||
) => {
|
||||
const key = alternateTitle.suffix
|
||||
? `${alternateTitle.title} ${alternateTitle.suffix}`
|
||||
: alternateTitle.title;
|
||||
const item = acc[key];
|
||||
|
||||
if (item) {
|
||||
item.comment = alternateTitle.comment
|
||||
? `${item.comment}/${alternateTitle.comment}`
|
||||
: item.comment;
|
||||
} else {
|
||||
acc[key] = {
|
||||
title: alternateTitle.title,
|
||||
suffix: alternateTitle.suffix,
|
||||
comment: alternateTitle.comment ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
}, [
|
||||
alternateTitles,
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
sceneSeasonNumber,
|
||||
sceneEpisodeNumber,
|
||||
]);
|
||||
|
||||
return (
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
{sceneSeasonNumber === undefined ? null : (
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Season')}
|
||||
data={sceneSeasonNumber}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sceneEpisodeNumber === undefined ? null : (
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Episode')}
|
||||
data={sceneEpisodeNumber}
|
||||
/>
|
||||
)}
|
||||
|
||||
{seriesType === 'anime' && sceneAbsoluteEpisodeNumber !== undefined ? (
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Absolute')}
|
||||
data={sceneAbsoluteEpisodeNumber}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{alternateTitles.length ? (
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title={
|
||||
groupedAlternateTitles.length === 1
|
||||
? translate('Title')
|
||||
: translate('Titles')
|
||||
}
|
||||
data={
|
||||
<div>
|
||||
{groupedAlternateTitles.map(({ title, suffix, comment }) => {
|
||||
return (
|
||||
<div key={`${title} ${suffix}`}>
|
||||
{title}
|
||||
{suffix && <span> ({suffix})</span>}
|
||||
{comment ? (
|
||||
<span className={styles.comment}> {comment}</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default SceneInfo;
|
@ -1,20 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import EpisodeAiring from './EpisodeAiring';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return _.pick(uiSettings, [
|
||||
'shortDateFormat',
|
||||
'showRelativeDates',
|
||||
'timeFormat'
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(EpisodeAiring);
|
@ -1,205 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MediaInfo from './MediaInfo';
|
||||
import styles from './EpisodeFileRow.css';
|
||||
|
||||
class EpisodeFileRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRemoveEpisodeFileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveEpisodeFilePress = () => {
|
||||
this.setState({ isRemoveEpisodeFileModalOpen: true });
|
||||
};
|
||||
|
||||
onConfirmRemoveEpisodeFile = () => {
|
||||
this.props.onDeleteEpisodeFile();
|
||||
|
||||
this.setState({ isRemoveEpisodeFileModalOpen: false });
|
||||
};
|
||||
|
||||
onRemoveEpisodeFileModalClose = () => {
|
||||
this.setState({ isRemoveEpisodeFileModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
path,
|
||||
size,
|
||||
languages,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
qualityCutoffNotMet,
|
||||
mediaInfo,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.languages}
|
||||
>
|
||||
<EpisodeLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.quality}
|
||||
>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormats}
|
||||
>
|
||||
<EpisodeFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.customFormatScore}
|
||||
>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
mediaInfo ?
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.MEDIA_INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MediaInfo')}
|
||||
body={<MediaInfo {...mediaInfo} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<IconButton
|
||||
title={translate('DeleteEpisodeFromDisk')}
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onRemoveEpisodeFilePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isRemoveEpisodeFileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteEpisodeFile')}
|
||||
message={translate('DeleteEpisodeFileMessage', { path })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmRemoveEpisodeFile}
|
||||
onCancel={this.onRemoveEpisodeFileModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
EpisodeFileRow.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeleteEpisodeFile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeFileRow;
|
@ -0,0 +1,149 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MediaInfo from './MediaInfo';
|
||||
import styles from './EpisodeFileRow.css';
|
||||
|
||||
interface EpisodeFileRowProps {
|
||||
path: string;
|
||||
size: number;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
qualityCutoffNotMet: boolean;
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mediaInfo: object;
|
||||
columns: Column[];
|
||||
onDeleteEpisodeFile(): void;
|
||||
}
|
||||
|
||||
function EpisodeFileRow(props: EpisodeFileRowProps) {
|
||||
const {
|
||||
path,
|
||||
size,
|
||||
languages,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
qualityCutoffNotMet,
|
||||
mediaInfo,
|
||||
columns,
|
||||
onDeleteEpisodeFile,
|
||||
} = props;
|
||||
|
||||
const [
|
||||
isRemoveEpisodeFileModalOpen,
|
||||
setRemoveEpisodeFileModalOpen,
|
||||
setRemoveEpisodeFileModalClosed,
|
||||
] = useModalOpenState(false);
|
||||
|
||||
const handleRemoveEpisodeFilePress = useCallback(() => {
|
||||
onDeleteEpisodeFile();
|
||||
|
||||
setRemoveEpisodeFileModalClosed();
|
||||
}, [onDeleteEpisodeFile, setRemoveEpisodeFileModalClosed]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{columns.map(({ name, isVisible }) => {
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return <TableRowCell key={name}>{path}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.languages}>
|
||||
<EpisodeLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.quality}>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.customFormats}>
|
||||
<EpisodeFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormatScore') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
{mediaInfo ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.MEDIA_INFO} />}
|
||||
title={translate('MediaInfo')}
|
||||
body={<MediaInfo {...mediaInfo} />}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
title={translate('DeleteEpisodeFromDisk')}
|
||||
name={icons.REMOVE}
|
||||
onPress={setRemoveEpisodeFileModalOpen}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isRemoveEpisodeFileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteEpisodeFile')}
|
||||
message={translate('DeleteEpisodeFileMessage', { path })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleRemoveEpisodeFilePress}
|
||||
onCancel={setRemoveEpisodeFileModalClosed}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeFileRow;
|
@ -1,198 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeAiringConnector from './EpisodeAiringConnector';
|
||||
import EpisodeFileRow from './EpisodeFileRow';
|
||||
import styles from './EpisodeSummary.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Path'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('Formats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class EpisodeSummary extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRemoveEpisodeFileModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveEpisodeFilePress = () => {
|
||||
this.setState({ isRemoveEpisodeFileModalOpen: true });
|
||||
};
|
||||
|
||||
onConfirmRemoveEpisodeFile = () => {
|
||||
this.props.onDeleteEpisodeFile();
|
||||
this.setState({ isRemoveEpisodeFileModalOpen: false });
|
||||
};
|
||||
|
||||
onRemoveEpisodeFileModalClose = () => {
|
||||
this.setState({ isRemoveEpisodeFileModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
qualityProfileId,
|
||||
network,
|
||||
overview,
|
||||
airDateUtc,
|
||||
mediaInfo,
|
||||
path,
|
||||
size,
|
||||
languages,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
qualityCutoffNotMet,
|
||||
onDeleteEpisodeFile
|
||||
} = this.props;
|
||||
|
||||
const hasOverview = !!overview;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<span className={styles.infoTitle}>{translate('Airs')}</span>
|
||||
|
||||
<EpisodeAiringConnector
|
||||
airDateUtc={airDateUtc}
|
||||
network={network}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className={styles.infoTitle}>{translate('QualityProfile')}</span>
|
||||
|
||||
<Label
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={styles.overview}>
|
||||
{
|
||||
hasOverview ?
|
||||
overview :
|
||||
translate('NoEpisodeOverview')
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
path ?
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
<EpisodeFileRow
|
||||
path={path}
|
||||
size={size}
|
||||
languages={languages}
|
||||
quality={quality}
|
||||
qualityCutoffNotMet={qualityCutoffNotMet}
|
||||
customFormats={customFormats}
|
||||
customFormatScore={customFormatScore}
|
||||
mediaInfo={mediaInfo}
|
||||
columns={columns}
|
||||
onDeleteEpisodeFile={onDeleteEpisodeFile}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isRemoveEpisodeFileModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteEpisodeFile')}
|
||||
message={translate('DeleteEpisodeFileMessage', { path })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmRemoveEpisodeFile}
|
||||
onCancel={this.onRemoveEpisodeFileModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeSummary.propTypes = {
|
||||
episodeFileId: PropTypes.number.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
network: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
airDateUtc: PropTypes.string.isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
path: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
languages: PropTypes.arrayOf(PropTypes.object),
|
||||
quality: PropTypes.object,
|
||||
qualityCutoffNotMet: PropTypes.bool,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
onDeleteEpisodeFile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EpisodeSummary;
|
@ -0,0 +1,161 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import Episode from 'Episode/Episode';
|
||||
import useEpisode, { EpisodeEntities } from 'Episode/useEpisode';
|
||||
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import {
|
||||
deleteEpisodeFile,
|
||||
fetchEpisodeFile,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeAiring from './EpisodeAiring';
|
||||
import EpisodeFileRow from './EpisodeFileRow';
|
||||
import styles from './EpisodeSummary.css';
|
||||
|
||||
const COLUMNS: Column[] = [
|
||||
{
|
||||
name: 'path',
|
||||
label: () => translate('Path'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('Formats'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
interface EpisodeSummaryProps {
|
||||
seriesId: number;
|
||||
episodeId: number;
|
||||
episodeEntity: EpisodeEntities;
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
||||
function EpisodeSummary(props: EpisodeSummaryProps) {
|
||||
const { seriesId, episodeId, episodeEntity, episodeFileId } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { qualityProfileId, network } = useSeries(seriesId) as Series;
|
||||
|
||||
const { airDateUtc, overview } = useEpisode(
|
||||
episodeId,
|
||||
episodeEntity
|
||||
) as Episode;
|
||||
|
||||
const {
|
||||
path,
|
||||
mediaInfo,
|
||||
size,
|
||||
languages,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
} = useEpisodeFile(episodeFileId) || {};
|
||||
|
||||
const handleDeleteEpisodeFile = useCallback(() => {
|
||||
dispatch(
|
||||
deleteEpisodeFile({
|
||||
id: episodeFileId,
|
||||
episodeEntity,
|
||||
})
|
||||
);
|
||||
}, [episodeFileId, episodeEntity, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (episodeFileId && !path) {
|
||||
dispatch(fetchEpisodeFile({ id: episodeFileId }));
|
||||
}
|
||||
}, [episodeFileId, path, dispatch]);
|
||||
|
||||
const hasOverview = !!overview;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<span className={styles.infoTitle}>{translate('Airs')}</span>
|
||||
|
||||
<EpisodeAiring airDateUtc={airDateUtc} network={network} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className={styles.infoTitle}>{translate('QualityProfile')}</span>
|
||||
|
||||
<Label kind={kinds.PRIMARY} size={sizes.MEDIUM}>
|
||||
<QualityProfileNameConnector qualityProfileId={qualityProfileId} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={styles.overview}>
|
||||
{hasOverview ? overview : translate('NoEpisodeOverview')}
|
||||
</div>
|
||||
|
||||
{path ? (
|
||||
<Table columns={COLUMNS}>
|
||||
<TableBody>
|
||||
<EpisodeFileRow
|
||||
path={path}
|
||||
size={size!}
|
||||
languages={languages!}
|
||||
quality={quality!}
|
||||
qualityCutoffNotMet={qualityCutoffNotMet!}
|
||||
customFormats={customFormats!}
|
||||
customFormatScore={customFormatScore!}
|
||||
mediaInfo={mediaInfo!}
|
||||
columns={COLUMNS}
|
||||
onDeleteEpisodeFile={handleDeleteEpisodeFile}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EpisodeSummary;
|
@ -1,109 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteEpisodeFile, fetchEpisodeFile } from 'Store/Actions/episodeFileActions';
|
||||
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
|
||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||
import EpisodeSummary from './EpisodeSummary';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSeriesSelector(),
|
||||
createEpisodeSelector(),
|
||||
createEpisodeFileSelector(),
|
||||
(series, episode, episodeFile = {}) => {
|
||||
const {
|
||||
qualityProfileId,
|
||||
network
|
||||
} = series;
|
||||
|
||||
const {
|
||||
airDateUtc,
|
||||
overview
|
||||
} = episode;
|
||||
|
||||
const {
|
||||
mediaInfo,
|
||||
path,
|
||||
size,
|
||||
languages,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore
|
||||
} = episodeFile;
|
||||
|
||||
return {
|
||||
network,
|
||||
qualityProfileId,
|
||||
airDateUtc,
|
||||
overview,
|
||||
mediaInfo,
|
||||
path,
|
||||
size,
|
||||
languages,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onDeleteEpisodeFile() {
|
||||
dispatch(deleteEpisodeFile({
|
||||
id: props.episodeFileId,
|
||||
episodeEntity: props.episodeEntity
|
||||
}));
|
||||
},
|
||||
|
||||
dispatchFetchEpisodeFile() {
|
||||
dispatch(fetchEpisodeFile({
|
||||
id: props.episodeFileId
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EpisodeSummaryConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
episodeFileId,
|
||||
path,
|
||||
dispatchFetchEpisodeFile
|
||||
} = this.props;
|
||||
|
||||
if (episodeFileId && !path) {
|
||||
dispatchFetchEpisodeFile({ id: episodeFileId });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchEpisodeFile,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return <EpisodeSummary {...otherProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
EpisodeSummaryConnector.propTypes = {
|
||||
episodeFileId: PropTypes.number,
|
||||
path: PropTypes.string,
|
||||
dispatchFetchEpisodeFile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSummaryConnector);
|
@ -0,0 +1,18 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createEpisodeFileSelector(episodeFileId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodeFiles.items,
|
||||
(episodeFiles) => {
|
||||
return episodeFiles.find(({ id }) => id === episodeFileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function useEpisodeFile(episodeFileId: number | undefined) {
|
||||
return useSelector(createEpisodeFileSelector(episodeFileId));
|
||||
}
|
||||
|
||||
export default useEpisodeFile;
|
@ -1,38 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent';
|
||||
|
||||
function SeasonInteractiveSearchModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SeasonInteractiveSearchModalContent
|
||||
seriesId={seriesId}
|
||||
seasonNumber={seasonNumber}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
SeasonInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SeasonInteractiveSearchModal;
|
@ -0,0 +1,55 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import {
|
||||
cancelFetchReleases,
|
||||
clearReleases,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import SeasonInteractiveSearchModalContent from './SeasonInteractiveSearchModalContent';
|
||||
|
||||
interface SeasonInteractiveSearchModalProps {
|
||||
isOpen: boolean;
|
||||
seriesId: number;
|
||||
seasonNumber: number;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SeasonInteractiveSearchModal(
|
||||
props: SeasonInteractiveSearchModalProps
|
||||
) {
|
||||
const { isOpen, seriesId, seasonNumber, onModalClose } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={handleModalClose}
|
||||
>
|
||||
<SeasonInteractiveSearchModalContent
|
||||
seriesId={seriesId}
|
||||
seasonNumber={seasonNumber}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeasonInteractiveSearchModal;
|
@ -1,59 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import SeasonInteractiveSearchModal from './SeasonInteractiveSearchModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchCancelFetchReleases() {
|
||||
dispatch(cancelFetchReleases());
|
||||
},
|
||||
|
||||
dispatchClearReleases() {
|
||||
dispatch(clearReleases());
|
||||
},
|
||||
|
||||
onModalClose() {
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
props.onModalClose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class SeasonInteractiveSearchModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearReleases();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchCancelFetchReleases,
|
||||
dispatchClearReleases,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<SeasonInteractiveSearchModal
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeasonInteractiveSearchModalConnector.propTypes = {
|
||||
...SeasonInteractiveSearchModal.propTypes,
|
||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(SeasonInteractiveSearchModalConnector);
|
Loading…
Reference in new issue