New: Show downloading status for series progress bar

Closes #5474
This commit is contained in:
Mark McDowall 2023-03-12 23:51:30 -07:00
parent 6d88a98282
commit ac806a2933
12 changed files with 143 additions and 34 deletions

View File

@ -159,13 +159,15 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
</div> </div>
<SeriesIndexProgressBar <SeriesIndexProgressBar
seriesId={seriesId}
monitored={monitored} monitored={monitored}
status={status} status={status}
episodeCount={episodeCount} episodeCount={episodeCount}
episodeFileCount={episodeFileCount} episodeFileCount={episodeFileCount}
totalEpisodeCount={totalEpisodeCount} totalEpisodeCount={totalEpisodeCount}
posterWidth={posterWidth} width={posterWidth}
detailedProgressBar={overviewOptions.detailedProgressBar} detailedProgressBar={overviewOptions.detailedProgressBar}
isStandalone={false}
/> />
</div> </div>

View File

@ -173,13 +173,15 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
</div> </div>
<SeriesIndexProgressBar <SeriesIndexProgressBar
seriesId={seriesId}
monitored={monitored} monitored={monitored}
status={status} status={status}
episodeCount={episodeCount} episodeCount={episodeCount}
episodeFileCount={episodeFileCount} episodeFileCount={episodeFileCount}
totalEpisodeCount={totalEpisodeCount} totalEpisodeCount={totalEpisodeCount}
posterWidth={posterWidth} width={posterWidth}
detailedProgressBar={detailedProgressBar} detailedProgressBar={detailedProgressBar}
isStandalone={false}
/> />
{showTitle ? <div className={styles.title}>{title}</div> : null} {showTitle ? <div className={styles.title}>{title}</div> : null}

View File

@ -4,7 +4,6 @@
border-radius: 0; border-radius: 0;
background-color: #5b5b5b; background-color: #5b5b5b;
color: var(--white); color: var(--white);
transition: width 200ms ease;
} }
.progressBar { .progressBar {

View File

@ -1,44 +1,66 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import createSeriesQueueItemsDetailsSelector, {
SeriesQueueDetails,
} from 'Series/Index/createSeriesQueueDetailsSelector';
import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; import getProgressBarKind from 'Utilities/Series/getProgressBarKind';
import styles from './SeriesIndexProgressBar.css'; import styles from './SeriesIndexProgressBar.css';
interface SeriesIndexProgressBarProps { interface SeriesIndexProgressBarProps {
seriesId: number;
seasonNumber?: number;
monitored: boolean; monitored: boolean;
status: string; status: string;
episodeCount: number; episodeCount: number;
episodeFileCount: number; episodeFileCount: number;
totalEpisodeCount: number; totalEpisodeCount: number;
posterWidth: number; width: number;
detailedProgressBar: boolean; detailedProgressBar: boolean;
isStandalone: boolean;
} }
function SeriesIndexProgressBar(props: SeriesIndexProgressBarProps) { function SeriesIndexProgressBar(props: SeriesIndexProgressBarProps) {
const { const {
seriesId,
seasonNumber,
monitored, monitored,
status, status,
episodeCount, episodeCount,
episodeFileCount, episodeFileCount,
totalEpisodeCount, totalEpisodeCount,
posterWidth, width,
detailedProgressBar, detailedProgressBar,
isStandalone,
} = props; } = props;
const queueDetails: SeriesQueueDetails = useSelector(
createSeriesQueueItemsDetailsSelector(seriesId, seasonNumber)
);
const newDownloads = queueDetails.count - queueDetails.episodesWithFiles;
const progress = episodeCount ? (episodeFileCount / episodeCount) * 100 : 100; const progress = episodeCount ? (episodeFileCount / episodeCount) * 100 : 100;
const text = `${episodeFileCount} / ${episodeCount}`; const text = newDownloads
? `${episodeFileCount} + ${newDownloads} / ${episodeCount}`
: `${episodeFileCount} / ${episodeCount}`;
return ( return (
<ProgressBar <ProgressBar
className={styles.progressBar} className={styles.progressBar}
containerClassName={styles.progress} containerClassName={isStandalone ? undefined : styles.progress}
progress={progress} progress={progress}
kind={getProgressBarKind(status, monitored, progress)} kind={getProgressBarKind(
status,
monitored,
progress,
queueDetails.count > 0
)}
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL} size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
showText={detailedProgressBar} showText={detailedProgressBar}
text={text} text={text}
title={`${episodeFileCount} / ${episodeCount} (Total: ${totalEpisodeCount})`} title={`${episodeFileCount} / ${episodeCount} (Total: ${totalEpisodeCount}, Downloading: ${queueDetails.count})`}
width={posterWidth} width={width}
/> />
); );
} }

View File

@ -1,4 +1,10 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext'; import { SelectProvider } from 'App/SelectContext';
import { REFRESH_SERIES, RSS_SYNC } from 'Commands/commandNames'; import { REFRESH_SERIES, RSS_SYNC } from 'Commands/commandNames';
@ -16,6 +22,8 @@ import { align, icons } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection'; import SortDirection from 'Helpers/Props/SortDirection';
import NoSeries from 'Series/NoSeries'; import NoSeries from 'Series/NoSeries';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchSeries } from 'Store/Actions/seriesActions';
import { import {
setSeriesFilter, setSeriesFilter,
setSeriesSort, setSeriesSort,
@ -88,6 +96,11 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null); const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [isSelectMode, setIsSelectMode] = useState(false); const [isSelectMode, setIsSelectMode] = useState(false);
useEffect(() => {
dispatch(fetchSeries());
dispatch(fetchQueueDetails({ all: true }));
}, [dispatch]);
const onRefreshSeriesPress = useCallback(() => { const onRefreshSeriesPress = useCallback(() => {
dispatch( dispatch(
executeCommand({ executeCommand({

View File

@ -50,6 +50,12 @@
} }
} }
.downloading {
composes: legendItemColor;
background-color: var(--purple);
}
.statistics { .statistics {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'continuing': string; 'continuing': string;
'downloading': string;
'ended': string; 'ended': string;
'footer': string; 'footer': string;
'legendItem': string; 'legendItem': string;

View File

@ -114,6 +114,16 @@ export default function SeriesIndexFooter() {
/> />
<div>Missing Episodes (Series not monitored)</div> <div>Missing Episodes (Series not monitored)</div>
</div> </div>
<div className={styles.legendItem}>
<div
className={classNames(
styles.downloading,
enableColorImpairedMode && 'colorImpaired'
)}
/>
<div>Downloading (One or more episodes)</div>
</div>
</div> </div>
<div className={styles.statistics}> <div className={styles.statistics}>

View File

@ -24,6 +24,7 @@ import { executeCommand } from 'Store/Actions/commandActions';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; import getProgressBarKind from 'Utilities/Series/getProgressBarKind';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import SeriesIndexProgressBar from '../ProgressBar/SeriesIndexProgressBar';
import hasGrowableColumns from './hasGrowableColumns'; import hasGrowableColumns from './hasGrowableColumns';
import SeasonsCell from './SeasonsCell'; import SeasonsCell from './SeasonsCell';
import selectTableOptions from './selectTableOptions'; import selectTableOptions from './selectTableOptions';
@ -306,19 +307,18 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
} }
if (name === 'episodeProgress') { if (name === 'episodeProgress') {
const progress = episodeCount
? (episodeFileCount / episodeCount) * 100
: 100;
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
<ProgressBar <SeriesIndexProgressBar
progress={progress} seriesId={seriesId}
kind={getProgressBarKind(status, monitored, progress)} monitored={monitored}
showText={true} status={status}
text={`${episodeFileCount} / ${episodeCount}`} episodeCount={episodeCount}
title={`${episodeFileCount} / ${episodeCount} (Total: ${totalEpisodeCount})`} episodeFileCount={episodeFileCount}
totalEpisodeCount={totalEpisodeCount}
width={125} width={125}
detailedProgressBar={true}
isStandalone={true}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
); );
@ -330,21 +330,20 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
} }
const seasonStatistics = latestSeason.statistics || {}; const seasonStatistics = latestSeason.statistics || {};
const progress = seasonStatistics.episodeCount
? (seasonStatistics.episodeFileCount /
seasonStatistics.episodeCount) *
100
: 100;
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
<ProgressBar <SeriesIndexProgressBar
progress={progress} seriesId={seriesId}
kind={getProgressBarKind(status, monitored, progress)} seasonNumber={latestSeason.seasonNumber}
showText={true} monitored={monitored}
text={`${seasonStatistics.episodeFileCount} / ${seasonStatistics.episodeCount}`} status={status}
title={`${seasonStatistics.episodeFileCount} / ${seasonStatistics.episodeCount} (Total: ${seasonStatistics.totalEpisodeCount})`} episodeCount={seasonStatistics.episodeCount}
episodeFileCount={seasonStatistics.episodeFileCount}
totalEpisodeCount={seasonStatistics.totalEpisodeCount}
width={125} width={125}
detailedProgressBar={true}
isStandalone={true}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
); );

View File

@ -0,0 +1,42 @@
import { createSelector } from 'reselect';
export interface SeriesQueueDetails {
count: number;
episodesWithFiles: number;
}
function createSeriesQueueDetailsSelector(
seriesId: number,
seasonNumber?: number
) {
return createSelector(
(state) => state.queue.details.items,
(queueItems) => {
return queueItems.reduce(
(acc: SeriesQueueDetails, item) => {
if (item.seriesId !== seriesId) {
return acc;
}
if (seasonNumber != null && item.seasonNumber !== seasonNumber) {
return acc;
}
acc.count++;
if (item.episodeHasFile) {
acc.episodesWithFiles++;
}
return acc;
},
{
count: 0,
episodesWithFiles: 0,
}
);
}
);
}
export default createSeriesQueueDetailsSelector;

View File

@ -1,6 +1,15 @@
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
function getProgressBarKind(status, monitored, progress) { function getProgressBarKind(
status: string,
monitored: boolean,
progress: number,
isDownloading: boolean
) {
if (isDownloading) {
return kinds.PURPLE;
}
if (progress === 100) { if (progress === 100) {
return status === 'ended' ? kinds.SUCCESS : kinds.PRIMARY; return status === 'ended' ? kinds.SUCCESS : kinds.PRIMARY;
} }

View File

@ -17,6 +17,7 @@ namespace Sonarr.Api.V3.Queue
{ {
public int? SeriesId { get; set; } public int? SeriesId { get; set; }
public int? EpisodeId { get; set; } public int? EpisodeId { get; set; }
public int? SeasonNumber { get; set; }
public SeriesResource Series { get; set; } public SeriesResource Series { get; set; }
public EpisodeResource Episode { get; set; } public EpisodeResource Episode { get; set; }
public List<Language> Languages { get; set; } public List<Language> Languages { get; set; }
@ -37,6 +38,7 @@ namespace Sonarr.Api.V3.Queue
public string DownloadClient { get; set; } public string DownloadClient { get; set; }
public string Indexer { get; set; } public string Indexer { get; set; }
public string OutputPath { get; set; } public string OutputPath { get; set; }
public bool EpisodeHasFile { get; set; }
} }
public static class QueueResourceMapper public static class QueueResourceMapper
@ -53,6 +55,7 @@ namespace Sonarr.Api.V3.Queue
Id = model.Id, Id = model.Id,
SeriesId = model.Series?.Id, SeriesId = model.Series?.Id,
EpisodeId = model.Episode?.Id, EpisodeId = model.Episode?.Id,
SeasonNumber = model.Episode?.SeasonNumber,
Series = includeSeries && model.Series != null ? model.Series.ToResource() : null, Series = includeSeries && model.Series != null ? model.Series.ToResource() : null,
Episode = includeEpisode && model.Episode != null ? model.Episode.ToResource() : null, Episode = includeEpisode && model.Episode != null ? model.Episode.ToResource() : null,
Languages = model.Languages, Languages = model.Languages,
@ -72,7 +75,8 @@ namespace Sonarr.Api.V3.Queue
Protocol = model.Protocol, Protocol = model.Protocol,
DownloadClient = model.DownloadClient, DownloadClient = model.DownloadClient,
Indexer = model.Indexer, Indexer = model.Indexer,
OutputPath = model.OutputPath OutputPath = model.OutputPath,
EpisodeHasFile = model.Episode?.HasFile ?? false
}; };
} }