diff --git a/Gruntfile.js b/Gruntfile.js index 257bb44e6..c6618dfc3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -17,6 +17,7 @@ module.exports = function (grunt) { 'UI/JsLibraries/handlebars.runtime.js' : 'http://raw.github.com/wycats/handlebars.js/master/dist/handlebars.runtime.js', 'UI/JsLibraries/jquery.cookie.js' : 'http://raw.github.com/carhartl/jquery-cookie/master/jquery.cookie.js', 'UI/JsLibraries/jquery.js' : 'http://code.jquery.com/jquery.js', + 'UI/JsLibraries/jquery.backstretch.js' : 'http://raw.github.com/srobbin/jquery-backstretch/master/jquery.backstretch.js', //'NzbDrone.Backbone/JsLibraries/jquery.tablesorter.bootstrap.js': //'NzbDrone.Backbone/JsLibraries/jquery.tablesorter.js': 'UI/JsLibraries/require.js' : 'http://raw.github.com/jrburke/requirejs/master/require.js', diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index e7b035444..500b43e30 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -1,4 +1,4 @@ - + @@ -66,6 +66,10 @@ False ..\packages\Nancy.0.16.1\lib\net40\Nancy.dll + + False + ..\packages\Newtonsoft.Json.5.0.3\lib\net35\Newtonsoft.Json.dll + False ..\packages\NLog.2.0.1.2\lib\net40\NLog.dll diff --git a/NzbDrone.Api/REST/RestResource.cs b/NzbDrone.Api/REST/RestResource.cs index 5d0470ab3..d0e4acdc3 100644 --- a/NzbDrone.Api/REST/RestResource.cs +++ b/NzbDrone.Api/REST/RestResource.cs @@ -1,4 +1,5 @@ using FluentValidation; +using Newtonsoft.Json; namespace NzbDrone.Api.REST { @@ -6,6 +7,7 @@ namespace NzbDrone.Api.REST { public int Id { get; set; } + [JsonIgnore] public virtual string ResourceName { get diff --git a/NzbDrone.Api/packages.config b/NzbDrone.Api/packages.config index 3778d25db..639265482 100644 --- a/NzbDrone.Api/packages.config +++ b/NzbDrone.Api/packages.config @@ -3,6 +3,7 @@ + \ No newline at end of file diff --git a/UI/.idea/jsLinters/jshint.xml b/UI/.idea/jsLinters/jshint.xml index 773318293..5718af90f 100644 --- a/UI/.idea/jsLinters/jshint.xml +++ b/UI/.idea/jsLinters/jshint.xml @@ -61,7 +61,7 @@ diff --git a/UI/Index.html b/UI/Index.html index 3b9894255..dcc819db7 100644 --- a/UI/Index.html +++ b/UI/Index.html @@ -93,6 +93,7 @@ + diff --git a/UI/JsLibraries/jquery.backstretch.js b/UI/JsLibraries/jquery.backstretch.js new file mode 100644 index 000000000..effee3afa --- /dev/null +++ b/UI/JsLibraries/jquery.backstretch.js @@ -0,0 +1,357 @@ +/*! Backstretch - v2.0.3 - 2012-11-30 +* http://srobbin.com/jquery-plugins/backstretch/ +* Copyright (c) 2012 Scott Robbin; Licensed MIT */ + +;(function ($, window, undefined) { + 'use strict'; + + /* PLUGIN DEFINITION + * ========================= */ + + $.fn.backstretch = function (images, options) { + // We need at least one image + if (images === undefined || images.length === 0) { + $.error("No images were supplied for Backstretch"); + } + + /* + * Scroll the page one pixel to get the right window height on iOS + * Pretty harmless for everyone else + */ + if ($(window).scrollTop() === 0 ) { + window.scrollTo(0, 0); + } + + return this.each(function () { + var $this = $(this) + , obj = $this.data('backstretch'); + + // If we've already attached Backstretch to this element, remove the old instance. + if (obj) { + // Merge the old options with the new + options = $.extend(obj.options, options); + + // Remove the old instance + obj.destroy(true); + } + + obj = new Backstretch(this, images, options); + $this.data('backstretch', obj); + }); + }; + + // If no element is supplied, we'll attach to body + $.backstretch = function (images, options) { + // Return the instance + return $('body') + .backstretch(images, options) + .data('backstretch'); + }; + + // Custom selector + $.expr[':'].backstretch = function(elem) { + return $(elem).data('backstretch') !== undefined; + }; + + /* DEFAULTS + * ========================= */ + + $.fn.backstretch.defaults = { + centeredX: true // Should we center the image on the X axis? + , centeredY: true // Should we center the image on the Y axis? + , duration: 5000 // Amount of time in between slides (if slideshow) + , fade: 0 // Speed of fade transition between slides + }; + + /* STYLES + * + * Baked-in styles that we'll apply to our elements. + * In an effort to keep the plugin simple, these are not exposed as options. + * That said, anyone can override these in their own stylesheet. + * ========================= */ + var styles = { + wrap: { + left: 0 + , top: 0 + , overflow: 'hidden' + , margin: 0 + , padding: 0 + , height: '100%' + , width: '100%' + , zIndex: -999999 + } + , img: { + position: 'absolute' + , display: 'none' + , margin: 0 + , padding: 0 + , border: 'none' + , width: 'auto' + , height: 'auto' + , maxWidth: 'none' + , zIndex: -999999 + } + }; + + /* CLASS DEFINITION + * ========================= */ + var Backstretch = function (container, images, options) { + this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); + + /* In its simplest form, we allow Backstretch to be called on an image path. + * e.g. $.backstretch('/path/to/image.jpg') + * So, we need to turn this back into an array. + */ + this.images = $.isArray(images) ? images : [images]; + + // Preload images + $.each(this.images, function () { + $('')[0].src = this; + }); + + // Convenience reference to know if the container is body. + this.isBody = container === document.body; + + /* We're keeping track of a few different elements + * + * Container: the element that Backstretch was called on. + * Wrap: a DIV that we place the image into, so we can hide the overflow. + * Root: Convenience reference to help calculate the correct height. + */ + this.$container = $(container); + this.$wrap = $('
').css(styles.wrap).appendTo(this.$container); + this.$root = this.isBody ? supportsFixedPosition ? $(window) : $(document) : this.$container; + + // Non-body elements need some style adjustments + if (!this.isBody) { + // If the container is statically positioned, we need to make it relative, + // and if no zIndex is defined, we should set it to zero. + var position = this.$container.css('position') + , zIndex = this.$container.css('zIndex'); + + this.$container.css({ + position: position === 'static' ? 'relative' : position + , zIndex: zIndex === 'auto' ? 0 : zIndex + , background: 'none' + }); + + // Needs a higher z-index + this.$wrap.css({zIndex: -999998}); + } + + // Fixed or absolute positioning? + this.$wrap.css({ + position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' + }); + + // Set the first image + this.index = 0; + this.show(this.index); + + // Listen for resize + $(window).on('resize.backstretch', $.proxy(this.resize, this)) + .on('orientationchange.backstretch', $.proxy(function () { + // Need to do this in order to get the right window height + if (this.isBody && window.pageYOffset === 0) { + window.scrollTo(0, 1); + this.resize(); + } + }, this)); + }; + + /* PUBLIC METHODS + * ========================= */ + Backstretch.prototype = { + resize: function () { + try { + var bgCSS = {left: 0, top: 0} + , rootWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() + , bgWidth = rootWidth + , rootHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() + , bgHeight = bgWidth / this.$img.data('ratio') + , bgOffset; + + // Make adjustments based on image ratio + if (bgHeight >= rootHeight) { + bgOffset = (bgHeight - rootHeight) / 2; + if(this.options.centeredY) { + bgCSS.top = '-' + bgOffset + 'px'; + } + } else { + bgHeight = rootHeight; + bgWidth = bgHeight * this.$img.data('ratio'); + bgOffset = (bgWidth - rootWidth) / 2; + if(this.options.centeredX) { + bgCSS.left = '-' + bgOffset + 'px'; + } + } + + this.$wrap.css({width: rootWidth, height: rootHeight}) + .find('img:not(.deleteable)').css({width: bgWidth, height: bgHeight}).css(bgCSS); + } catch(err) { + // IE7 seems to trigger resize before the image is loaded. + // This try/catch block is a hack to let it fail gracefully. + } + + return this; + } + + // Show the slide at a certain position + , show: function (index) { + // Validate index + if (Math.abs(index) > this.images.length - 1) { + return; + } else { + this.index = index; + } + + // Vars + var self = this + , oldImage = self.$wrap.find('img').addClass('deleteable') + , evt = $.Event('backstretch.show', { + relatedTarget: self.$container[0] + }); + + // Pause the slideshow + clearInterval(self.interval); + + // New image + self.$img = $('') + .css(styles.img) + .bind('load', function (e) { + var imgWidth = this.width || $(e.target).width() + , imgHeight = this.height || $(e.target).height(); + + // Save the ratio + $(this).data('ratio', imgWidth / imgHeight); + + // Show the image, then delete the old one + // "speed" option has been deprecated, but we want backwards compatibilty + $(this).fadeIn(self.options.speed || self.options.fade, function () { + oldImage.remove(); + + // Resume the slideshow + if (!self.paused) { + self.cycle(); + } + + // Trigger the event + self.$container.trigger(evt, self); + }); + + // Resize + self.resize(); + }) + .appendTo(self.$wrap); + + // Hack for IE img onload event + self.$img.attr('src', self.images[index]); + return self; + } + + , next: function () { + // Next slide + return this.show(this.index < this.images.length - 1 ? this.index + 1 : 0); + } + + , prev: function () { + // Previous slide + return this.show(this.index === 0 ? this.images.length - 1 : this.index - 1); + } + + , pause: function () { + // Pause the slideshow + this.paused = true; + return this; + } + + , resume: function () { + // Resume the slideshow + this.paused = false; + this.next(); + return this; + } + + , cycle: function () { + // Start/resume the slideshow + if(this.images.length > 1) { + // Clear the interval, just in case + clearInterval(this.interval); + + this.interval = setInterval($.proxy(function () { + // Check for paused slideshow + if (!this.paused) { + this.next(); + } + }, this), this.options.duration); + } + return this; + } + + , destroy: function (preserveBackground) { + // Stop the resize events + $(window).off('resize.backstretch orientationchange.backstretch'); + + // Clear the interval + clearInterval(this.interval); + + // Remove Backstretch + if(!preserveBackground) { + this.$wrap.remove(); + } + this.$container.removeData('backstretch'); + } + }; + + /* SUPPORTS FIXED POSITION? + * + * Based on code from jQuery Mobile 1.1.0 + * http://jquerymobile.com/ + * + * In a nutshell, we need to figure out if fixed positioning is supported. + * Unfortunately, this is very difficult to do on iOS, and usually involves + * injecting content, scrolling the page, etc.. It's ugly. + * jQuery Mobile uses this workaround. It's not ideal, but works. + * + * Modified to detect IE6 + * ========================= */ + + var supportsFixedPosition = (function () { + var ua = navigator.userAgent + , platform = navigator.platform + // Rendering engine is Webkit, and capture major version + , wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ) + , wkversion = !!wkmatch && wkmatch[ 1 ] + , ffmatch = ua.match( /Fennec\/([0-9]+)/ ) + , ffversion = !!ffmatch && ffmatch[ 1 ] + , operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ) + , omversion = !!operammobilematch && operammobilematch[ 1 ] + , iematch = ua.match( /MSIE ([0-9]+)/ ) + , ieversion = !!iematch && iematch[ 1 ]; + + return !( + // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) + ((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) || + + // Opera Mini + (window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") || + (operammobilematch && omversion < 7458) || + + //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) + (ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) || + + // Firefox Mobile before 6.0 - + (ffversion && ffversion < 6) || + + // WebOS less than 3 + ("palmGetResource" in window && wkversion && wkversion < 534) || + + // MeeGo + (ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) || + + // IE6 + (ieversion && ieversion <= 6) + ); + }()); + +}(jQuery, window)); \ No newline at end of file diff --git a/UI/Series/Details/EpisodeItemTemplate.html b/UI/Series/Details/EpisodeItemTemplate.html deleted file mode 100644 index 88490cec6..000000000 --- a/UI/Series/Details/EpisodeItemTemplate.html +++ /dev/null @@ -1 +0,0 @@ -{{title}} \ No newline at end of file diff --git a/UI/Series/Details/EpisodeItemView.js b/UI/Series/Details/EpisodeItemView.js deleted file mode 100644 index e0ce02211..000000000 --- a/UI/Series/Details/EpisodeItemView.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -define(['app', 'Series/SeasonModel'], function () { - - NzbDrone.Series.Details.EpisodeItemView = Backbone.Marionette.ItemView.extend({ - template: 'Series/Details/EpisodeItemTemplate', - tagName : 'tr', - - ui: { - - }, - - events: { - - } - }); -}); diff --git a/UI/Series/Details/SeasonCompositeTemplate.html b/UI/Series/Details/SeasonCompositeTemplate.html deleted file mode 100644 index f71621a3b..000000000 --- a/UI/Series/Details/SeasonCompositeTemplate.html +++ /dev/null @@ -1,15 +0,0 @@ -

{{seasonTitle}}

- - - - - - - - - - - - - -
#TitleAir DateQualityControls
\ No newline at end of file diff --git a/UI/Series/Details/SeasonCompositeView.js b/UI/Series/Details/SeasonCompositeView.js deleted file mode 100644 index 3963f9904..000000000 --- a/UI/Series/Details/SeasonCompositeView.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict'; -define(['app', 'Series/Details/EpisodeItemView'], function () { - NzbDrone.Series.Details.SeasonCompositeView = Backbone.Marionette.CompositeView.extend({ - itemView : NzbDrone.Series.Details.EpisodeItemView, - itemViewContainer: '.x-episodes', - template : 'Series/Details/SeasonCompositeTemplate', - - initialize: function () { - this.collection = new NzbDrone.Series.EpisodeCollection(); - this.collection.fetch({data: { - seriesId : this.model.get('seriesId'), - seasonNumber: this.model.get('seasonNumber') - }}); - } - }); -}); \ No newline at end of file diff --git a/UI/Series/Details/SeasonLayout.js b/UI/Series/Details/SeasonLayout.js new file mode 100644 index 000000000..c2a585aa9 --- /dev/null +++ b/UI/Series/Details/SeasonLayout.js @@ -0,0 +1,52 @@ +'use strict'; +define(['app'], function () { + NzbDrone.Series.Details.SeasonLayout = Backbone.Marionette.Layout.extend({ + template: 'Series/Details/SeasonLayoutTemplate', + + regions: { + episodeGrid: '#x-episode-grid' + }, + + columns: [ + { + name : 'episodeNumber', + label : '#', + editable: false, + cell : 'integer' + }, + + { + name : 'title', + label : 'Title', + editable: false, + cell : 'string' + }, + { + name : 'airDate', + label : 'Air Date', + editable : false, + cell : 'datetime', + formatter: new Backgrid.AirDateFormatter() + } + ], + + initialize: function () { + this.episodeCollection = new NzbDrone.Series.EpisodeCollection(); + this.episodeCollection.fetch({data: { + seriesId : this.model.get('seriesId'), + seasonNumber: this.model.get('seasonNumber') + }}); + }, + + onShow: function () { + + this.episodeGrid.show(new Backgrid.Grid( + { + columns : this.columns, + collection: this.episodeCollection, + className : 'table table-hover' + })); + + } + }); +}); diff --git a/UI/Series/Details/SeasonLayoutTemplate.html b/UI/Series/Details/SeasonLayoutTemplate.html new file mode 100644 index 000000000..3b5b8d433 --- /dev/null +++ b/UI/Series/Details/SeasonLayoutTemplate.html @@ -0,0 +1,4 @@ +
+

{{seasonTitle}}

+
+
diff --git a/UI/Series/Details/SeriesDetailsTemplate.html b/UI/Series/Details/SeriesDetailsTemplate.html index d073f2002..1d353442a 100644 --- a/UI/Series/Details/SeriesDetailsTemplate.html +++ b/UI/Series/Details/SeriesDetailsTemplate.html @@ -1,6 +1,21 @@ -
-
- {{overview}} +
+ -
-
\ No newline at end of file +
+
+

{{title}}

+
+
+ {{overview}} +
+
+ {{network}} + {{runtime}} minutes +
+
+
+ +
diff --git a/UI/Series/Details/SeriesDetailsView.js b/UI/Series/Details/SeriesDetailsView.js index 72dfe869d..4779cabcc 100644 --- a/UI/Series/Details/SeriesDetailsView.js +++ b/UI/Series/Details/SeriesDetailsView.js @@ -1,14 +1,20 @@ "use strict"; -define(['app', 'Quality/QualityProfileCollection', 'Series/Details/SeasonCompositeView', 'Series/SeasonCollection'], function () { +define(['app', 'Quality/QualityProfileCollection', 'Series/Details/SeasonLayout', 'Series/SeasonCollection'], function () { NzbDrone.Series.Details.SeriesDetailsView = Backbone.Marionette.CompositeView.extend({ - itemView : NzbDrone.Series.Details.SeasonCompositeView, + itemView : NzbDrone.Series.Details.SeasonLayout, itemViewContainer: '.x-series-seasons', template : 'Series/Details/SeriesDetailsTemplate', initialize: function () { this.collection = new NzbDrone.Series.SeasonCollection(); this.collection.fetch({data: { seriesId: this.model.get('id') }}); + + //$.backstretch(this.model.get('fanArt')); + }, + + onClose: function(){ + $('.backstretch').remove(); } }); }); diff --git a/UI/Series/Index/SeriesIndexLayout.js b/UI/Series/Index/SeriesIndexLayout.js index 0bf484407..6f6e8a9de 100644 --- a/UI/Series/Index/SeriesIndexLayout.js +++ b/UI/Series/Index/SeriesIndexLayout.js @@ -8,7 +8,7 @@ define([ 'Series/Index/Table/AirDateCell', 'Series/Index/Table/SeriesStatusCell' ], - function (app) { + function () { NzbDrone.Series.Index.SeriesIndexLayout = Backbone.Marionette.Layout.extend({ template: 'Series/Index/SeriesIndexLayoutTemplate', @@ -27,7 +27,61 @@ define([ showTable: function () { var columns = - [ + [ + { + name : 'status', + label : '', + editable: false, + cell : 'seriesStatus' + }, + { + name : 'title', + label : 'Title', + editable: false, + cell : 'string' + }, + { + name : 'seasonCount', + label : 'Seasons', + editable: false, + cell : 'integer' + }, + { + name : 'quality', + label : 'Quality', + editable: false, + cell : 'integer' + }, + { + name : 'network', + label : 'Network', + editable: false, + cell : 'string' + }, + { + name : 'nextAiring', + label : 'Next Airing', + editable : false, + cell : 'datetime', + formatter: new Backgrid.AirDateFormatter() + }, + { + name : 'episodes', + label : 'Episodes', + editable: false, + sortable: false, + cell : 'string' + }, + { + name : 'edit', + label : '', + editable: false, + sortable: false, + cell : 'string' + } + ]; + + var grid = new Backgrid.Grid( { name: 'status', label: '', diff --git a/UI/Series/SeriesModel.js b/UI/Series/SeriesModel.js index fa5648c5f..4fffe122e 100644 --- a/UI/Series/SeriesModel.js +++ b/UI/Series/SeriesModel.js @@ -31,17 +31,28 @@ return undefined; }, + fanArt : function () { + var poster = _.find(this.get('images'), function (image) { + return image.coverType === 3; + }); + + if (poster) { + return poster.url; + } + + return undefined; + }, traktUrl : function () { return "http://trakt.tv/show/" + this.get('titleSlug'); }, isContinuing : function () { - if (this.get('status') === 0){ + if (this.get('status') === 0) { return true; } return false; }, - statusText: function () { + statusText : function () { if (this.get('status') === 0) { return 'Continuing'; } @@ -56,7 +67,7 @@ qualityProfiles : qualityProfileCollection, rootFolders : rootFolders, isExisting : false, - status: 0 + status : 0 } }); diff --git a/UI/Series/series.less b/UI/Series/series.less index 643cd5d42..ad1f7dbfe 100644 --- a/UI/Series/series.less +++ b/UI/Series/series.less @@ -13,6 +13,8 @@ display: inline-block; vertical-align: top; } +.series-page-header { + padding-bottom: 50px; } .series-posters-item { @@ -45,6 +47,9 @@ display: block; } } +.series-season { + padding-bottom: 20px; +} } .series-poster-container { @@ -66,4 +71,4 @@ left: -120px; text-align: center; } -} \ No newline at end of file +}