From ab6233dd3fd65add49b1bdf589557aa576a91348 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 28 Mar 2015 09:10:30 -0700 Subject: [PATCH] New: Option to show unmonitored episodes on calendar --- .../Calendar/CalendarFeedModule.cs | 2 +- src/NzbDrone.Api/Calendar/CalendarModule.cs | 5 +- .../EpisodesBetweenDatesFixture.cs | 2 +- .../IndexerSearch/EpisodeSearchService.cs | 2 +- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 22 ++++-- src/NzbDrone.Core/Tv/EpisodeService.cs | 6 +- .../{Collection.js => CalendarCollection.js} | 5 +- src/UI/Calendar/CalendarLayout.js | 74 +++++++++++++++++-- src/UI/Calendar/CalendarLayoutTemplate.hbs | 19 ++--- src/UI/Calendar/CalendarView.js | 22 +++++- src/UI/Calendar/UpcomingCollectionView.js | 13 +++- src/UI/Calendar/calendar.less | 36 ++++++++- src/UI/Handlebars/Helpers/Episode.js | 5 ++ src/UI/Shared/Toolbar/ToolbarLayout.js | 6 +- .../Shared/Toolbar/ToolbarLayoutTemplate.hbs | 4 +- 15 files changed, 176 insertions(+), 47 deletions(-) rename src/UI/Calendar/{Collection.js => CalendarCollection.js} (71%) diff --git a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs index 14a07e2dd..90450e610 100644 --- a/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarFeedModule.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Api.Calendar if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); - var episodes = _episodeService.EpisodesBetweenDates(start, end); + var episodes = _episodeService.EpisodesBetweenDates(start, end, false); var icalCalendar = new iCalendar(); foreach (var episode in episodes.OrderBy(v => v.AirDateUtc.Value)) diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs index d8f234326..c89d5f52d 100644 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarModule.cs @@ -23,14 +23,17 @@ namespace NzbDrone.Api.Calendar { var start = DateTime.Today; var end = DateTime.Today.AddDays(2); + var includeUnmonitored = false; var queryStart = Request.Query.Start; var queryEnd = Request.Query.End; + var queryIncludeUnmonitored = Request.Query.Unmonitored; if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); + if (queryIncludeUnmonitored.HasValue) includeUnmonitored = Convert.ToBoolean(queryIncludeUnmonitored.Value); - var resources = ToListResource(() => _episodeService.EpisodesBetweenDates(start, end)); + var resources = ToListResource(() => _episodeService.EpisodesBetweenDates(start, end, includeUnmonitored)); return resources.OrderBy(e => e.AirDateUtc).ToList(); } diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs index dbb2861e7..10cb1393f 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesBetweenDatesFixture.cs @@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests [Test] public void should_get_episodes() { - var episodes = Subject.EpisodesBetweenDates(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(3)); + var episodes = Subject.EpisodesBetweenDates(DateTime.Today.AddDays(-1), DateTime.Today.AddDays(3), false); episodes.Should().HaveCount(1); } } diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index d5eacd707..aeb0ddf70 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -47,7 +47,7 @@ namespace NzbDrone.Core.IndexerSearch public void MissingEpisodesAiredAfter(DateTime dateTime, IEnumerable grabbed) { - var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow) + var missing = _episodeService.EpisodesBetweenDates(dateTime, DateTime.UtcNow, false) .Where(e => !e.HasFile && !_queueService.GetQueue().Select(q => q.Episode.Id).Contains(e.Id) && !grabbed.Contains(e.Id)) diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 77ac3f756..28116079d 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -26,7 +26,7 @@ namespace NzbDrone.Core.Tv PagingSpec EpisodesWhereCutoffUnmet(PagingSpec pagingSpec, List qualitiesBelowCutoff, bool includeSpecials); List FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); - List EpisodesBetweenDates(DateTime startDate, DateTime endDate); + List EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); void SetMonitoredFlat(Episode episode, bool monitored); void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored); void SetFileId(int episodeId, int fileId); @@ -151,14 +151,20 @@ namespace NzbDrone.Core.Tv return episodes.Single(); } - public List EpisodesBetweenDates(DateTime startDate, DateTime endDate) + public List EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored) { - return Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) - .Where(e => e.AirDateUtc >= startDate) - .AndWhere(e => e.AirDateUtc <= endDate) - .AndWhere(e => e.Monitored) - .AndWhere(e => e.Series.Monitored) - .ToList(); + var query = Query.Join(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) + .Where(e => e.AirDateUtc >= startDate) + .AndWhere(e => e.AirDateUtc <= endDate); + + + if (!includeUnmonitored) + { + query.AndWhere(e => e.Monitored) + .AndWhere(e => e.Series.Monitored); + } + + return query.ToList(); } public void SetMonitoredFlat(Episode episode, bool monitored) diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index edda6ce28..e2d7cd39d 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Tv void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); void UpdateEpisodes(List episodes); - List EpisodesBetweenDates(DateTime start, DateTime end); + List EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); void InsertMany(List episodes); void UpdateMany(List episodes); void DeleteMany(List episodes); @@ -169,9 +169,9 @@ namespace NzbDrone.Core.Tv _episodeRepository.UpdateMany(episodes); } - public List EpisodesBetweenDates(DateTime start, DateTime end) + public List EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored) { - var episodes = _episodeRepository.EpisodesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime()); + var episodes = _episodeRepository.EpisodesBetweenDates(start.ToUniversalTime(), end.ToUniversalTime(), includeUnmonitored); return episodes; } diff --git a/src/UI/Calendar/Collection.js b/src/UI/Calendar/CalendarCollection.js similarity index 71% rename from src/UI/Calendar/Collection.js rename to src/UI/Calendar/CalendarCollection.js index 63c3f1b69..12739955c 100644 --- a/src/UI/Calendar/Collection.js +++ b/src/UI/Calendar/CalendarCollection.js @@ -2,8 +2,9 @@ var Backbone = require('backbone'); var EpisodeModel = require('../Series/EpisodeModel'); module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/calendar', - model : EpisodeModel, + url : window.NzbDrone.ApiRoot + '/calendar', + model : EpisodeModel, + tableName : 'calendar', comparator : function(model) { var date = new Date(model.get('airDateUtc')); diff --git a/src/UI/Calendar/CalendarLayout.js b/src/UI/Calendar/CalendarLayout.js index efc5707c2..15aa74dfe 100644 --- a/src/UI/Calendar/CalendarLayout.js +++ b/src/UI/Calendar/CalendarLayout.js @@ -3,34 +3,94 @@ var Marionette = require('marionette'); var UpcomingCollectionView = require('./UpcomingCollectionView'); var CalendarView = require('./CalendarView'); var CalendarFeedView = require('./CalendarFeedView'); +var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout'); module.exports = Marionette.Layout.extend({ template : 'Calendar/CalendarLayoutTemplate', regions : { upcoming : '#x-upcoming', - calendar : '#x-calendar' - }, - - events : { - 'click .x-ical' : '_showiCal' + calendar : '#x-calendar', + toolbar : '#x-toolbar' }, onShow : function() { this._showUpcoming(); this._showCalendar(); + this._showToolbar(); }, _showUpcoming : function() { - this.upcoming.show(new UpcomingCollectionView()); + this.upcomingView = new UpcomingCollectionView(); + this.upcoming.show(this.upcomingView); }, _showCalendar : function() { - this.calendar.show(new CalendarView()); + this.calendarView = new CalendarView(); + this.calendar.show(this.calendarView); }, _showiCal : function() { var view = new CalendarFeedView(); AppLayout.modalRegion.show(view); + }, + + _showToolbar : function() { + var leftSideButtons = { + type : 'default', + storeState : false, + items : [ + { + title : 'Get iCal Link', + icon : 'icon-sonarr-calendar-o', + callback : this._showiCal, + ownerContext : this + } + ] + }; + + var filterOptions = { + type : 'radio', + storeState : true, + menuKey : 'calendar.show', + defaultAction : 'monitored', + items : [ + { + key : 'all', + title : '', + tooltip : 'All', + icon : 'icon-sonarr-all', + callback : this._setCalendarFilter + }, + { + key : 'monitored', + title : '', + tooltip : 'Monitored Only', + icon : 'icon-sonarr-monitored', + callback : this._setCalendarFilter + } + ] + }; + + this.toolbar.show(new ToolbarLayout({ + left : [leftSideButtons], + right : [filterOptions], + context : this, + floatOnMobile : true + })); + }, + + _setCalendarFilter : function(buttonContext) { + var mode = buttonContext.model.get('key'); + + if (mode === 'all') { + this.calendarView.setShowUnmonitored(true); + this.upcomingView.setShowUnmonitored(true); + } + + else { + this.calendarView.setShowUnmonitored(false); + this.upcomingView.setShowUnmonitored(false); + } } }); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarLayoutTemplate.hbs b/src/UI/Calendar/CalendarLayoutTemplate.hbs index 9c6136905..db8c097be 100644 --- a/src/UI/Calendar/CalendarLayoutTemplate.hbs +++ b/src/UI/Calendar/CalendarLayoutTemplate.hbs @@ -3,23 +3,20 @@

Upcoming

-
-

- -

-
+
    -
  • Unaired Premiere
  • -
  • Unaired
  • -
  • On Air
  • -
  • Downloading
  • -
  • Missing
  • -
  • Downloaded
  • +
  • Unaired Premiere
  • +
  • Unaired
  • +
  • On Air
  • +
  • Downloading
  • +
  • Missing
  • +
  • Downloaded
  • +
  • Unmonitored
diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js index 17918f773..6558a80e6 100644 --- a/src/UI/Calendar/CalendarView.js +++ b/src/UI/Calendar/CalendarView.js @@ -2,10 +2,11 @@ var $ = require('jquery'); var vent = require('vent'); var Marionette = require('marionette'); var moment = require('moment'); -var CalendarCollection = require('./Collection'); +var CalendarCollection = require('./CalendarCollection'); var UiSettings = require('../Shared/UiSettingsModel'); var QueueCollection = require('../Activity/Queue/QueueCollection'); var Config = require('../Config'); + require('../Mixins/backbone.signalr.mixin'); require('fullcalendar'); require('jquery.easypiechart'); @@ -14,6 +15,7 @@ module.exports = Marionette.ItemView.extend({ storageKey : 'calendar.view', initialize : function() { + this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all'; this.collection = new CalendarCollection().bindSignalR({ updateOnly : true }); this.listenTo(this.collection, 'change', this._reloadCalendarEvents); this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents); @@ -27,6 +29,13 @@ module.exports = Marionette.ItemView.extend({ this.$('.fc-button-today').click(); }, + setShowUnmonitored : function (showUnmonitored) { + if (this.showUnmonitored !== showUnmonitored) { + this.showUnmonitored = showUnmonitored; + this._getEvents(this.$el.fullCalendar('getView')); + } + }, + _viewRender : function(view) { if ($(window).width() < 768) { this.$('.fc-header-title').show(); @@ -105,8 +114,9 @@ module.exports = Marionette.ItemView.extend({ this.collection.fetch({ data : { - start : start, - end : end + start : start, + end : end, + unmonitored : this.showUnmonitored }, success : this._setEventData.bind(this) }); @@ -145,6 +155,7 @@ module.exports = Marionette.ItemView.extend({ var currentTime = moment(); var start = moment(element.get('airDateUtc')); var end = moment(endTime); + var monitored = element.get('series').monitored && element.get('monitored'); var statusLevel = 'primary'; @@ -156,6 +167,10 @@ module.exports = Marionette.ItemView.extend({ statusLevel = 'purple'; } + else if (!monitored) { + statusLevel = 'unmonitored'; + } + else if (currentTime.isAfter(start) && currentTime.isBefore(end)) { statusLevel = 'warning'; } @@ -231,6 +246,7 @@ module.exports = Marionette.ItemView.extend({ return options; }, + _addStatusIcon : function(element, icon, tooltip) { this.$(element).find('.fc-event-time').after(''.format(icon)); this.$(element).find('.status').tooltip({ diff --git a/src/UI/Calendar/UpcomingCollectionView.js b/src/UI/Calendar/UpcomingCollectionView.js index 29d33c22d..9a8944f3d 100644 --- a/src/UI/Calendar/UpcomingCollectionView.js +++ b/src/UI/Calendar/UpcomingCollectionView.js @@ -2,14 +2,16 @@ var _ = require('underscore'); var Marionette = require('marionette'); var UpcomingCollection = require('./UpcomingCollection'); var UpcomingItemView = require('./UpcomingItemView'); +var Config = require('../Config'); require('../Mixins/backbone.signalr.mixin'); module.exports = Marionette.CollectionView.extend({ itemView : UpcomingItemView, initialize : function() { + this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all'; this.collection = new UpcomingCollection().bindSignalR({ updateOnly : true }); - this.collection.fetch(); + this._fetchCollection(); this._fetchCollection = _.bind(this._fetchCollection, this); this.timer = window.setInterval(this._fetchCollection, 60 * 60 * 1000); @@ -19,7 +21,14 @@ module.exports = Marionette.CollectionView.extend({ window.clearInterval(this.timer); }, + setShowUnmonitored : function (showUnmonitored) { + if (this.showUnmonitored !== showUnmonitored) { + this.showUnmonitored = showUnmonitored; + this._fetchCollection(); + } + }, + _fetchCollection : function() { - this.collection.fetch(); + this.collection.fetch({ data: { unmonitored : this.showUnmonitored }}); } }); \ No newline at end of file diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less index 32a1c8955..2d3fa2cc2 100644 --- a/src/UI/Calendar/calendar.less +++ b/src/UI/Calendar/calendar.less @@ -36,7 +36,7 @@ } .fc-state-highlight { - background : #f1f1f1; + background : #dbdbdb; } .past { @@ -119,17 +119,21 @@ border-color : @droneTeal; } + .unmonitored { + border-color : grey; + } + .episode-title { .btn-link; .text-overflow; color : @link-color; margin-top : 1px; display : inline-block; - + @media (max-width: @screen-xs-min) { width : 140px; } - + @media (min-width: @screen-md-min) { width : 135px; } @@ -138,7 +142,7 @@ .calendar { - background-position : -160px -128px; +// background-position : -160px -128px; .primary { border-color : @btn-primary-bg; @@ -185,10 +189,28 @@ background-color : @droneTeal; } + .unmonitored { + border-color : grey; + background-color : grey; + } + .chart { margin-top : 2px; margin-right: 2px; } + + .legend-labels { + width : 500px; + + @media (max-width: @screen-xs-min) { + width : 400px; + } + } + + .legend-label { + display : inline-block; + width : 150px; + } } .ical { @@ -211,3 +233,9 @@ margin-bottom : 5px; } } + +.calendar-toolbar { + .page-toolbar { + margin-bottom : 10px; + } +} diff --git a/src/UI/Handlebars/Helpers/Episode.js b/src/UI/Handlebars/Helpers/Episode.js index 96f3f392a..154236489 100644 --- a/src/UI/Handlebars/Helpers/Episode.js +++ b/src/UI/Handlebars/Helpers/Episode.js @@ -20,6 +20,7 @@ Handlebars.registerHelper('StatusLevel', function() { var currentTime = moment(); var start = moment(this.airDateUtc); var end = moment(this.end); + var monitored = this.series.monitored && this.monitored; if (hasFile) { return 'success'; @@ -29,6 +30,10 @@ Handlebars.registerHelper('StatusLevel', function() { return 'purple'; } + else if (!monitored) { + return 'unmonitored'; + } + if (this.episodeNumber === 1) { return 'premiere'; } diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index 4010dafc9..ebad4c789 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -24,6 +24,10 @@ module.exports = Marionette.Layout.extend({ throw 'context needs to be passed'; } + this.templateHelpers = { + floatOnMobile : options.floatOnMobile || false + }; + this.left = options.left; this.right = options.right; this.toolbarContext = options.context; @@ -51,7 +55,7 @@ module.exports = Marionette.Layout.extend({ _.each(buttonGroup.items, function(button) { if (buttonGroup.storeState && !button.key) { - throw 'must provide key for all buttons when storSstate is enabled'; + throw 'must provide key for all buttons when storeState is enabled'; } var model = new ButtonModel(button); diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs index f1e5f3c1a..0cd0e21e0 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs +++ b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs @@ -1,2 +1,2 @@ -
-
+
+