Merge branch 'signalr' into develop

This commit is contained in:
Keivan Beigi 2013-09-12 11:12:04 -07:00
commit 93481728cc
233 changed files with 2205 additions and 1813 deletions

View file

@ -1,9 +1,9 @@
{{#if folder.path}}
<div class="row well unmapped-folder-path">
<div class="span11">
{{folder.path}}
</div>
<div class="row well unmapped-folder-path">
<div class="span11">
{{folder.path}}
</div>
</div>
{{/if}}
<div class="row x-search-bar">
<div class="input-prepend nz-input-large add-series-search span11">

View file

@ -1,4 +1,4 @@
'use strict';
'use strict';
define(
[
'backbone',
@ -8,10 +8,19 @@ define(
var CommandCollection = Backbone.Collection.extend({
url : window.ApiRoot + '/command',
model: CommandModel
model: CommandModel,
findCommand: function (command) {
return this.find(function (model) {
return model.isSameCommand(command);
});
}
});
var collection = new CommandCollection().bindSignalR();
collection.fetch();
return collection;
});

View file

@ -1,16 +1,56 @@
'use strict';
define({
Execute: function (name, properties) {
var data = { command: name };
define(
[
'Commands/CommandModel',
'Commands/CommandCollection',
'underscore',
'jQuery/jquery.spin'
], function (CommandModel, CommandCollection, _) {
if (properties) {
$.extend(data, properties);
return{
Execute: function (name, properties) {
var attr = _.extend({name: name.toLocaleLowerCase()}, properties);
var commandModel = new CommandModel(attr);
return commandModel.save().success(function () {
CommandCollection.add(commandModel);
});
},
bindToCommand: function (options) {
var self = this;
var existingCommand = CommandCollection.findCommand(options.command);
if (existingCommand) {
this._bindToCommandModel.call(this, existingCommand, options);
}
CommandCollection.bind('add sync', function (model) {
if (model.isSameCommand(options.command)) {
self._bindToCommandModel.call(self, model, options);
}
});
},
_bindToCommandModel: function bindToCommand(model, options) {
if (!model.isActive()) {
options.element.stopSpin();
return;
}
model.bind('change:state', function (model) {
if (!model.isActive()) {
options.element.stopSpin();
}
});
options.element.startSpin();
}
return $.ajax({
type: 'POST',
url : window.ApiRoot + '/command',
data: JSON.stringify(data)
});
}
});

View file

@ -0,0 +1,15 @@
'use strict';
define(
[
'app',
'marionette',
'Commands/CommandCollection',
'Commands/CommandMessengerItemView'
], function (App, Marionette, commandCollection, CommandMessengerItemView) {
var CollectionView = Marionette.CollectionView.extend({
itemView : CommandMessengerItemView
});
new CollectionView({collection: commandCollection});
});

View file

@ -0,0 +1,48 @@
'use strict';
define(
[
'app',
'marionette',
'Shared/Messenger'
], function (App, Marionette, Messenger) {
return Marionette.ItemView.extend({
initialize: function () {
this.listenTo(this.model, 'change', this.render);
},
render: function () {
if (!this.model.get('message') || !this.model.get('sendUpdatesToClient')) {
return;
}
var message = {
type : 'info',
message : '[{0}] {1}'.format(this.model.get('name'), this.model.get('message')),
id : this.model.id,
hideAfter: 0
};
switch (this.model.get('state')) {
case 'completed':
message.hideAfter = 4;
break;
case 'failed':
message.hideAfter = 4;
message.type = 'error';
break;
default :
message.hideAfter = 0;
}
Messenger.show(message);
console.log(message.message);
}
});
});

View file

@ -4,5 +4,30 @@ define(
'backbone'
], function (Backbone) {
return Backbone.Model.extend({
url: window.ApiRoot + '/command',
parse: function (response) {
response.name = response.name.toLocaleLowerCase();
return response;
},
isActive: function () {
return this.get('state') !== 'completed' && this.get('state') !== 'failed';
},
isSameCommand: function (command) {
if (command.name.toLocaleLowerCase() != this.get('name').toLocaleLowerCase()) {
return false;
}
for (var key in command) {
if (key !== 'name' && command[key] !== this.get(key)) {
return false;
}
}
return true;
}
});
});

View file

@ -10,7 +10,6 @@ define(
'Series/Details/SeriesDetailsLayout',
'Series/SeriesCollection',
'Missing/MissingLayout',
'Series/SeriesModel',
'Calendar/CalendarLayout',
'Logs/Layout',
'Logs/Files/Layout',
@ -19,7 +18,7 @@ define(
'SeasonPass/Layout',
'Shared/NotFoundView',
'Shared/Modal/Region'
], function (App, Marionette, HistoryLayout, SettingsLayout, AddSeriesLayout, SeriesIndexLayout, SeriesDetailsLayout, SeriesCollection, MissingLayout, SeriesModel, CalendarLayout,
], function (App, Marionette, HistoryLayout, SettingsLayout, AddSeriesLayout, SeriesIndexLayout, SeriesDetailsLayout, SeriesCollection, MissingLayout, CalendarLayout,
LogsLayout, LogFileLayout, ReleaseLayout, SystemLayout, SeasonPassLayout, NotFoundView) {
return Marionette.Controller.extend({

View file

@ -7,11 +7,9 @@ define(
'Episode/Search/ManualLayout',
'Release/Collection',
'Series/SeriesCollection',
'Shared/LoadingView',
'Shared/Messenger',
'Shared/Actioneer',
'Shared/FormatHelpers'
], function (App, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection, LoadingView, Messenger, Actioneer, FormatHelpers) {
'Commands/CommandController',
'Shared/LoadingView'
], function (App, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection,CommandController, LoadingView) {
return Marionette.Layout.extend({
template: 'Episode/Search/LayoutTemplate',
@ -39,18 +37,8 @@ define(
e.preventDefault();
}
var series = SeriesCollection.get(this.model.get('seriesId'));
var seriesTitle = series.get('title');
var season = this.model.get('seasonNumber');
var episode = this.model.get('episodeNumber');
var message = seriesTitle + ' - ' + season + 'x' + FormatHelpers.pad(episode, 2);
Actioneer.ExecuteCommand({
command : 'episodeSearch',
properties: {
episodeId: this.model.get('id')
},
errorMessage: 'Search failed for: ' + message
CommandController.Execute('episodeSearch', {
episodeId: this.model.get('id')
});
App.vent.trigger(App.Commands.CloseModalCommand);

View file

@ -5,59 +5,29 @@ define(
], function () {
_.extend(Backbone.Collection.prototype, {
bindSignalR: function (options) {
bindSignalR: function () {
if (!options) {
options = {};
}
var collection = this;
if (!options.url) {
console.assert(this.url, 'url must be provided or collection must have url');
options.url = this.url.replace('api', 'signalr');
}
var processMessage = function (options) {
var self = this;
var _getStatus = function (status) {
switch (status) {
case 0:
return 'connecting';
case 1:
return 'connected';
case 2:
return 'reconnecting';
case 4:
return 'disconnected';
default:
throw 'invalid status ' + status;
}
var model = new collection.model(options.resource, {parse: true});
collection.add(model, {merge: true});
console.log(options.action + ": %O", options.resource);
};
this.signalRconnection = $.connection(options.url);
this.signalRconnection.stateChanged(function (change) {
console.debug('{0} [{1}]'.format(options.url, _getStatus(change.newState)));
});
this.signalRconnection.received(function (message) {
console.debug(message);
self.fetch();
});
this.signalRconnection.start({ transport:
require(
[
'longPolling'
] });
'app'
], function (app) {
collection.listenTo(app.vent, 'server:' + collection.url.replace('/api/', ''), processMessage)
});
return this;
},
unbindSignalR: function () {
if(this.signalRconnection){
this.signalRconnection.stop();
delete this.signalRconnection;
}
}});
});

View file

@ -1,31 +1,36 @@
'use strict';
'use strict';
define(
[
'app',
'backbone',
'ProgressMessaging/ProgressMessageModel',
'Shared/Messenger',
'Mixins/backbone.signalr.mixin'
], function (Backbone, ProgressMessageModel, Messenger) {
], function (App, Backbone, Messenger) {
var ProgressMessageCollection = Backbone.Collection.extend({
url : window.ApiRoot + '/progressmessage',
model: ProgressMessageModel
model: Backbone.Model,
initialize: function(){
}
});
var collection = new ProgressMessageCollection().bindSignalR();
var collection = new ProgressMessageCollection();//.bindSignalR();
collection.signalRconnection.received(function (message) {
/* collection.signalRconnection.received(function (message) {
var type = getMessengerType(message.status);
var hideAfter = type === 'info' ? 60 : 5;
var type = getMessengerType(message.status);
var hideAfter = type === 'info' ? 60 :5;
Messenger.show({
id : message.commandId,
message : message.message,
type : type,
hideAfter: hideAfter
});
});
Messenger.show({
id : message.commandId,
message : message.message,
type : type,
hideAfter: hideAfter
});
});*/
var getMessengerType = function (status) {
switch (status) {

View file

@ -1,8 +0,0 @@
'use strict';
define(
[
'backbone'
], function (Backbone) {
return Backbone.Model.extend({
});
});

View file

@ -1,4 +1,4 @@
'use strict';
'use strict';
define(
[
'backbone',

View file

@ -6,11 +6,11 @@ require(
'Controller',
'Series/SeriesCollection',
'ProgressMessaging/ProgressMessageCollection',
'Shared/Actioneer',
'Commands/CommandMessengerCollectionView',
'Navbar/NavbarView',
'jQuery/RouteBinder',
'jquery'
], function (App, Marionette, Controller, SeriesCollection, ProgressMessageCollection, Actioneer, NavbarView, RouterBinder, $) {
], function (App, Marionette, Controller, SeriesCollection, ProgressMessageCollection, CommandMessengerCollectionView, NavbarView, RouterBinder, $) {
var Router = Marionette.AppRouter.extend({
@ -40,11 +40,11 @@ require(
App.Router = new Router();
SeriesCollection.fetch().done(function () {
Backbone.history.start({ pushState: true });
RouterBinder.bind(App.Router);
App.navbarRegion.show(new NavbarView());
$('body').addClass('started');
});
Backbone.history.start({ pushState: true });
RouterBinder.bind(App.Router);
App.navbarRegion.show(new NavbarView());
$('body').addClass('started');
});
});
return App.Router;

View file

@ -2,9 +2,9 @@
define(
[
'marionette',
'Series/SeasonCollection',
'Shared/Actioneer'
], function (Marionette, SeasonCollection, Actioneer) {
'backgrid',
'Series/SeasonCollection'
], function (Marionette, Backgrid, SeasonCollection) {
return Marionette.Layout.extend({
template: 'SeasonPass/SeriesLayoutTemplate',
@ -111,11 +111,9 @@ define(
this.model.setSeasonMonitored(seasonNumber);
Actioneer.SaveModel({
element: element,
context: this,
always : this._afterToggleSeasonMonitored
});
var savePromise =this.model.save()
.always(this.render.bind(this));
element.spinForPromise(savePromise);
},
_afterToggleSeasonMonitored: function () {

View file

@ -5,6 +5,10 @@ define(
], function (Marionette) {
return Marionette.ItemView.extend({
template: 'Series/Details/InfoViewTemplate'
template: 'Series/Details/InfoViewTemplate',
initialize: function () {
this.listenTo(this.model, 'change', this.render);
}
});
});

View file

@ -8,9 +8,9 @@ define(
'Cells/EpisodeTitleCell',
'Cells/RelativeDateCell',
'Cells/EpisodeStatusCell',
'Shared/Actioneer',
'Commands/CommandController',
'moment'
], function (App, Marionette, Backgrid, ToggleCell, EpisodeTitleCell, RelativeDateCell, EpisodeStatusCell, Actioneer, Moment) {
], function (App, Marionette, Backgrid, ToggleCell, EpisodeTitleCell, RelativeDateCell, EpisodeStatusCell, CommandController, Moment) {
return Marionette.Layout.extend({
template: 'Series/Details/SeasonLayoutTemplate',
@ -21,11 +21,11 @@ define(
},
events: {
'click .x-season-search' : '_seasonSearch',
'click .x-season-monitored' : '_seasonMonitored',
'click .x-season-rename' : '_seasonRename',
'click .x-show-hide-episodes' : '_showHideEpisodes',
'dblclick .series-season h2' : '_showHideEpisodes'
'click .x-season-monitored' : '_seasonMonitored',
'click .x-season-search' : '_seasonSearch',
'click .x-season-rename' : '_seasonRename',
'click .x-show-hide-episodes': '_showHideEpisodes',
'dblclick .series-season h2' : '_showHideEpisodes'
},
regions: {
@ -51,11 +51,11 @@ define(
})
},
{
name : 'this',
label : 'Title',
hideSeriesLink : true,
cell : EpisodeTitleCell,
sortable: false
name : 'this',
label : 'Title',
hideSeriesLink: true,
cell : EpisodeTitleCell,
sortable : false
},
{
name : 'airDateUtc',
@ -76,41 +76,58 @@ define(
throw 'episodeCollection is needed';
}
this.templateHelpers = {};
this.episodeCollection = options.episodeCollection.bySeason(this.model.get('seasonNumber'));
this.series = options.series;
this.showingEpisodes = this._shouldShowEpisodes();
this._generateTemplateHelpers();
this.listenTo(this.model, 'sync', function () {
this._afterSeasonMonitored();
}, this);
this.listenTo(this.episodeCollection, 'sync', function () {
this.render();
}, this);
this.listenTo(this.model, 'sync', this._afterSeasonMonitored);
this.listenTo(this.episodeCollection, 'sync', this.render);
},
onRender: function () {
if (this.showingEpisodes) {
this._showEpisodes();
}
this._setSeasonMonitoredState();
CommandController.bindToCommand({
element: this.ui.seasonSearch,
command: {
name : 'seasonSearch',
seriesId : this.series.id,
seasonNumber: this.model.get('seasonNumber')
}
});
CommandController.bindToCommand({
element: this.ui.seasonRename,
command: {
name : 'renameSeason',
seriesId : this.series.id,
seasonNumber: this.model.get('seasonNumber')
}
});
},
_seasonSearch: function () {
Actioneer.ExecuteCommand({
command : 'seasonSearch',
properties : {
seriesId : this.model.get('seriesId'),
seasonNumber: this.model.get('seasonNumber')
},
element : this.ui.seasonSearch,
errorMessage : 'Search for season {0} failed'.format(this.model.get('seasonNumber')),
startMessage : 'Search for season {0} started'.format(this.model.get('seasonNumber')),
successMessage: 'Search for season {0} completed'.format(this.model.get('seasonNumber'))
CommandController.Execute('seasonSearch', {
name : 'seasonSearch',
seriesId : this.series.id,
seasonNumber: this.model.get('seasonNumber')
});
},
_seasonRename: function () {
CommandController.Execute('renameSeason', {
name : 'renameSeason',
seriesId : this.series.id,
seasonNumber: this.model.get('seasonNumber')
});
},
@ -119,12 +136,9 @@ define(
this.model.set(name, !this.model.get(name));
this.series.setSeasonMonitored(this.model.get('seasonNumber'));
Actioneer.SaveModel({
model : this.series,
context: this,
element: this.ui.seasonMonitored,
always : this._afterSeasonMonitored
});
var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this));
this.ui.seasonMonitored.spinForPromise(savePromise);
},
_afterSeasonMonitored: function () {
@ -150,19 +164,6 @@ define(
}
},
_seasonRename: function () {
Actioneer.ExecuteCommand({
command : 'renameSeason',
properties : {
seriesId : this.model.get('seriesId'),
seasonNumber: this.model.get('seasonNumber')
},
element : this.ui.seasonRename,
errorMessage: 'Season rename failed',
context : this,
onSuccess : this._afterRename
});
},
_afterRename: function () {
App.vent.trigger(App.Events.SeasonRenamed, { series: this.series, seasonNumber: this.model.get('seasonNumber') });
@ -180,12 +181,11 @@ define(
var startDate = Moment().add('month', -1);
var endDate = Moment().add('year', 1);
var result = this.episodeCollection.some(function(episode) {
return this.episodeCollection.some(function (episode) {
var airDate = episode.get('airDateUtc');
if (airDate)
{
if (airDate) {
var airDateMoment = Moment(airDate);
if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) {
@ -195,15 +195,12 @@ define(
return false;
});
return result;
},
_generateTemplateHelpers: function () {
this.templateHelpers.showingEpisodes = this.showingEpisodes;
templateHelpers: function () {
var episodeCount = this.episodeCollection.filter(function (episode) {
return (episode.get('monitored') && Moment(episode.get('airDateUtc')).isBefore(Moment())) || episode.get('hasFile');
return episode.get('hasFile') || (episode.get('monitored') && Moment(episode.get('airDateUtc')).isBefore(Moment()));
}).length;
var episodeFileCount = this.episodeCollection.where({ hasFile: true }).length;
@ -213,22 +210,22 @@ define(
percentOfEpisodes = episodeFileCount / episodeCount * 100;
}
this.templateHelpers.episodeCount = episodeCount;
this.templateHelpers.episodeFileCount = episodeFileCount;
this.templateHelpers.percentOfEpisodes = percentOfEpisodes;
return {
showingEpisodes : this.showingEpisodes,
episodeCount : episodeCount,
episodeFileCount : episodeFileCount,
percentOfEpisodes: percentOfEpisodes
};
},
_showHideEpisodes: function () {
if (this.showingEpisodes) {
this.showingEpisodes = false;
this.episodeGrid.$el.slideUp();
this.episodeGrid.close();
}
else {
this.showingEpisodes = true;
this._showEpisodes();
this.episodeGrid.$el.slideDown();
}
this.templateHelpers.showingEpisodes = this.showingEpisodes;

View file

@ -1,35 +1,41 @@
<div class="series-season" id="season-{{seasonNumber}}">
<h2>
<i class="x-season-monitored clickable" title="Toggle season monitored status"/>
{{#if seasonNumber}}
Season {{seasonNumber}}
Season {{seasonNumber}}
{{else}}
Specials
Specials
{{/if}}
{{#if_eq episodeCount compare=0}}
<i class="icon-nd-status season-status status-primary" title="No aired episodes"/>
<i class="icon-nd-status season-status status-primary" title="No aired episodes"/>
{{else}}
{{#if_eq percentOfEpisodes compare=100}}
<i class="icon-nd-status season-status status-success" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
{{else}}
<i class="icon-nd-status season-status status-danger" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
{{/if_eq}}
{{#if_eq percentOfEpisodes compare=100}}
<i class="icon-nd-status season-status status-success" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
{{else}}
<i class="icon-nd-status season-status status-danger" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded"/>
{{/if_eq}}
{{/if_eq}}
<span class="season-actions pull-right">
<i class="icon-nd-rename x-season-rename" title="Rename all episodes in season {{seasonNumber}}"/>
<i class="icon-search x-season-search" title="Search for all episodes in season {{seasonNumber}}"/>
<div class="x-season-rename">
<i class="icon-nd-rename" title="Rename all episodes in season {{seasonNumber}}"/>
</div>
<div class="x-season-search">
<i class="icon-search" title="Search for all episodes in season {{seasonNumber}}"/>
</div>
</span>
</h2>
<div class="x-episode-grid"></div>
<div class="show-hide-episodes x-show-hide-episodes">
<h4>
{{#if showingEpisodes}}
<i class="icon-chevron-sign-up"/> Hide Episodes
<i class="icon-chevron-sign-up"/>
Hide Episodes
{{else}}
<i class="icon-chevron-sign-down"/> Show Episodes
<i class="icon-chevron-sign-down"/>
Show Episodes
{{/if}}
</h4>
</div>

View file

@ -8,27 +8,19 @@ define(
'Series/SeasonCollection',
'Series/Details/SeasonCollectionView',
'Series/Details/InfoView',
'Commands/CommandController',
'Shared/LoadingView',
'Shared/Actioneer',
'backstrech',
'Mixins/backbone.signalr.mixin'
], function (App,
Marionette,
EpisodeCollection,
EpisodeFileCollection,
SeasonCollection,
SeasonCollectionView,
InfoView,
LoadingView,
Actioneer) {
], function (App, Marionette, EpisodeCollection, EpisodeFileCollection, SeasonCollection, SeasonCollectionView, InfoView, CommandController, LoadingView) {
return Marionette.Layout.extend({
itemViewContainer: '.x-series-seasons',
template : 'Series/Details/SeriesDetailsTemplate',
regions: {
seasons : '#seasons',
info : '#info'
seasons: '#seasons',
info : '#info'
},
ui: {
@ -51,11 +43,7 @@ define(
initialize: function () {
$('body').addClass('backdrop');
this.listenTo(this.model, 'sync', function () {
this._setMonitoredState();
this._showInfo();
}, this);
this.listenTo(this.model, 'change:monitored', this._setMonitoredState);
this.listenTo(App.vent, App.Events.SeriesDeleted, this._onSeriesDeleted);
this.listenTo(App.vent, App.Events.SeasonRenamed, this._onSeasonRenamed);
},
@ -73,6 +61,31 @@ define(
this._showSeasons();
this._setMonitoredState();
this._showInfo();
},
onRender: function () {
CommandController.bindToCommand({
element: this.ui.refresh,
command: {
name: 'refreshSeries'
}
});
CommandController.bindToCommand({
element: this.ui.search,
command: {
name: 'seriesSearch'
}
});
CommandController.bindToCommand({
element: this.ui.rename,
command: {
name: 'renameSeries'
}
});
},
_getFanArt: function () {
@ -97,22 +110,19 @@ define(
},
_toggleMonitored: function () {
var name = 'monitored';
this.model.set(name, !this.model.get(name), { silent: true });
Actioneer.SaveModel({
context: this,
element: this.ui.monitored,
always : this._setMonitoredState()
var savePromise = this.model.save('monitored', !this.model.get('monitored'), {
wait: true
});
this.ui.monitored.spinForPromise(savePromise);
},
_setMonitoredState: function () {
var monitored = this.model.get('monitored');
this.ui.monitored.removeClass('icon-spin icon-spinner');
this.ui.monitored.removeAttr('data-idle-icon');
if (this.model.get('monitored')) {
if (monitored) {
this.ui.monitored.addClass('icon-nd-monitored');
this.ui.monitored.removeClass('icon-nd-unmonitored');
}
@ -127,15 +137,9 @@ define(
},
_refreshSeries: function () {
Actioneer.ExecuteCommand({
command : 'refreshSeries',
properties: {
seriesId: this.model.get('id')
},
element : this.ui.refresh,
leaveIcon : true,
context : this,
onSuccess : this._showSeasons
CommandController.Execute('refreshSeries', {
name : 'refreshSeries',
seriesId: this.model.id
});
},
@ -147,27 +151,18 @@ define(
},
_renameSeries: function () {
Actioneer.ExecuteCommand({
command : 'renameSeries',
properties : {
seriesId: this.model.get('id')
},
element : this.ui.rename,
context : this,
onSuccess : this._refetchEpisodeFiles,
errorMessage: 'Series search failed'
CommandController.Execute('renameSeries', {
name : 'renameSeries',
seriesId: this.model.id
});
},
_seriesSearch: function () {
Actioneer.ExecuteCommand({
command : 'seriesSearch',
properties : {
seriesId: this.model.get('id')
},
element : this.ui.search,
errorMessage: 'Series search failed',
startMessage: 'Search for {0} started'.format(this.model.get('title'))
CommandController.Execute('seriesSearch', {
name : 'seriesSearch',
seriesId: this.model.id
});
},
@ -187,15 +182,10 @@ define(
series : self.model
});
App.reqres.setHandler(App.Reqres.GetEpisodeFileById, function(episodeFileId){
App.reqres.setHandler(App.Reqres.GetEpisodeFileById, function (episodeFileId) {
return self.episodeFileCollection.get(episodeFileId);
});
/* self.episodeCollection.bindSignalR({
onReceived: seasonCollectionView.onEpisodeGrabbed,
context : seasonCollectionView
});*/
self.seasons.show(seasonCollectionView);
});
},
@ -204,13 +194,9 @@ define(
this.info.show(new InfoView({ model: this.model }));
},
_refetchEpisodeFiles: function () {
this.episodeFileCollection.fetch();
},
_onSeasonRenamed: function(event) {
_onSeasonRenamed: function (event) {
if (this.model.get('id') === event.series.get('id')) {
this._refetchEpisodeFiles();
this.episodeFileCollection.fetch();
}
}
});

View file

@ -2,14 +2,23 @@
<div class="span11">
<div class="row">
<h1>
<i class="icon-bookmark x-monitored clickable" title="Toggle monitored state for entire series"/>
<i class="x-monitored clickable series-monitor-toggle" title="Toggle monitored state for entire series"/>
{{title}}
<span class="series-actions pull-right">
<i class="icon-refresh x-refresh" title="Update series info and scan disk"/>
<i class="icon-nd-rename x-rename" title="Rename all episodes in this series"/>
<i class="icon-search x-search" title="Search for all episodes in this series"/>
<i class="icon-nd-edit x-edit" title="Edit series"/>
</span>
<div class="series-actions pull-right">
<div class="x-refresh">
<i class="icon-refresh icon-can-spin" title="Update series info and scan disk"/>
</div>
<div class="x-rename">
<i class="icon-nd-rename" title="Rename all episodes in this series"/>
</div>
<div class="x-search">
<i class="icon-search" title="Search for all episodes in this series"/>
</div>
<div class="x-edit">
<i class="icon-nd-edit" title="Edit series"/>
</div>
</div>
</h1>
</div>
<div class="row series-detail-overview">

View file

@ -6,18 +6,18 @@
overflow : visible;
.series-poster {
padding-left: 20px;
width : 168px;
padding-left : 20px;
width : 168px;
}
.form-horizontal {
margin-top: 10px;
margin-top : 10px;
}
}
.delete-series-modal {
.path {
margin-left: 30px;
margin-left : 30px;
}
}
@ -54,7 +54,7 @@
.show-hide-episodes {
.clickable();
text-align: center;
text-align : center;
i {
.clickable();
@ -84,24 +84,24 @@
text-align : center;
.progress {
text-align: left;
text-align : left;
margin-top : 5px;
left: 0px;
width: 170px;
left : 0px;
width : 170px;
.progressbar-front-text {
width: 170px;
width : 170px;
}
}
}
.title-container {
position: relative;
position : relative;
.title {
position : absolute;
top : -100px;
opacity: 0.0;
top : -100px;
opacity : 0.0;
}
}
@ -188,7 +188,7 @@
.episode-detail-modal {
.episode-info {
margin-bottom: 10px;
margin-bottom : 10px;
}
.episode-overview {
@ -221,35 +221,41 @@
}
.season-actions, .series-actions {
font-size : 24px;
div {
display : inline-block
}
text-transform : none;
i {
.clickable;
font-size : 24px;
padding-left : 5px;
}
}
.series-stats {
font-size: 11px;
font-size : 11px;
}
.series-legend {
padding-top: 5px;
padding-top : 5px;
}
.seasonpass-series {
.card;
margin : 20px 0px;
margin : 20px 0px;
.title {
font-weight: 300;
font-size: 24px;
line-height: 30px;
margin-left: 5px;
font-weight : 300;
font-size : 24px;
line-height : 30px;
margin-left : 5px;
}
.season-select {
margin-bottom: 0px;
margin-bottom : 0px;
}
.expander {
@ -260,11 +266,11 @@
}
.season-grid {
margin-top: 10px;
margin-top : 10px;
}
}
.season-status {
font-size: 16px;
vertical-align: middle !important;
}
font-size : 16px;
vertical-align : middle !important;
}

View file

@ -6,11 +6,11 @@ define([
'Settings/Notifications/Model',
'Settings/Notifications/DeleteView',
'Shared/Messenger',
'Shared/Actioneer',
'Commands/CommandController',
'Mixins/AsModelBoundView',
'Form/FormBuilder'
], function (App, Marionette, NotificationModel, DeleteView, Messenger, Actioneer, AsModelBoundView) {
], function (App, Marionette, NotificationModel, DeleteView, Messenger, CommandController, AsModelBoundView) {
var model = Marionette.ItemView.extend({
template: 'Settings/Notifications/EditTemplate',
@ -76,16 +76,8 @@ define([
properties[field.name] = field.value;
});
Actioneer.ExecuteCommand({
command : testCommand,
properties : properties,
button : this.ui.testButton,
element : this.ui.testIcon,
errorMessage : 'Failed to test notification settings',
successMessage: 'Notification settings tested successfully',
always : this._testOnAlways,
context : this
});
CommandController.Execute(testCommand, properties);
}
},

View file

@ -1,198 +0,0 @@
'use strict';
define(
[
'Commands/CommandController',
'Commands/CommandCollection',
'Shared/Messenger'],
function(CommandController, CommandCollection, Messenger) {
var actioneer = Marionette.AppRouter.extend({
initialize: function () {
this.trackedCommands = [];
CommandCollection.fetch();
this.listenTo(CommandCollection, 'sync', this._handleCommands);
},
ExecuteCommand: function (options) {
options.iconClass = this._getIconClass(options.element);
if (options.button) {
options.button.addClass('disable');
}
this._setSpinnerOnElement(options);
var promise = CommandController.Execute(options.command, options.properties);
this._showStartMessage(options, promise);
},
SaveModel: function (options) {
options.iconClass = this._getIconClass(options.element);
this._showStartMessage(options);
this._setSpinnerOnElement(options);
var model = options.model ? options.model : options.context.model;
var promise = model.save();
this._handlePromise(promise, options);
},
_handlePromise: function (promise, options) {
var self = this;
promise.done(function () {
self._onSuccess(options);
});
promise.fail(function (ajaxOptions) {
if (ajaxOptions.readyState === 0 || ajaxOptions.status === 0) {
return;
}
self._onError(options);
});
promise.always(function () {
self._onComplete(options);
});
},
_handleCommands: function () {
var self = this;
_.each(this.trackedCommands, function (trackedCommand){
if (trackedCommand.completed === true) {
return;
}
var options = trackedCommand.options;
var command = CommandCollection.find({ 'id': trackedCommand.id });
if (!command) {
trackedCommand.completed = true;
self._onError(options, trackedCommand.id);
self._onComplete(options);
return;
}
if (command.get('state') === 'completed') {
trackedCommand.completed = true;
self._onSuccess(options, command.get('id'));
self._onComplete(options);
return;
}
if (command.get('state') === 'failed') {
trackedCommand.completed = true;
self._onError(options, command.get('id'));
self._onComplete(options);
}
});
},
_getIconClass: function(element) {
if (!element) {
return '';
}
return element.attr('class').match(/(?:^|\s)icon\-.+?(?:$|\s)/)[0];
},
_setSpinnerOnElement: function (options) {
if (!options.element) {
return;
}
if (options.leaveIcon) {
options.element.addClass('icon-spin');
}
else {
options.element.removeClass(options.iconClass);
options.element.addClass('icon-nd-spinner');
}
},
_onSuccess: function (options, id) {
if (options.successMessage) {
Messenger.show({
id : id,
message: options.successMessage,
type : 'success'
});
}
if (options.onSuccess) {
options.onSuccess.call(options.context);
}
},
_onError: function (options, id) {
if (options.errorMessage) {
Messenger.show({
id : id,
message: options.errorMessage,
type : 'error'
});
}
if (options.onError) {
options.onError.call(options.context);
}
},
_onComplete: function (options) {
if (options.button) {
options.button.removeClass('disable');
}
if (options.element) {
if (options.leaveIcon) {
options.element.removeClass('icon-spin');
}
else {
options.element.addClass(options.iconClass);
options.element.removeClass('icon-nd-spinner');
options.element.removeClass('icon-spin');
}
}
if (options.always) {
options.always.call(options.context);
}
},
_showStartMessage: function (options, promise) {
var self = this;
if (!promise) {
if (options.startMessage) {
Messenger.show({
message: options.startMessage
});
}
return;
}
promise.done(function (data) {
self.trackedCommands.push({ id: data.id, options: options });
if (options.startMessage) {
Messenger.show({
id : data.id,
message: options.startMessage
});
}
});
}
});
return new actioneer();
});

View file

@ -1,4 +1,4 @@
'use strict';
'use strict';
define(
[
'marionette'

View file

@ -1,13 +1,14 @@
'use strict';
define(function () {
return {
show: function (options) {
if (!options.type) {
options.type = 'info';
}
if (!options.hideAfter) {
if (options.hideAfter === undefined) {
switch (options.type) {
case 'info':
options.hideAfter = 5;
@ -18,7 +19,7 @@ define(function () {
break;
default :
options.hideAfter = 0;
options.hideAfter = 5;
}
}
@ -31,6 +32,7 @@ define(function () {
});
},
monitor: function (options) {
if (!options.promise) {

View file

@ -0,0 +1,51 @@
'use strict';
define(
[
'app',
'signalR'
], function () {
return {
appInitializer: function () {
console.log('starting signalR');
var getStatus = function (status) {
switch (status) {
case 0:
return 'connecting';
case 1:
return 'connected';
case 2:
return 'reconnecting';
case 4:
return 'disconnected';
default:
throw 'invalid status ' + status;
}
};
this.signalRconnection = $.connection("/signalr");
this.signalRconnection.stateChanged(function (change) {
console.debug('SignalR: [{0}]'.format(getStatus(change.newState)));
});
this.signalRconnection.received(function (message) {
require(
[
'app'
], function (app) {
app.vent.trigger('server:' + message.name, message.body);
})
});
this.signalRconnection.start({ transport:
[
'longPolling'
] });
return this;
}
};
});

View file

@ -3,9 +3,8 @@ define(
[
'app',
'marionette',
'Shared/Actioneer',
'Shared/Messenger'
], function (App, Marionette, Actioneer, Messenger) {
'Commands/CommandController'
], function (App, Marionette, CommandController) {
return Marionette.ItemView.extend({
template : 'Shared/Toolbar/ButtonTemplate',
@ -15,13 +14,8 @@ define(
'click': 'onClick'
},
ui: {
icon: '.x-icon'
},
initialize: function () {
this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key');
this.idle = true;
},
onRender: function () {
@ -30,33 +24,34 @@ define(
this.invokeCallback();
}
if(!this.model.get('title')){
if (!this.model.get('title')) {
this.$el.addClass('btn-icon-only');
}
var command = this.model.get('command');
if (command) {
CommandController.bindToCommand({
command: {name: command},
element: this.$el
});
}
},
onClick: function () {
if (this.idle) {
this.invokeCallback();
this.invokeRoute();
this.invokeCommand();
if (this.$el.hasClass('disabled')) {
return;
}
this.invokeCallback();
this.invokeRoute();
this.invokeCommand();
},
invokeCommand: function () {
var command = this.model.get('command');
if (command) {
this.idle = false;
Actioneer.ExecuteCommand({
command : command,
button : this.$el,
element : this.ui.icon,
errorMessage : this.model.get('errorMessage'),
successMessage: this.model.get('successMessage'),
always : this._commandAlways,
context : this
});
CommandController.Execute(command);
}
},
@ -83,12 +78,6 @@ define(
if (callback) {
callback.call(this.model.ownerContext);
}
},
_commandAlways: function () {
if (!this.isClosed) {
this.idle = true;
}
}
});
});

View file

@ -1,4 +1,4 @@
'use strict';
'use strict';
define(
[
'app',
@ -6,15 +6,13 @@ define(
'System/StatusModel',
'System/About/View',
'Logs/Layout',
'Shared/Toolbar/ToolbarLayout',
'Shared/LoadingView'
'Shared/Toolbar/ToolbarLayout'
], function (App,
Marionette,
StatusModel,
AboutView,
LogsLayout,
ToolbarLayout,
LoadingView) {
ToolbarLayout) {
return Marionette.Layout.extend({
template: 'System/LayoutTemplate',
@ -38,21 +36,7 @@ define(
title : 'Check for Update',
icon : 'icon-nd-update',
command: 'applicationUpdate'
},
// {
// title : 'Restart',
// icon : 'icon-repeat',
// command : 'restart',
// successMessage: 'NzbDrone restart has been triggered',
// errorMessage : 'Failed to restart NzbDrone'
// },
// {
// title : 'Shutdown',
// icon : 'icon-power-off',
// command : 'shutdown',
// successMessage: 'NzbDrone shutdown has started',
// errorMessage : 'Failed to shutdown NzbDrone'
// }
}
]
},

View file

@ -181,8 +181,9 @@ require.config({
define(
[
'marionette',
'Shared/SignalRBroadcaster',
'Instrumentation/StringFormat'
], function (Marionette) {
], function (Marionette, SignalRBroadcaster) {
var app = new Marionette.Application();
@ -210,6 +211,8 @@ define(
console.log('starting application');
});
app.addInitializer(SignalRBroadcaster.appInitializer, {app: app});
app.addRegions({
navbarRegion: '#nav-region',
mainRegion : '#main-region',

58
UI/jQuery/jquery.spin.js Normal file
View file

@ -0,0 +1,58 @@
define(
[
'jquery'
], function ($) {
'use strict';
$.fn.spinForPromise = function (promise) {
var self = this;
if (!promise || promise.state() !== 'pending') {
return this;
}
promise.always(function () {
self.stopSpin();
});
return this.startSpin();
};
$.fn.startSpin = function () {
var icon = this.find('i').andSelf('i');
var iconClasses = icon.attr('class').match(/(?:^|\s)icon\-.+?(?:$|\s)/);
if (iconClasses.length === 0) {
return this;
}
var iconClass = $.trim(iconClasses[0]);
this.addClass('disabled');
if (icon.hasClass('icon-can-spin')) {
icon.addClass('icon-spin');
}
else {
icon.attr('data-idle-icon', iconClass);
icon.removeClass(iconClass);
icon.addClass('icon-nd-spinner');
}
return this;
};
$.fn.stopSpin = function () {
var icon = this.find('i').andSelf('i');
this.removeClass('disabled');
icon.removeClass('icon-spin icon-nd-spinner');
var idleIcon = icon.attr('data-idle-icon');
if (idleIcon) {
icon.addClass(idleIcon);
}
return this;
};
});