mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-10-29 23:12:39 +01:00
Convert Episode and Season search to TypeScript
Co-authored-by: Mark McDowall <markus.mcd5@gmail.com>
This commit is contained in:
parent
4548dcdf97
commit
041fdd3929
@ -1,10 +1,10 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||||
import EpisodesAppState from './EpisodesAppState';
|
import EpisodesAppState from './EpisodesAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
@ -12,6 +12,7 @@ import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
|||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
import SystemAppState from './SystemAppState';
|
import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
|
import WantedAppState from './WantedAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
interface FilterBuilderPropOption {
|
||||||
id: string;
|
id: string;
|
||||||
@ -62,8 +63,8 @@ interface AppState {
|
|||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
episodes: EpisodesAppState;
|
|
||||||
episodeFiles: EpisodeFilesAppState;
|
episodeFiles: EpisodeFilesAppState;
|
||||||
|
episodes: EpisodesAppState;
|
||||||
episodesSelection: EpisodesAppState;
|
episodesSelection: EpisodesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
@ -75,6 +76,7 @@ interface AppState {
|
|||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
system: SystemAppState;
|
system: SystemAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
|
wanted: WantedAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppState;
|
export default AppState;
|
||||||
|
13
frontend/src/App/State/WantedAppState.ts
Normal file
13
frontend/src/App/State/WantedAppState.ts
Normal file
@ -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;
|
@ -26,6 +26,7 @@ export interface CommandBody {
|
|||||||
seriesId?: number;
|
seriesId?: number;
|
||||||
seriesIds?: number[];
|
seriesIds?: number[];
|
||||||
seasonNumber?: number;
|
seasonNumber?: number;
|
||||||
|
episodeIds?: number[];
|
||||||
[key: string]: string | number | boolean | number[] | undefined;
|
[key: string]: string | number | boolean | number[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ interface Episode extends ModelBase {
|
|||||||
episodeFile?: object;
|
episodeFile?: object;
|
||||||
hasFile: boolean;
|
hasFile: boolean;
|
||||||
monitored: boolean;
|
monitored: boolean;
|
||||||
|
grabbed?: boolean;
|
||||||
unverifiedSceneNumbering: boolean;
|
unverifiedSceneNumbering: boolean;
|
||||||
endTime?: string;
|
endTime?: string;
|
||||||
grabDate?: string;
|
grabDate?: string;
|
||||||
|
@ -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;
|
|
52
frontend/src/Episode/EpisodeDetailsModal.tsx
Normal file
52
frontend/src/Episode/EpisodeDetailsModal.tsx
Normal file
@ -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;
|
|
204
frontend/src/Episode/EpisodeDetailsModalContent.tsx
Normal file
204
frontend/src/Episode/EpisodeDetailsModalContent.tsx
Normal file
@ -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);
|
|
3
frontend/src/Episode/EpisodeDetailsTab.ts
Normal file
3
frontend/src/Episode/EpisodeDetailsTab.ts
Normal file
@ -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;
|
|
22
frontend/src/Episode/EpisodeFormats.tsx
Normal file
22
frontend/src/Episode/EpisodeFormats.tsx
Normal file
@ -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;
|
|
75
frontend/src/Episode/EpisodeSearchCell.tsx
Normal file
75
frontend/src/Episode/EpisodeSearchCell.tsx
Normal file
@ -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,34 +1,44 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import Episode from 'Episode/Episode';
|
||||||
|
import useEpisode, { EpisodeEntities } from 'Episode/useEpisode';
|
||||||
|
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||||
import isBefore from 'Utilities/Date/isBefore';
|
import isBefore from 'Utilities/Date/isBefore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EpisodeQuality from './EpisodeQuality';
|
import EpisodeQuality from './EpisodeQuality';
|
||||||
import styles from './EpisodeStatus.css';
|
import styles from './EpisodeStatus.css';
|
||||||
|
|
||||||
function EpisodeStatus(props) {
|
interface EpisodeStatusProps {
|
||||||
|
episodeId: number;
|
||||||
|
episodeEntity?: EpisodeEntities;
|
||||||
|
episodeFileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EpisodeStatus(props: EpisodeStatusProps) {
|
||||||
|
const { episodeId, episodeEntity = 'episodes', episodeFileId } = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
airDateUtc,
|
airDateUtc,
|
||||||
monitored,
|
monitored,
|
||||||
grabbed,
|
grabbed = false,
|
||||||
queueItem,
|
} = useEpisode(episodeId, episodeEntity) as Episode;
|
||||||
episodeFile
|
|
||||||
} = props;
|
const queueItem = useSelector(createQueueItemSelectorForHook(episodeId));
|
||||||
|
const episodeFile = useEpisodeFile(episodeFileId);
|
||||||
|
|
||||||
const hasEpisodeFile = !!episodeFile;
|
const hasEpisodeFile = !!episodeFile;
|
||||||
const isQueued = !!queueItem;
|
const isQueued = !!queueItem;
|
||||||
const hasAired = isBefore(airDateUtc);
|
const hasAired = isBefore(airDateUtc);
|
||||||
|
|
||||||
if (isQueued) {
|
if (isQueued) {
|
||||||
const {
|
const { sizeleft, size } = queueItem;
|
||||||
sizeleft,
|
|
||||||
size
|
|
||||||
} = queueItem;
|
|
||||||
|
|
||||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.center}>
|
<div className={styles.center}>
|
||||||
@ -76,10 +86,7 @@ function EpisodeStatus(props) {
|
|||||||
if (!airDateUtc) {
|
if (!airDateUtc) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.center}>
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon name={icons.TBA} title={translate('Tba')} />
|
||||||
name={icons.TBA}
|
|
||||||
title={translate('Tba')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -109,20 +116,9 @@ function EpisodeStatus(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.center}>
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon name={icons.NOT_AIRED} title={translate('EpisodeHasNotAired')} />
|
||||||
name={icons.NOT_AIRED}
|
|
||||||
title={translate('EpisodeHasNotAired')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeStatus.propTypes = {
|
|
||||||
airDateUtc: PropTypes.string,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
grabbed: PropTypes.bool,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
episodeFile: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EpisodeStatus;
|
export default EpisodeStatus;
|
@ -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,13 +1,14 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||||
|
import { EpisodeEntities } from 'Episode/useEpisode';
|
||||||
import FinaleType from './FinaleType';
|
import FinaleType from './FinaleType';
|
||||||
import styles from './EpisodeTitleLink.css';
|
import styles from './EpisodeTitleLink.css';
|
||||||
|
|
||||||
interface EpisodeTitleLinkProps {
|
interface EpisodeTitleLinkProps {
|
||||||
episodeId: number;
|
episodeId: number;
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
episodeEntity: string;
|
episodeEntity: EpisodeEntities;
|
||||||
episodeTitle: string;
|
episodeTitle: string;
|
||||||
finaleType?: string;
|
finaleType?: string;
|
||||||
showOpenSeriesButton: boolean;
|
showOpenSeriesButton: boolean;
|
||||||
|
@ -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;
|
|
168
frontend/src/Episode/SceneInfo.tsx
Normal file
168
frontend/src/Episode/SceneInfo.tsx
Normal file
@ -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,28 +1,29 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import { kinds, sizes } from 'Helpers/Props';
|
import { kinds, sizes } from 'Helpers/Props';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import formatTime from 'Utilities/Date/formatTime';
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
import isInNextWeek from 'Utilities/Date/isInNextWeek';
|
import isInNextWeek from 'Utilities/Date/isInNextWeek';
|
||||||
import isToday from 'Utilities/Date/isToday';
|
import isToday from 'Utilities/Date/isToday';
|
||||||
import isTomorrow from 'Utilities/Date/isTomorrow';
|
import isTomorrow from 'Utilities/Date/isTomorrow';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function EpisodeAiring(props) {
|
interface EpisodeAiringProps {
|
||||||
const {
|
airDateUtc?: string;
|
||||||
airDateUtc,
|
network: string;
|
||||||
network,
|
}
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates,
|
function EpisodeAiring(props: EpisodeAiringProps) {
|
||||||
timeFormat
|
const { airDateUtc, network } = props;
|
||||||
} = props;
|
|
||||||
|
const { shortDateFormat, showRelativeDates, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
const networkLabel = (
|
const networkLabel = (
|
||||||
<Label
|
<Label kind={kinds.INFO} size={sizes.MEDIUM}>
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
>
|
|
||||||
{network}
|
{network}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
@ -31,7 +32,8 @@ function EpisodeAiring(props) {
|
|||||||
if (!airDateUtc) {
|
if (!airDateUtc) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate('AirsTbaOn', { networkLabel: '' })}{networkLabel}
|
{translate('AirsTbaOn', { networkLabel: '' })}
|
||||||
|
{networkLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -41,7 +43,12 @@ function EpisodeAiring(props) {
|
|||||||
if (!showRelativeDates) {
|
if (!showRelativeDates) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel}
|
{translate('AirsDateAtTimeOn', {
|
||||||
|
date: moment(airDateUtc).format(shortDateFormat),
|
||||||
|
time,
|
||||||
|
networkLabel: '',
|
||||||
|
})}
|
||||||
|
{networkLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -49,7 +56,8 @@ function EpisodeAiring(props) {
|
|||||||
if (isToday(airDateUtc)) {
|
if (isToday(airDateUtc)) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate('AirsTimeOn', { time, networkLabel: '' })}{networkLabel}
|
{translate('AirsTimeOn', { time, networkLabel: '' })}
|
||||||
|
{networkLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -57,7 +65,8 @@ function EpisodeAiring(props) {
|
|||||||
if (isTomorrow(airDateUtc)) {
|
if (isTomorrow(airDateUtc)) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate('AirsTomorrowOn', { time, networkLabel: '' })}{networkLabel}
|
{translate('AirsTomorrowOn', { time, networkLabel: '' })}
|
||||||
|
{networkLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,24 +74,26 @@ function EpisodeAiring(props) {
|
|||||||
if (isInNextWeek(airDateUtc)) {
|
if (isInNextWeek(airDateUtc)) {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format('dddd'), time, networkLabel: '' })}{networkLabel}
|
{translate('AirsDateAtTimeOn', {
|
||||||
|
date: moment(airDateUtc).format('dddd'),
|
||||||
|
time,
|
||||||
|
networkLabel: '',
|
||||||
|
})}
|
||||||
|
{networkLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{translate('AirsDateAtTimeOn', { date: moment(airDateUtc).format(shortDateFormat), time, networkLabel: '' })}{networkLabel}
|
{translate('AirsDateAtTimeOn', {
|
||||||
|
date: moment(airDateUtc).format(shortDateFormat),
|
||||||
|
time,
|
||||||
|
networkLabel: '',
|
||||||
|
})}
|
||||||
|
{networkLabel}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeAiring.propTypes = {
|
|
||||||
airDateUtc: PropTypes.string.isRequired,
|
|
||||||
network: PropTypes.string.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EpisodeAiring;
|
export default EpisodeAiring;
|
@ -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;
|
|
149
frontend/src/Episode/Summary/EpisodeFileRow.tsx
Normal file
149
frontend/src/Episode/Summary/EpisodeFileRow.tsx
Normal file
@ -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;
|
|
161
frontend/src/Episode/Summary/EpisodeSummary.tsx
Normal file
161
frontend/src/Episode/Summary/EpisodeSummary.tsx
Normal file
@ -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);
|
|
@ -9,5 +9,5 @@ export default {
|
|||||||
EPISODES,
|
EPISODES,
|
||||||
INTERACTIVE_IMPORT,
|
INTERACTIVE_IMPORT,
|
||||||
WANTED_CUTOFF_UNMET,
|
WANTED_CUTOFF_UNMET,
|
||||||
WANTED_MISSING
|
WANTED_MISSING,
|
||||||
};
|
} as const;
|
@ -5,15 +5,15 @@ import AppState from 'App/State/AppState';
|
|||||||
export type EpisodeEntities =
|
export type EpisodeEntities =
|
||||||
| 'calendar'
|
| 'calendar'
|
||||||
| 'episodes'
|
| 'episodes'
|
||||||
| 'interactiveImport'
|
| 'interactiveImport.episodes'
|
||||||
| 'cutoffUnmet'
|
| 'wanted.cutoffUnmet'
|
||||||
| 'missing';
|
| 'wanted.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) => {
|
||||||
return episodes.find((e) => e.id === episodeId);
|
return episodes.find(({ id }) => id === episodeId);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -22,7 +22,25 @@ function createCalendarEpisodeSelector(episodeId?: number) {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.calendar.items,
|
(state: AppState) => state.calendar.items,
|
||||||
(episodes) => {
|
(episodes) => {
|
||||||
return episodes.find((e) => e.id === episodeId);
|
return episodes.find(({ id }) => id === episodeId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWantedCutoffUnmetEpisodeSelector(episodeId?: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.wanted.cutoffUnmet.items,
|
||||||
|
(episodes) => {
|
||||||
|
return episodes.find(({ id }) => id === episodeId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWantedMissingEpisodeSelector(episodeId?: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.wanted.missing.items,
|
||||||
|
(episodes) => {
|
||||||
|
return episodes.find(({ id }) => id === episodeId);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -37,6 +55,12 @@ function useEpisode(
|
|||||||
case 'calendar':
|
case 'calendar':
|
||||||
selector = createCalendarEpisodeSelector;
|
selector = createCalendarEpisodeSelector;
|
||||||
break;
|
break;
|
||||||
|
case 'wanted.cutoffUnmet':
|
||||||
|
selector = createWantedCutoffUnmetEpisodeSelector;
|
||||||
|
break;
|
||||||
|
case 'wanted.missing':
|
||||||
|
selector = createWantedMissingEpisodeSelector;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ export interface EpisodeFile extends ModelBase {
|
|||||||
languages: Language[];
|
languages: Language[];
|
||||||
quality: QualityModel;
|
quality: QualityModel;
|
||||||
customFormats: CustomFormat[];
|
customFormats: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
releaseType: ReleaseType;
|
releaseType: ReleaseType;
|
||||||
mediaInfo: MediaInfo;
|
mediaInfo: MediaInfo;
|
||||||
|
18
frontend/src/EpisodeFile/useEpisodeFile.ts
Normal file
18
frontend/src/EpisodeFile/useEpisodeFile.ts
Normal file
@ -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;
|
@ -32,6 +32,7 @@ import {
|
|||||||
reprocessInteractiveImportItems,
|
reprocessInteractiveImportItems,
|
||||||
updateInteractiveImportItem,
|
updateInteractiveImportItem,
|
||||||
} from 'Store/Actions/interactiveImportActions';
|
} from 'Store/Actions/interactiveImportActions';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import Rejection from 'typings/Rejection';
|
import Rejection from 'typings/Rejection';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
@ -66,7 +67,7 @@ interface InteractiveImportRowProps {
|
|||||||
languages?: Language[];
|
languages?: Language[];
|
||||||
size: number;
|
size: number;
|
||||||
releaseType: ReleaseType;
|
releaseType: ReleaseType;
|
||||||
customFormats?: object[];
|
customFormats?: CustomFormat[];
|
||||||
customFormatScore?: number;
|
customFormatScore?: number;
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
@ -92,7 +93,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||||||
releaseGroup,
|
releaseGroup,
|
||||||
size,
|
size,
|
||||||
releaseType,
|
releaseType,
|
||||||
customFormats,
|
customFormats = [],
|
||||||
customFormatScore,
|
customFormatScore,
|
||||||
indexerFlags,
|
indexerFlags,
|
||||||
rejections,
|
rejections,
|
||||||
|
@ -4,6 +4,7 @@ import ReleaseType from 'InteractiveImport/ReleaseType';
|
|||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
import { QualityModel } from 'Quality/Quality';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
import Rejection from 'typings/Rejection';
|
import Rejection from 'typings/Rejection';
|
||||||
|
|
||||||
export interface InteractiveImportCommandOptions {
|
export interface InteractiveImportCommandOptions {
|
||||||
@ -33,7 +34,7 @@ interface InteractiveImport extends ModelBase {
|
|||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
episodes: Episode[];
|
episodes: Episode[];
|
||||||
qualityWeight: number;
|
qualityWeight: number;
|
||||||
customFormats: object[];
|
customFormats: CustomFormat[];
|
||||||
indexerFlags: number;
|
indexerFlags: number;
|
||||||
releaseType: ReleaseType;
|
releaseType: ReleaseType;
|
||||||
rejections: Rejection[];
|
rejections: Rejection[];
|
||||||
|
@ -86,9 +86,10 @@ class InteractiveSearchConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearchConnector.propTypes = {
|
InteractiveSearchConnector.propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
searchPayload: PropTypes.object.isRequired,
|
searchPayload: PropTypes.object.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool,
|
||||||
dispatchFetchReleases: PropTypes.func.isRequired
|
dispatchFetchReleases: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||||
|
@ -9,8 +9,8 @@ import Popover from 'Components/Tooltip/Popover';
|
|||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeNumber from 'Episode/EpisodeNumber';
|
import EpisodeNumber from 'Episode/EpisodeNumber';
|
||||||
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
|
||||||
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
|
import EpisodeStatus from 'Episode/EpisodeStatus';
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
import IndexerFlags from 'Episode/IndexerFlags';
|
import IndexerFlags from 'Episode/IndexerFlags';
|
||||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||||
@ -147,6 +147,7 @@ class EpisodeRow extends Component {
|
|||||||
episodeId={id}
|
episodeId={id}
|
||||||
seriesId={seriesId}
|
seriesId={seriesId}
|
||||||
episodeTitle={title}
|
episodeTitle={title}
|
||||||
|
episodeEntity="episodes"
|
||||||
finaleType={finaleType}
|
finaleType={finaleType}
|
||||||
showOpenSeriesButton={false}
|
showOpenSeriesButton={false}
|
||||||
/>
|
/>
|
||||||
@ -351,7 +352,7 @@ class EpisodeRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.status}
|
className={styles.status}
|
||||||
>
|
>
|
||||||
<EpisodeStatusConnector
|
<EpisodeStatus
|
||||||
episodeId={id}
|
episodeId={id}
|
||||||
episodeFileId={episodeFileId}
|
episodeFileId={episodeFileId}
|
||||||
/>
|
/>
|
||||||
@ -361,9 +362,10 @@ class EpisodeRow extends Component {
|
|||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<EpisodeSearchCellConnector
|
<EpisodeSearchCell
|
||||||
key={name}
|
key={name}
|
||||||
episodeId={id}
|
episodeId={id}
|
||||||
|
episodeEntity='episodes'
|
||||||
seriesId={seriesId}
|
seriesId={seriesId}
|
||||||
episodeTitle={title}
|
episodeTitle={title}
|
||||||
/>
|
/>
|
||||||
|
@ -18,7 +18,7 @@ import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
|
|||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
||||||
import SeasonInteractiveSearchModalConnector from 'Series/Search/SeasonInteractiveSearchModalConnector';
|
import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal';
|
||||||
import isAfter from 'Utilities/Date/isAfter';
|
import isAfter from 'Utilities/Date/isAfter';
|
||||||
import isBefore from 'Utilities/Date/isBefore';
|
import isBefore from 'Utilities/Date/isBefore';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
@ -505,7 +505,7 @@ class SeriesDetailsSeason extends Component {
|
|||||||
onModalClose={this.onHistoryModalClose}
|
onModalClose={this.onHistoryModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SeasonInteractiveSearchModalConnector
|
<SeasonInteractiveSearchModal
|
||||||
isOpen={isInteractiveSearchModalOpen}
|
isOpen={isInteractiveSearchModalOpen}
|
||||||
seriesId={seriesId}
|
seriesId={seriesId}
|
||||||
seasonNumber={seasonNumber}
|
seasonNumber={seasonNumber}
|
||||||
|
@ -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;
|
|
55
frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx
Normal file
55
frontend/src/Series/Search/SeasonInteractiveSearchModal.tsx
Normal file
@ -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);
|
|
@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
@ -10,20 +9,25 @@ import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConne
|
|||||||
import formatSeason from 'Season/formatSeason';
|
import formatSeason from 'Season/formatSeason';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function SeasonInteractiveSearchModalContent(props) {
|
interface SeasonInteractiveSearchModalContentProps {
|
||||||
const {
|
seriesId: number;
|
||||||
seriesId,
|
seasonNumber: number;
|
||||||
seasonNumber,
|
onModalClose(): void;
|
||||||
onModalClose
|
}
|
||||||
} = props;
|
|
||||||
|
function SeasonInteractiveSearchModalContent(
|
||||||
|
props: SeasonInteractiveSearchModalContentProps
|
||||||
|
) {
|
||||||
|
const { seriesId, seasonNumber, onModalClose } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{seasonNumber === null ?
|
{seasonNumber === null
|
||||||
translate('InteractiveSearchModalHeader') :
|
? translate('InteractiveSearchModalHeader')
|
||||||
translate('InteractiveSearchModalHeaderSeason', { season: formatSeason(seasonNumber) })
|
: translate('InteractiveSearchModalHeaderSeason', {
|
||||||
}
|
season: formatSeason(seasonNumber) as string,
|
||||||
|
})}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
@ -31,24 +35,16 @@ function SeasonInteractiveSearchModalContent(props) {
|
|||||||
type="season"
|
type="season"
|
||||||
searchPayload={{
|
searchPayload={{
|
||||||
seriesId,
|
seriesId,
|
||||||
seasonNumber
|
seasonNumber,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SeasonInteractiveSearchModalContent.propTypes = {
|
|
||||||
seriesId: PropTypes.number.isRequired,
|
|
||||||
seasonNumber: PropTypes.number.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SeasonInteractiveSearchModalContent;
|
export default SeasonInteractiveSearchModalContent;
|
@ -53,6 +53,7 @@ export interface AlternateTitle {
|
|||||||
sceneSeasonNumber?: number;
|
sceneSeasonNumber?: number;
|
||||||
title: string;
|
title: string;
|
||||||
sceneOrigin: 'unknown' | 'unknown:tvdb' | 'mixed' | 'tvdb';
|
sceneOrigin: 'unknown' | 'unknown:tvdb' | 'mixed' | 'tvdb';
|
||||||
|
comment?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SeriesAddOptions {
|
export interface SeriesAddOptions {
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
export function createQueueItemSelectorForHook(episodeId: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.details.items,
|
||||||
|
(details) => {
|
||||||
|
if (!episodeId || !details) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.find((item) => item.episodeId === episodeId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createQueueItemSelector() {
|
function createQueueItemSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(_: AppState, { episodeId }: { episodeId: number }) => episodeId,
|
(_: AppState, { episodeId }: { episodeId: number }) => episodeId,
|
||||||
|
@ -5,8 +5,7 @@ export function createSeriesSelectorForHook(seriesId) {
|
|||||||
(state) => state.series.itemMap,
|
(state) => state.series.itemMap,
|
||||||
(state) => state.series.items,
|
(state) => state.series.items,
|
||||||
(itemMap, allSeries) => {
|
(itemMap, allSeries) => {
|
||||||
|
return seriesId ? allSeries[itemMap[seriesId]] : undefined;
|
||||||
return seriesId ? allSeries[itemMap[seriesId]]: undefined;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
|
||||||
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
|
import EpisodeStatus from 'Episode/EpisodeStatus';
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
|
||||||
@ -125,7 +125,7 @@ function CutoffUnmetRow(props) {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.status}
|
className={styles.status}
|
||||||
>
|
>
|
||||||
<EpisodeStatusConnector
|
<EpisodeStatus
|
||||||
episodeId={id}
|
episodeId={id}
|
||||||
episodeFileId={episodeFileId}
|
episodeFileId={episodeFileId}
|
||||||
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
|
episodeEntity={episodeEntities.WANTED_CUTOFF_UNMET}
|
||||||
@ -136,7 +136,7 @@ function CutoffUnmetRow(props) {
|
|||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<EpisodeSearchCellConnector
|
<EpisodeSearchCell
|
||||||
key={name}
|
key={name}
|
||||||
episodeId={id}
|
episodeId={id}
|
||||||
seriesId={series.id}
|
seriesId={series.id}
|
||||||
|
@ -5,8 +5,8 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
|
import EpisodeSearchCell from 'Episode/EpisodeSearchCell';
|
||||||
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
|
import EpisodeStatus from 'Episode/EpisodeStatus';
|
||||||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||||
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
|
||||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||||
@ -115,7 +115,7 @@ function MissingRow(props) {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.status}
|
className={styles.status}
|
||||||
>
|
>
|
||||||
<EpisodeStatusConnector
|
<EpisodeStatus
|
||||||
episodeId={id}
|
episodeId={id}
|
||||||
episodeFileId={episodeFileId}
|
episodeFileId={episodeFileId}
|
||||||
episodeEntity={episodeEntities.WANTED_MISSING}
|
episodeEntity={episodeEntities.WANTED_MISSING}
|
||||||
@ -126,7 +126,7 @@ function MissingRow(props) {
|
|||||||
|
|
||||||
if (name === 'actions') {
|
if (name === 'actions') {
|
||||||
return (
|
return (
|
||||||
<EpisodeSearchCellConnector
|
<EpisodeSearchCell
|
||||||
key={name}
|
key={name}
|
||||||
episodeId={id}
|
episodeId={id}
|
||||||
seriesId={series.id}
|
seriesId={series.id}
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
"react-slider": "1.1.4",
|
"react-slider": "1.1.4",
|
||||||
"react-tabs": "3.2.2",
|
"react-tabs": "4.3.0",
|
||||||
"react-text-truncate": "0.18.0",
|
"react-text-truncate": "0.18.0",
|
||||||
"react-use-measure": "2.1.1",
|
"react-use-measure": "2.1.1",
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
|
@ -5817,10 +5817,10 @@ react-slider@1.1.4:
|
|||||||
resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-1.1.4.tgz#08b55f9be3e04cc10ae00cc3aedb6891dffe9bf3"
|
resolved "https://registry.yarnpkg.com/react-slider/-/react-slider-1.1.4.tgz#08b55f9be3e04cc10ae00cc3aedb6891dffe9bf3"
|
||||||
integrity sha512-lL/MvzFcDue0ztdJItwLqas2lOy8Gg46eCDGJc4cJGldThmBHcHfGQePgBgyY1SEN95OwsWAakd3SuI8RyixDQ==
|
integrity sha512-lL/MvzFcDue0ztdJItwLqas2lOy8Gg46eCDGJc4cJGldThmBHcHfGQePgBgyY1SEN95OwsWAakd3SuI8RyixDQ==
|
||||||
|
|
||||||
react-tabs@3.2.2:
|
react-tabs@4.3.0:
|
||||||
version "3.2.2"
|
version "4.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-3.2.2.tgz#07bdc3cdb17bdffedd02627f32a93cd4b3d6e4d0"
|
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-4.3.0.tgz#9f4db0fd209ba4ab2c1e78993ff964435f84af62"
|
||||||
integrity sha512-/o52eGKxFHRa+ssuTEgSM8qORnV4+k7ibW+aNQzKe+5gifeVz8nLxCrsI9xdRhfb0wCLdgIambIpb1qCxaMN+A==
|
integrity sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx "^1.1.0"
|
clsx "^1.1.0"
|
||||||
prop-types "^15.5.0"
|
prop-types "^15.5.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user