1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-22 22:16:13 -04:00

New: Season Pass is now part of series list

This commit is contained in:
Mark McDowall
2023-01-26 20:26:12 -08:00
committed by Mark McDowall
parent a18c377466
commit bdcfef80d6
20 changed files with 464 additions and 26 deletions
@@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ChangeMonitoringModalContent from './ChangeMonitoringModalContent';
interface ChangeMonitoringModalProps {
isOpen: boolean;
seriesIds: number[];
onSavePress(monitor: string): void;
onModalClose(): void;
}
function ChangeMonitoringModal(props: ChangeMonitoringModalProps) {
const { isOpen, seriesIds, onSavePress, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ChangeMonitoringModalContent
seriesIds={seriesIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default ChangeMonitoringModal;
@@ -0,0 +1,16 @@
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
.selected {
font-weight: bold;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}
@@ -0,0 +1,79 @@
import React, { useCallback, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
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 { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ChangeMonitoringModalContent.css';
const NO_CHANGE = 'noChange';
interface ChangeMonitoringModalContentProps {
seriesIds: number[];
saveError?: object;
onSavePress(monitor: string): void;
onModalClose(): void;
}
function ChangeMonitoringModalContent(
props: ChangeMonitoringModalContentProps
) {
const { seriesIds, onSavePress, onModalClose, ...otherProps } = props;
const [monitor, setMonitor] = useState(NO_CHANGE);
const onInputChange = useCallback(
({ value }) => {
setMonitor(value);
},
[setMonitor]
);
const onSavePressWrapper = useCallback(() => {
onSavePress(monitor);
}, [monitor, onSavePress]);
const selectedCount = seriesIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('Monitor Series')}</ModalHeader>
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Monitoring')}</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_EPISODES_SELECT}
name="monitor"
value={monitor}
includeNoChange={true}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('{count} series selected', { count: selectedCount })}
</div>
<div>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button onPress={onSavePressWrapper}>{translate('Save')}</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
export default ChangeMonitoringModalContent;
@@ -0,0 +1,10 @@
.seasons {
display: flex;
flex-wrap: wrap;
}
.truncated {
align-self: center;
flex: 0 0 100%;
padding: 4px 6px;
}
@@ -0,0 +1,44 @@
import React, { useMemo } from 'react';
import { Season } from 'Series/Series';
import SeasonPassSeason from './SeasonPassSeason';
import styles from './SeasonDetails.css';
interface SeasonDetailsProps {
seriesId: number;
seasons: Season[];
}
function SeasonDetails(props: SeasonDetailsProps) {
const { seriesId, seasons } = props;
const latestSeasons = useMemo(() => {
return seasons.slice(Math.max(seasons.length - 25, 0));
}, [seasons]);
return (
<div className={styles.seasons}>
{latestSeasons.map((season) => {
const { seasonNumber, monitored, statistics, isSaving } = season;
return (
<SeasonPassSeason
key={seasonNumber}
seriesId={seriesId}
seasonNumber={seasonNumber}
monitored={monitored}
statistics={statistics}
isSaving={isSaving}
/>
);
})}
{latestSeasons.length < seasons.length ? (
<div className={styles.truncated}>
Only latest 25 seasons are shown, go to details to see all seasons
</div>
) : null}
</div>
);
}
export default SeasonDetails;
@@ -0,0 +1,25 @@
.season {
display: flex;
align-items: stretch;
overflow: hidden;
margin: 2px 4px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--seasonBackgroundColor);
cursor: default;
}
.info {
padding: 0 4px;
}
.episodes {
padding: 0 4px;
background-color: var(--episodesBackgroundColor);
color: var(--defaultColor);
}
.allEpisodes {
background-color: #e0ffe0;
color: var(--darkGray);
}
@@ -0,0 +1,69 @@
import classNames from 'classnames';
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { Statistics } from 'Series/Series';
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
import padNumber from 'Utilities/Number/padNumber';
import styles from './SeasonPassSeason.css';
interface SeasonPassSeasonProps {
seriesId: number;
seasonNumber: number;
monitored: boolean;
statistics: Statistics;
isSaving: boolean;
}
function SeasonPassSeason(props: SeasonPassSeasonProps) {
const {
seriesId,
seasonNumber,
monitored,
statistics = {
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0,
},
isSaving = false,
} = props;
const { episodeFileCount, totalEpisodeCount, percentOfEpisodes } = statistics;
const dispatch = useDispatch();
const onSeasonMonitoredPress = useCallback(() => {
dispatch(
toggleSeasonMonitored({ seriesId, seasonNumber, monitored: !monitored })
);
}, [seriesId, seasonNumber, monitored, dispatch]);
return (
<div className={styles.season}>
<div className={styles.info}>
<MonitorToggleButton
monitored={monitored}
isSaving={isSaving}
onPress={onSeasonMonitoredPress}
/>
<span>
{seasonNumber === 0 ? 'Specials' : `S${padNumber(seasonNumber, 2)}`}
</span>
</div>
<div
className={classNames(
styles.episodes,
percentOfEpisodes === 100 && styles.allEpisodes
)}
title={`${episodeFileCount}/${totalEpisodeCount} episodes downloaded`}
>
{totalEpisodeCount === 0
? '0/0'
: `${episodeFileCount}/${totalEpisodeCount}`}
</div>
</div>
);
}
export default SeasonPassSeason;
@@ -51,6 +51,10 @@
gap: 20px;
}
.actionButtons {
flex-wrap: wrap;
}
.actionButtons,
.deleteButtons {
display: flex;
@@ -7,13 +7,17 @@ import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { saveSeriesEditor } from 'Store/Actions/seriesActions';
import {
saveSeriesEditor,
updateSeriesMonitor,
} from 'Store/Actions/seriesActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import DeleteSeriesModal from './Delete/DeleteSeriesModal';
import EditSeriesModal from './Edit/EditSeriesModal';
import OrganizeSeriesModal from './Organize/OrganizeSeriesModal';
import ChangeMonitoringModal from './SeasonPass/ChangeMonitoringModal';
import TagsModal from './Tags/TagsModal';
import styles from './SeriesIndexSelectFooter.css';
@@ -21,12 +25,12 @@ const seriesEditorSelector = createSelector(
(state) => state.series,
(series) => {
const { isSaving, isDeleting, deleteError } = series;
return {
isSaving,
isDeleting,
deleteError
}
deleteError,
};
}
);
@@ -43,9 +47,11 @@ function SeriesIndexSelectFooter() {
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false);
const [isTagsModalOpen, setIsTagsModalOpen] = useState(false);
const [isMonitoringModalOpen, setIsMonitoringModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [isSavingSeries, setIsSavingSeries] = useState(false);
const [isSavingTags, setIsSavingTags] = useState(false);
const [isSavingMonitoring, setIsSavingMonitoring] = useState(false);
const [selectState, selectDispatch] = useSelect();
const { selectedState } = selectState;
@@ -111,6 +117,29 @@ function SeriesIndexSelectFooter() {
[seriesIds, dispatch]
);
const onMonitoringPress = useCallback(() => {
setIsMonitoringModalOpen(true);
}, [setIsMonitoringModalOpen]);
const onMonitoringClose = useCallback(() => {
setIsEditModalOpen(false);
}, [setIsMonitoringModalOpen]);
const onMonitoringSavePress = useCallback(
(monitor) => {
setIsSavingMonitoring(true);
setIsMonitoringModalOpen(false);
dispatch(
updateSeriesMonitor({
seriesIds,
monitor,
})
);
},
[seriesIds, dispatch]
);
const onDeletePress = useCallback(() => {
setIsDeleteModalOpen(true);
}, [setIsDeleteModalOpen]);
@@ -123,6 +152,7 @@ function SeriesIndexSelectFooter() {
if (!isSaving) {
setIsSavingSeries(false);
setIsSavingTags(false);
setIsSavingMonitoring(false);
}
}, [isSaving]);
@@ -166,6 +196,14 @@ function SeriesIndexSelectFooter() {
>
{translate('Set Tags')}
</SpinnerButton>
<SpinnerButton
isSpinning={isSaving && isSavingMonitoring}
isDisabled={!anySelected || isOrganizingSeries}
onPress={onMonitoringPress}
>
{translate('Update Monitoring')}
</SpinnerButton>
</div>
<div className={styles.deleteButtons}>
@@ -198,6 +236,13 @@ function SeriesIndexSelectFooter() {
onModalClose={onTagsModalClose}
/>
<ChangeMonitoringModal
isOpen={isMonitoringModalOpen}
seriesIds={seriesIds}
onSavePress={onMonitoringSavePress}
onModalClose={onMonitoringClose}
/>
<OrganizeSeriesModal
isOpen={isOrganizeModalOpen}
seriesIds={seriesIds}