Updated full calendar to 1.6.4

Calendar/Upcoming now update on grab/download events
Better use of backbone collection on calendar
New: Calendar will auto refresh when episodes are grabbed and downloaded
This commit is contained in:
Mark McDowall 2013-11-30 02:53:53 -08:00
parent 26495aaa4b
commit 00717a638a
13 changed files with 2307 additions and 1502 deletions

View File

@ -3,25 +3,35 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Api.Episodes; using NzbDrone.Api.Episodes;
using NzbDrone.Api.Extensions; using NzbDrone.Api.Extensions;
using NzbDrone.Api.Mapping;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
using NzbDrone.Core.MediaFiles.Events;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
namespace NzbDrone.Api.Calendar namespace NzbDrone.Api.Calendar
{ {
public class CalendarModule : NzbDroneRestModule<EpisodeResource> public class CalendarModule : NzbDroneRestModuleWithSignalR<EpisodeResource, Episode>,
IHandle<EpisodeGrabbedEvent>,
IHandle<EpisodeDownloadedEvent>
{ {
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly SeriesRepository _seriesRepository; private readonly SeriesRepository _seriesRepository;
public CalendarModule(IEpisodeService episodeService, SeriesRepository seriesRepository) public CalendarModule(ICommandExecutor commandExecutor,
: base("/calendar") IEpisodeService episodeService,
SeriesRepository seriesRepository)
: base(commandExecutor, "calendar")
{ {
_episodeService = episodeService; _episodeService = episodeService;
_seriesRepository = seriesRepository; _seriesRepository = seriesRepository;
GetResourceAll = GetPaged; GetResourceAll = GetCalendar;
} }
private List<EpisodeResource> GetPaged() private List<EpisodeResource> GetCalendar()
{ {
var start = DateTime.Today; var start = DateTime.Today;
var end = DateTime.Today.AddDays(2); var end = DateTime.Today.AddDays(2);
@ -37,5 +47,24 @@ namespace NzbDrone.Api.Calendar
return resources.OrderBy(e => e.AirDate).ToList(); return resources.OrderBy(e => e.AirDate).ToList();
} }
public void Handle(EpisodeGrabbedEvent message)
{
foreach (var episode in message.Episode.Episodes)
{
var resource = episode.InjectTo<EpisodeResource>();
resource.Downloading = true;
BroadcastResourceChange(ModelAction.Updated, resource);
}
}
public void Handle(EpisodeDownloadedEvent message)
{
foreach (var episode in message.Episode.Episodes)
{
BroadcastResourceChange(ModelAction.Updated, episode.Id);
}
}
} }
} }

View File

@ -2,10 +2,9 @@
define( define(
[ [
'marionette', 'marionette',
'Calendar/UpcomingCollection',
'Calendar/UpcomingCollectionView', 'Calendar/UpcomingCollectionView',
'Calendar/CalendarView', 'Calendar/CalendarView'
], function (Marionette, UpcomingCollection, UpcomingCollectionView, CalendarView) { ], function (Marionette, UpcomingCollectionView, CalendarView) {
return Marionette.Layout.extend({ return Marionette.Layout.extend({
template: 'Calendar/CalendarLayoutTemplate', template: 'Calendar/CalendarLayoutTemplate',
@ -14,20 +13,13 @@ define(
calendar: '#x-calendar' calendar: '#x-calendar'
}, },
initialize: function () {
this.upcomingCollection = new UpcomingCollection();
this.upcomingCollection.fetch();
},
onShow: function () { onShow: function () {
this._showUpcoming(); this._showUpcoming();
this._showCalendar(); this._showCalendar();
}, },
_showUpcoming: function () { _showUpcoming: function () {
this.upcoming.show(new UpcomingCollectionView({ this.upcoming.show(new UpcomingCollectionView());
collection: this.upcomingCollection
}));
}, },
_showCalendar: function () { _showCalendar: function () {

View File

@ -9,6 +9,7 @@
<ul class='legend-labels'> <ul class='legend-labels'>
<li><span class="primary" title="Episode hasn't aired yet"></span>Unaired</li> <li><span class="primary" title="Episode hasn't aired yet"></span>Unaired</li>
<li><span class="warning" title="Episode is currently airing"></span>On Air</li> <li><span class="warning" title="Episode is currently airing"></span>On Air</li>
<li><span class="purple" title="Episode is currently downloading"></span>Downloading</li>
<li><span class="danger" title="Episode file has not been found"></span>Missing</li> <li><span class="danger" title="Episode file has not been found"></span>Missing</li>
<li><span class="success" title="Episode was downloaded and sorted"></span>Downloaded</li> <li><span class="success" title="Episode was downloaded and sorted"></span>Downloaded</li>
</ul> </ul>

View File

@ -7,14 +7,17 @@ define(
'moment', 'moment',
'Calendar/Collection', 'Calendar/Collection',
'System/StatusModel', 'System/StatusModel',
'History/Queue/QueueCollection',
'Mixins/backbone.signalr.mixin',
'fullcalendar' 'fullcalendar'
], function (vent, Marionette, moment, CalendarCollection, StatusModel) { ], function (vent, Marionette, moment, CalendarCollection, StatusModel, QueueCollection) {
var _instance; var _instance;
return Marionette.ItemView.extend({ return Marionette.ItemView.extend({
initialize: function () { initialize: function () {
this.collection = new CalendarCollection(); this.collection = new CalendarCollection().bindSignalR();
this.listenTo(this.collection, 'change', this._reloadCalendarEvents);
}, },
render : function () { render : function () {
@ -36,7 +39,7 @@ define(
prev: '<i class="icon-arrow-left"></i>', prev: '<i class="icon-arrow-left"></i>',
next: '<i class="icon-arrow-right"></i>' next: '<i class="icon-arrow-right"></i>'
}, },
events : this.getEvents, viewRender : this._getEvents,
eventRender : function (event, element) { eventRender : function (event, element) {
self.$(element).addClass(event.statusLevel); self.$(element).addClass(event.statusLevel);
self.$(element).children('.fc-event-inner').addClass(event.statusLevel); self.$(element).children('.fc-event-inner').addClass(event.statusLevel);
@ -53,43 +56,50 @@ define(
this.$('.fc-button-today').click(); this.$('.fc-button-today').click();
}, },
getEvents: function (start, end, callback) { _getEvents: function (view) {
var startDate = moment(start).toISOString(); var start = moment(view.visStart).toISOString();
var endDate = moment(end).toISOString(); var end = moment(view.visEnd).toISOString();
_instance.$el.fullCalendar('removeEvents');
_instance.collection.fetch({ _instance.collection.fetch({
data : { start: startDate, end: endDate }, data : { start: start, end: end },
success: function (calendarCollection) { success: function (collection) {
calendarCollection.each(function (element) { _instance._setEventData(collection);
var episodeTitle = element.get('title');
var seriesTitle = element.get('series').title;
var start = element.get('airDateUtc');
var runtime = element.get('series').runtime;
var end = moment(start).add('minutes', runtime).toISOString();
element.set({
title : seriesTitle,
episodeTitle: episodeTitle,
start : start,
end : end,
allDay : false
});
element.set('statusLevel', _instance.getStatusLevel(element));
element.set('model', element);
});
callback(calendarCollection.toJSON());
} }
}); });
}, },
getStatusLevel: function (element) { _setEventData: function (collection) {
var events = [];
collection.each(function (model) {
var seriesTitle = model.get('series').title;
var start = model.get('airDateUtc');
var runtime = model.get('series').runtime;
var end = moment(start).add('minutes', runtime).toISOString();
var event = {
title : seriesTitle,
start : start,
end : end,
allDay : false,
statusLevel : _instance._getStatusLevel(model, end),
model : model
};
events.push(event);
});
_instance.$el.fullCalendar('addEventSource', events);
},
_getStatusLevel: function (element, endTime) {
var hasFile = element.get('hasFile'); var hasFile = element.get('hasFile');
var downloading = QueueCollection.findEpisode(element.get('id')) || element.get('downloading');
var currentTime = moment(); var currentTime = moment();
var start = moment(element.get('airDateUtc')); var start = moment(element.get('airDateUtc'));
var end = moment(element.get('end')); var end = moment(endTime);
var statusLevel = 'primary'; var statusLevel = 'primary';
@ -97,6 +107,10 @@ define(
statusLevel = 'success'; statusLevel = 'success';
} }
if (downloading) {
statusLevel = 'purple';
}
else if (currentTime.isAfter(start) && currentTime.isBefore(end)) { else if (currentTime.isAfter(start) && currentTime.isBefore(end)) {
statusLevel = 'warning'; statusLevel = 'warning';
} }
@ -105,13 +119,17 @@ define(
statusLevel = 'danger'; statusLevel = 'danger';
} }
var test = currentTime.startOf('day').format('LLLL');
if (end.isBefore(currentTime.startOf('day'))) { if (end.isBefore(currentTime.startOf('day'))) {
statusLevel += ' past'; statusLevel += ' past';
} }
return statusLevel; return statusLevel;
},
_reloadCalendarEvents: function () {
window.alert('collection changed');
this.$el.fullCalendar('removeEvents');
this._setEventData(this.collection);
} }
}); });
}); });

View File

@ -3,9 +3,22 @@
define( define(
[ [
'marionette', 'marionette',
'Calendar/UpcomingItemView' 'Calendar/UpcomingCollection',
], function (Marionette, UpcomingItemView) { 'Calendar/UpcomingItemView',
'Mixins/backbone.signalr.mixin'
], function (Marionette, UpcomingCollection, UpcomingItemView) {
return Marionette.CollectionView.extend({ return Marionette.CollectionView.extend({
itemView: UpcomingItemView itemView: UpcomingItemView,
initialize: function () {
this.collection = new UpcomingCollection().bindSignalR();
this.collection.fetch();
this.listenTo(this.collection, 'change', this._refresh);
},
_refresh: function () {
this.render();
}
}); });
}); });

View File

@ -3,12 +3,11 @@
define( define(
[ [
'reqres', 'reqres',
'underscore',
'Cells/NzbDroneCell', 'Cells/NzbDroneCell',
'History/Queue/QueueCollection', 'History/Queue/QueueCollection',
'moment', 'moment',
'Shared/FormatHelpers' 'Shared/FormatHelpers'
], function (reqres, _, NzbDroneCell, QueueCollection, Moment, FormatHelpers) { ], function (reqres, NzbDroneCell, QueueCollection, Moment, FormatHelpers) {
return NzbDroneCell.extend({ return NzbDroneCell.extend({
className: 'episode-status-cell', className: 'episode-status-cell',
@ -56,10 +55,7 @@ define(
else { else {
var model = this.model; var model = this.model;
var downloading = QueueCollection.findEpisode(model.get('id'));
var downloading = _.find(QueueCollection.models, function (queueModel) {
return queueModel.get('episode').id === model.get('id');
});
if (downloading || this.model.get('downloading')) { if (downloading || this.model.get('downloading')) {
icon = 'icon-nd-downloading'; icon = 'icon-nd-downloading';

View File

@ -0,0 +1,11 @@
.fc-view {
overflow: visible;
}
.fc-event-title {
padding: 0 2px;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

View File

@ -1,5 +1,5 @@
/*! /*!
* FullCalendar v1.6.1 Stylesheet * FullCalendar v1.6.4 Stylesheet
* Docs & License: http://arshaw.com/fullcalendar/ * Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw * (c) 2013 Adam Shaw
*/ */
@ -102,11 +102,12 @@ html .fc,
.fc-content { .fc-content {
clear: both; clear: both;
zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */
} }
.fc-view { .fc-view {
width: 100%; /* needed for view switching (when view is absolute) */ width: 100%;
/*overflow: hidden;*/ overflow: hidden;
} }
@ -232,8 +233,9 @@ html .fc,
.fc-state-down, .fc-state-down,
.fc-state-active { .fc-state-active {
background : #cccccc none; background-color: #cccccc;
outline: 0; background-image: none;
outline: 0;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
} }
@ -249,6 +251,15 @@ html .fc,
/* Global Event Styles /* Global Event Styles
------------------------------------------------------------------------*/ ------------------------------------------------------------------------*/
.fc-event-container > * {
z-index: 8;
}
.fc-event-container > .ui-draggable-dragging,
.fc-event-container > .ui-resizable-resizing {
z-index: 9;
}
.fc-event { .fc-event {
border: 1px solid #3a87ad; /* default BORDER color */ border: 1px solid #3a87ad; /* default BORDER color */
@ -279,15 +290,8 @@ a.fc-event,
.fc-event-time, .fc-event-time,
.fc-event-title { .fc-event-title {
padding: 0 2px; padding: 0 1px;
display: block; }
}
.fc-event-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.fc .ui-resizable-handle { .fc .ui-resizable-handle {
display: block; display: block;

View File

@ -1,3 +1,4 @@
@import "Overrides/bootstrap"; @import "Overrides/bootstrap";
@import "Overrides/browser"; @import "Overrides/browser";
@import "Overrides/bootstrap.toggle-switch"; @import "Overrides/bootstrap.toggle-switch";
@import "Overrides/fullcalendar";

View File

@ -4,11 +4,7 @@
<h3> <h3>
<i class="icon-bookmark x-episode-monitored" title="Toggle monitored status" /> <i class="icon-bookmark x-episode-monitored" title="Toggle monitored status" />
{{#if episodeTitle}} {{series.title}} - {{EpisodeNumber}} - {{title}}
{{title}} - {{EpisodeNumber}} - {{episodeTitle}}
{{else}}
{{series.title}} - {{EpisodeNumber}} - {{title}}
{{/if}}
</h3> </h3>
</div> </div>

View File

@ -20,6 +20,7 @@ define(
Handlebars.registerHelper('StatusLevel', function () { Handlebars.registerHelper('StatusLevel', function () {
var hasFile = this.hasFile; var hasFile = this.hasFile;
var downloading = require('History/Queue/QueueCollection').findEpisode(this.id) || this.downloading;
var currentTime = Moment(); var currentTime = Moment();
var start = Moment(this.airDateUtc); var start = Moment(this.airDateUtc);
var end = Moment(this.end); var end = Moment(this.end);
@ -28,6 +29,10 @@ define(
return 'success'; return 'success';
} }
if (downloading) {
return 'purple';
}
if (currentTime.isAfter(start) && currentTime.isBefore(end)) { if (currentTime.isAfter(start) && currentTime.isBefore(end)) {
return 'warning'; return 'warning';
} }

View File

@ -1,13 +1,20 @@
'use strict'; 'use strict';
define( define(
[ [
'underscore',
'backbone', 'backbone',
'History/Queue/QueueModel', 'History/Queue/QueueModel',
'Mixins/backbone.signalr.mixin' 'Mixins/backbone.signalr.mixin'
], function (Backbone, QueueModel) { ], function (_, Backbone, QueueModel) {
var QueueCollection = Backbone.Collection.extend({ var QueueCollection = Backbone.Collection.extend({
url : window.NzbDrone.ApiRoot + '/queue', url : window.NzbDrone.ApiRoot + '/queue',
model: QueueModel model: QueueModel,
findEpisode: function (episodeId) {
return _.find(this.models, function (queueModel) {
return queueModel.get('episode').id === episodeId;
});
}
}); });
var collection = new QueueCollection().bindSignalR(); var collection = new QueueCollection().bindSignalR();

File diff suppressed because it is too large Load Diff