mirror of
https://github.com/Radarr/Radarr
synced 2025-02-24 07:10:57 +00:00
Fixed: Misc Calendar Improvements
This commit is contained in:
parent
b2c1dbf3ab
commit
5a5e896eb4
13 changed files with 17 additions and 459 deletions
|
@ -37,22 +37,6 @@
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.episodeTitle {
|
||||
flex: 1 1 1px;
|
||||
}
|
||||
|
||||
.seasonEpisodeNumber {
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
.episodeSeparator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.absoluteEpisodeNumber {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
@ -73,18 +57,10 @@
|
|||
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.onAir {
|
||||
composes: onAir from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.missing {
|
||||
composes: missing from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.premiere {
|
||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.event {
|
||||
flex-direction: column;
|
||||
|
@ -101,16 +77,7 @@
|
|||
|
||||
.date,
|
||||
.time,
|
||||
.seriesTitle {
|
||||
.movieTitle {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
.seasonEpisodeNumber {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.episodeSeparator {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,18 +44,10 @@
|
|||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
.absoluteEpisodeNumber {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.airTime {
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
|
@ -97,7 +89,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.unaired {
|
||||
.unreleased {
|
||||
border-left-color: $primaryColor !important;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
|
|
|
@ -11,17 +11,6 @@ import styles from './CalendarEvent.css';
|
|||
|
||||
class CalendarEvent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
// isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -138,7 +127,6 @@ CalendarEvent.propTypes = {
|
|||
timeFormat: PropTypes.string.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired
|
||||
// onEventModalOpenToggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
CalendarEvent.defaultProps = {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
@ -10,13 +9,11 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state) => state.calendar.options,
|
||||
createMovieSelector(),
|
||||
createMovieFileSelector(),
|
||||
createQueueItemSelector(),
|
||||
createUISettingsSelector(),
|
||||
(calendarOptions, movie, movieFile, queueItem, uiSettings) => {
|
||||
(calendarOptions, movie, queueItem, uiSettings) => {
|
||||
return {
|
||||
movie,
|
||||
movieFile,
|
||||
queueItem,
|
||||
...calendarOptions,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
.eventGroup {
|
||||
overflow-x: hidden;
|
||||
margin: 4px 2px;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
border-left: 4px solid $borderColor;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info,
|
||||
.airingInfo {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.seriesTitle {
|
||||
@add-mixin truncate;
|
||||
flex: 1 0 1px;
|
||||
margin-right: 10px;
|
||||
color: #3a3f51;
|
||||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
.airTime {
|
||||
flex: 1 0 1px;
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.episodeInfo {
|
||||
margin-left: 10px;
|
||||
color: $calendarTextDim;
|
||||
}
|
||||
|
||||
.absoluteEpisodeNumber {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.expandContainerInline {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1 0 20px;
|
||||
}
|
||||
|
||||
.expandContainer,
|
||||
.collapseContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.collapseContainer {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
/*
|
||||
* Status
|
||||
*/
|
||||
|
||||
.downloaded {
|
||||
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.downloading {
|
||||
composes: downloading from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.unmonitored {
|
||||
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.onAir {
|
||||
composes: onAir from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.missing {
|
||||
composes: missing from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.premiere {
|
||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.unaired {
|
||||
composes: unaired from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
|
@ -1,200 +0,0 @@
|
|||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
|
||||
import styles from './CalendarEventGroup.css';
|
||||
|
||||
function getEventsInfo(events) {
|
||||
let files = 0;
|
||||
let queued = 0;
|
||||
let monitored = 0;
|
||||
let absoluteEpisodeNumbers = 0;
|
||||
|
||||
events.forEach((event) => {
|
||||
if (event.episodeFileId) {
|
||||
files++;
|
||||
}
|
||||
|
||||
if (event.queued) {
|
||||
queued++;
|
||||
}
|
||||
|
||||
if (event.monitored) {
|
||||
monitored++;
|
||||
}
|
||||
|
||||
if (event.absoluteEpisodeNumber) {
|
||||
absoluteEpisodeNumbers++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
allDownloaded: files === events.length,
|
||||
anyQueued: queued > 0,
|
||||
anyMonitored: monitored > 0,
|
||||
allAbsoluteEpisodeNumbers: absoluteEpisodeNumbers === events.length
|
||||
};
|
||||
}
|
||||
|
||||
class CalendarEventGroup extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isExpanded: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onExpandPress = () => {
|
||||
this.setState({ isExpanded: !this.state.isExpanded });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
series,
|
||||
events,
|
||||
isDownloading,
|
||||
showEpisodeInformation,
|
||||
showFinaleIcon,
|
||||
colorImpairedMode,
|
||||
onEventModalOpenToggle
|
||||
} = this.props;
|
||||
|
||||
const { isExpanded } = this.state;
|
||||
const {
|
||||
allDownloaded,
|
||||
anyQueued,
|
||||
anyMonitored
|
||||
} = getEventsInfo(events);
|
||||
const anyDownloading = isDownloading || anyQueued;
|
||||
const firstEpisode = events[0];
|
||||
const lastEpisode = events[events.length -1];
|
||||
const airDateUtc = firstEpisode.airDateUtc;
|
||||
const startTime = moment(airDateUtc);
|
||||
const endTime = moment(lastEpisode.airDateUtc).add(series.runtime, 'minutes');
|
||||
const seasonNumber = firstEpisode.seasonNumber;
|
||||
const statusStyle = getStatusStyle(allDownloaded, anyDownloading, startTime, endTime, anyMonitored);
|
||||
|
||||
if (isExpanded) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
events.map((event) => {
|
||||
if (event.isGroup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CalendarEventConnector
|
||||
key={event.id}
|
||||
episodeId={event.id}
|
||||
{...event}
|
||||
onEventModalOpenToggle={onEventModalOpenToggle}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.collapseContainer}
|
||||
component="div"
|
||||
onPress={this.onExpandPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.COLLAPSE}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.eventGroup,
|
||||
styles[statusStyle],
|
||||
colorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.seriesTitle}>
|
||||
{series.title}
|
||||
</div>
|
||||
|
||||
{
|
||||
anyDownloading &&
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.DOWNLOADING}
|
||||
title="An episode is downloading"
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.INFO}
|
||||
title={seasonNumber === 1 ? 'Series Premiere' : 'Season Premiere'}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
showFinaleIcon &&
|
||||
lastEpisode.episodeNumber !== 1 &&
|
||||
seasonNumber > 0 &&
|
||||
lastEpisode.episodeNumber === series.seasons.find((season) => season.seasonNumber === seasonNumber).statistics.totalEpisodeCount &&
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.WARNING}
|
||||
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
showEpisodeInformation &&
|
||||
<Link
|
||||
className={styles.expandContainer}
|
||||
component="div"
|
||||
onPress={this.onExpandPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXPAND}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CalendarEventGroup.propTypes = {
|
||||
series: PropTypes.object.isRequired,
|
||||
events: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDownloading: PropTypes.bool.isRequired,
|
||||
showEpisodeInformation: PropTypes.bool.isRequired,
|
||||
showFinaleIcon: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired,
|
||||
onEventModalOpenToggle: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CalendarEventGroup;
|
|
@ -1,37 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import CalendarEventGroup from './CalendarEventGroup';
|
||||
|
||||
function createIsDownloadingSelector() {
|
||||
return createSelector(
|
||||
(state, { movieIds }) => movieIds,
|
||||
(state) => state.queue.details,
|
||||
(movieIds, details) => {
|
||||
return details.items.some((item) => {
|
||||
return item.movie && movieIds.includes(item.movie.id);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.calendar.options,
|
||||
createMovieSelector(),
|
||||
createIsDownloadingSelector(),
|
||||
createUISettingsSelector(),
|
||||
(calendarOptions, movie, isDownloading, uiSettings) => {
|
||||
return {
|
||||
movie,
|
||||
isDownloading,
|
||||
...calendarOptions,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(CalendarEventGroup);
|
|
@ -7,34 +7,11 @@ import styles from './Legend.css';
|
|||
|
||||
function Legend(props) {
|
||||
const {
|
||||
showFinaleIcon,
|
||||
showSpecialIcon,
|
||||
showCutoffUnmetIcon,
|
||||
colorImpairedMode
|
||||
} = props;
|
||||
|
||||
const iconsToShow = [];
|
||||
if (showFinaleIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name="Finale"
|
||||
icon={icons.INFO}
|
||||
kind={kinds.WARNING}
|
||||
tooltip="Series or season finale"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showSpecialIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name="Special"
|
||||
icon={icons.INFO}
|
||||
kind={kinds.PINK}
|
||||
tooltip="Special episode"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showCutoffUnmetIcon) {
|
||||
iconsToShow.push(
|
||||
|
@ -51,8 +28,8 @@ function Legend(props) {
|
|||
<div className={styles.legend}>
|
||||
<div>
|
||||
<LegendItem
|
||||
status="unaired"
|
||||
tooltip="Movie hasn't aired yet"
|
||||
status="unreleased"
|
||||
tooltip="Movie hasn't released yet"
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
/>
|
||||
|
||||
|
@ -77,22 +54,10 @@ function Legend(props) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<LegendIconItem
|
||||
name="Premiere"
|
||||
icon={icons.INFO}
|
||||
kind={kinds.INFO}
|
||||
tooltip="Series or season premiere"
|
||||
/>
|
||||
|
||||
{iconsToShow[0]}
|
||||
</div>
|
||||
|
||||
{
|
||||
iconsToShow.length > 1 &&
|
||||
iconsToShow.length > 0 &&
|
||||
<div>
|
||||
{iconsToShow[1]}
|
||||
{iconsToShow[2]}
|
||||
{iconsToShow[0]}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -100,8 +65,6 @@ function Legend(props) {
|
|||
}
|
||||
|
||||
Legend.propTypes = {
|
||||
showFinaleIcon: PropTypes.bool.isRequired,
|
||||
showSpecialIcon: PropTypes.bool.isRequired,
|
||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired
|
||||
};
|
||||
|
|
|
@ -24,18 +24,10 @@
|
|||
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.onAir {
|
||||
composes: onAir from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.missing {
|
||||
composes: missing from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.premiere {
|
||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
||||
.unaired {
|
||||
composes: unaired from '~Calendar/Events/CalendarEvent.css';
|
||||
.unreleased {
|
||||
composes: unreleased from '~Calendar/Events/CalendarEvent.css';
|
||||
}
|
||||
|
|
|
@ -203,8 +203,6 @@ class CalendarOptionsModalContent extends Component {
|
|||
|
||||
CalendarOptionsModalContent.propTypes = {
|
||||
showMovieInformation: PropTypes.bool.isRequired,
|
||||
showFinaleIcon: PropTypes.bool.isRequired,
|
||||
showSpecialIcon: PropTypes.bool.isRequired,
|
||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||
firstDayOfWeek: PropTypes.number.isRequired,
|
||||
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
||||
|
|
|
@ -20,7 +20,7 @@ function getStatusStyle(hasFile, downloading, startTime, isMonitored) {
|
|||
return 'missing';
|
||||
}
|
||||
|
||||
return 'unaired';
|
||||
return 'unreleased';
|
||||
}
|
||||
|
||||
export default getStatusStyle;
|
||||
|
|
|
@ -17,7 +17,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
function getUrls(state) {
|
||||
const {
|
||||
unmonitored,
|
||||
premieresOnly,
|
||||
asAllDay,
|
||||
tags
|
||||
} = state;
|
||||
|
@ -28,10 +27,6 @@ function getUrls(state) {
|
|||
icalUrl += 'unmonitored=true&';
|
||||
}
|
||||
|
||||
if (premieresOnly) {
|
||||
icalUrl += 'premieresOnly=true&';
|
||||
}
|
||||
|
||||
if (asAllDay) {
|
||||
icalUrl += 'asAllDay=true&';
|
||||
}
|
||||
|
@ -61,7 +56,6 @@ class CalendarLinkModalContent extends Component {
|
|||
|
||||
const defaultState = {
|
||||
unmonitored: false,
|
||||
premieresOnly: false,
|
||||
asAllDay: false,
|
||||
tags: []
|
||||
};
|
||||
|
@ -105,7 +99,6 @@ class CalendarLinkModalContent extends Component {
|
|||
|
||||
const {
|
||||
unmonitored,
|
||||
premieresOnly,
|
||||
asAllDay,
|
||||
tags,
|
||||
iCalHttpUrl,
|
||||
|
@ -132,18 +125,6 @@ class CalendarLinkModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Season Premieres Only</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="premieresOnly"
|
||||
value={premieresOnly}
|
||||
helpText="Only the first episode in a season will be in the feed"
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Show as All-Day Events</FormLabel>
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.SignalR;
|
||||
using Radarr.Api.V3.Movies;
|
||||
|
@ -11,13 +12,16 @@ namespace Radarr.Api.V3.Calendar
|
|||
{
|
||||
public class CalendarModule : RadarrRestModuleWithSignalR<MovieResource, Movie>
|
||||
{
|
||||
protected readonly IMovieService _moviesService;
|
||||
private readonly IMovieService _moviesService;
|
||||
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
|
||||
|
||||
public CalendarModule(IBroadcastSignalRMessage signalR,
|
||||
IMovieService moviesService)
|
||||
IMovieService moviesService,
|
||||
IUpgradableSpecification qualityUpgradableSpecification)
|
||||
: base(signalR, "calendar")
|
||||
{
|
||||
_moviesService = moviesService;
|
||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||
|
||||
GetResourceAll = GetCalendar;
|
||||
}
|
||||
|
@ -59,7 +63,7 @@ protected MovieResource MapToResource(Movie movie)
|
|||
return null;
|
||||
}
|
||||
|
||||
var resource = movie.ToResource();
|
||||
var resource = movie.ToResource(_qualityUpgradableSpecification);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue