From 895a0c50eb6830b0f804960cb2bbab56bc3c6c38 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 21 Mar 2016 14:23:55 +0000 Subject: [PATCH 01/34] Added a subdir to CP, SickRage, Sonarr and Plex #43 --- .../SettingModels/CouchPotatoSettings.cs | 6 ++++++ .../SettingModels/PlexSettings.cs | 6 ++++++ .../SettingModels/SickRageSettings.cs | 6 ++++++ .../SettingModels/SonarrSettings.cs | 6 ++++++ PlexRequests.Helpers.Tests/UriHelperTests.cs | 19 ++++++++++++++++++- PlexRequests.Helpers/UriHelper.cs | 19 +++++++++++++++++++ .../Views/Admin/CouchPotato.cshtml | 6 ++++++ PlexRequests.UI/Views/Admin/Plex.cshtml | 6 ++++++ PlexRequests.UI/Views/Admin/Sickrage.cshtml | 7 +++++++ PlexRequests.UI/Views/Admin/Sonarr.cshtml | 6 ++++++ 10 files changed, 86 insertions(+), 1 deletion(-) diff --git a/PlexRequests.Core/SettingModels/CouchPotatoSettings.cs b/PlexRequests.Core/SettingModels/CouchPotatoSettings.cs index 7c18ca5d7..ff7ea01a5 100644 --- a/PlexRequests.Core/SettingModels/CouchPotatoSettings.cs +++ b/PlexRequests.Core/SettingModels/CouchPotatoSettings.cs @@ -38,12 +38,18 @@ namespace PlexRequests.Core.SettingModels public string ApiKey { get; set; } public bool Ssl { get; set; } public string ProfileId { get; set; } + public string SubDir { get; set; } [JsonIgnore] public Uri FullUri { get { + if (!string.IsNullOrEmpty(SubDir)) + { + var formattedSubDir = Ip.ReturnUriWithSubDir(Port, Ssl, SubDir); + return formattedSubDir; + } var formatted = Ip.ReturnUri(Port, Ssl); return formatted; } diff --git a/PlexRequests.Core/SettingModels/PlexSettings.cs b/PlexRequests.Core/SettingModels/PlexSettings.cs index 89bc01931..fa39af857 100644 --- a/PlexRequests.Core/SettingModels/PlexSettings.cs +++ b/PlexRequests.Core/SettingModels/PlexSettings.cs @@ -36,12 +36,18 @@ namespace PlexRequests.Core.SettingModels public string Ip { get; set; } public int Port { get; set; } public bool Ssl { get; set; } + public string SubDir { get; set; } [JsonIgnore] public Uri FullUri { get { + if (!string.IsNullOrEmpty(SubDir)) + { + var formattedSubDir = Ip.ReturnUriWithSubDir(Port, Ssl, SubDir); + return formattedSubDir; + } var formatted = Ip.ReturnUri(Port, Ssl); return formatted; } diff --git a/PlexRequests.Core/SettingModels/SickRageSettings.cs b/PlexRequests.Core/SettingModels/SickRageSettings.cs index 344e41434..657de3b50 100644 --- a/PlexRequests.Core/SettingModels/SickRageSettings.cs +++ b/PlexRequests.Core/SettingModels/SickRageSettings.cs @@ -39,12 +39,18 @@ namespace PlexRequests.Core.SettingModels public string ApiKey { get; set; } public string QualityProfile { get; set; } public bool Ssl { get; set; } + public string SubDir { get; set; } [JsonIgnore] public Uri FullUri { get { + if (!string.IsNullOrEmpty(SubDir)) + { + var formattedSubDir = Ip.ReturnUriWithSubDir(Port, Ssl, SubDir); + return formattedSubDir; + } var formatted = Ip.ReturnUri(Port, Ssl); return formatted; } diff --git a/PlexRequests.Core/SettingModels/SonarrSettings.cs b/PlexRequests.Core/SettingModels/SonarrSettings.cs index 2c128f993..3c680e0e5 100644 --- a/PlexRequests.Core/SettingModels/SonarrSettings.cs +++ b/PlexRequests.Core/SettingModels/SonarrSettings.cs @@ -41,12 +41,18 @@ namespace PlexRequests.Core.SettingModels public bool SeasonFolders { get; set; } public string RootPath { get; set; } public bool Ssl { get; set; } + public string SubDir { get; set; } [JsonIgnore] public Uri FullUri { get { + if (!string.IsNullOrEmpty(SubDir)) + { + var formattedSubDir = Ip.ReturnUriWithSubDir(Port, Ssl, SubDir); + return formattedSubDir; + } var formatted = Ip.ReturnUri(Port, Ssl); return formatted; } diff --git a/PlexRequests.Helpers.Tests/UriHelperTests.cs b/PlexRequests.Helpers.Tests/UriHelperTests.cs index 7c767dfd0..93fb32997 100644 --- a/PlexRequests.Helpers.Tests/UriHelperTests.cs +++ b/PlexRequests.Helpers.Tests/UriHelperTests.cs @@ -26,6 +26,7 @@ #endregion using System; +using System.Linq.Expressions; using NUnit.Framework; namespace PlexRequests.Helpers.Tests @@ -35,7 +36,7 @@ namespace PlexRequests.Helpers.Tests { [TestCaseSource(nameof(UriData))] public void CreateUri1(string uri, Uri expected) - { + { var result = uri.ReturnUri(); Assert.That(result, Is.EqualTo(expected)); @@ -58,6 +59,14 @@ namespace PlexRequests.Helpers.Tests Assert.That(result, Is.EqualTo(expected)); } + [TestCaseSource(nameof(UriDataWithSubDir))] + public void CreateUriWithSubDir(string uri, int port, bool ssl, string subDir, Uri expected) + { + var result = uri.ReturnUriWithSubDir(port, ssl, subDir); + + Assert.That(result, Is.EqualTo(expected)); + } + static readonly object[] UriData = { new object[] { "google.com", new Uri("http://google.com/"), }, @@ -84,5 +93,13 @@ namespace PlexRequests.Helpers.Tests new object[] {"http://www.google.com/id=2", 443, new Uri("http://www.google.com:443/id=2") }, new object[] {"https://www.google.com/id=2", 443, new Uri("https://www.google.com:443/id=2") }, }; + + static readonly object[] UriDataWithSubDir = +{ + new object[] {"www.google.com", 80, false,"test", new Uri("http://www.google.com:80/test"), }, + new object[] {"www.google.com", 443, false,"test", new Uri("http://www.google.com:443/test") }, + new object[] {"http://www.google.com", 443, true,"test", new Uri("https://www.google.com:443/test") }, + new object[] {"https://www.google.com", 443,true,"test", new Uri("https://www.google.com:443/test") }, + }; } } \ No newline at end of file diff --git a/PlexRequests.Helpers/UriHelper.cs b/PlexRequests.Helpers/UriHelper.cs index c33d5ddf4..707f70436 100644 --- a/PlexRequests.Helpers/UriHelper.cs +++ b/PlexRequests.Helpers/UriHelper.cs @@ -53,7 +53,10 @@ namespace PlexRequests.Helpers /// /// The value. /// The port. + /// if set to true [SSL]. + /// The subdir. /// + /// The URI is null, please check your settings to make sure you have configured the applications correctly. /// public static Uri ReturnUri(this string val, int port, bool ssl = default(bool)) { @@ -93,5 +96,21 @@ namespace PlexRequests.Helpers throw new Exception(exception.Message, exception); } } + + public static Uri ReturnUriWithSubDir(this string val, int port, bool ssl, string subDir) + { + var uriBuilder = new UriBuilder(val); + if (ssl) + { + uriBuilder.Scheme = Uri.UriSchemeHttps; + } + if (!string.IsNullOrEmpty(subDir)) + { + uriBuilder.Path = subDir; + } + uriBuilder.Port = port; + + return uriBuilder.Uri; + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/CouchPotato.cshtml b/PlexRequests.UI/Views/Admin/CouchPotato.cshtml index 4be47db0a..9e0aff8ef 100644 --- a/PlexRequests.UI/Views/Admin/CouchPotato.cshtml +++ b/PlexRequests.UI/Views/Admin/CouchPotato.cshtml @@ -51,6 +51,12 @@ +
+ +
+ +
+
diff --git a/PlexRequests.UI/Views/Admin/Plex.cshtml b/PlexRequests.UI/Views/Admin/Plex.cshtml index fca742fdf..af3702a63 100644 --- a/PlexRequests.UI/Views/Admin/Plex.cshtml +++ b/PlexRequests.UI/Views/Admin/Plex.cshtml @@ -43,6 +43,12 @@
+
+ +
+ +
+
diff --git a/PlexRequests.UI/Views/Admin/Sickrage.cshtml b/PlexRequests.UI/Views/Admin/Sickrage.cshtml index e03f6a458..15d57d873 100644 --- a/PlexRequests.UI/Views/Admin/Sickrage.cshtml +++ b/PlexRequests.UI/Views/Admin/Sickrage.cshtml @@ -64,6 +64,13 @@
+ +
+ +
+ +
+
diff --git a/PlexRequests.UI/Views/Admin/Sonarr.cshtml b/PlexRequests.UI/Views/Admin/Sonarr.cshtml index 5185e8ff4..454f06578 100644 --- a/PlexRequests.UI/Views/Admin/Sonarr.cshtml +++ b/PlexRequests.UI/Views/Admin/Sonarr.cshtml @@ -64,6 +64,12 @@
+
+ +
+ +
+
From bbe2bcbfed45bb5b8edd597fe78d655d285a20cf Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 21 Mar 2016 17:18:57 +0000 Subject: [PATCH 02/34] Started #57, currently there is a bug where the TV list won't filter --- PlexRequests.UI/Content/custom.css | 3 + PlexRequests.UI/Content/custom.min.css | 2 +- PlexRequests.UI/Content/custom.scss | 4 + PlexRequests.UI/Content/jquery.mixitup.js | 2098 +++++++++++++++++++ PlexRequests.UI/Content/requests.js | 18 + PlexRequests.UI/PlexRequests.UI.csproj | 3 + PlexRequests.UI/Views/Requests/Index.cshtml | 46 +- PlexRequests.UI/Views/Shared/_Layout.cshtml | 1 + 8 files changed, 2164 insertions(+), 11 deletions(-) create mode 100644 PlexRequests.UI/Content/jquery.mixitup.js diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css index a8c70de18..c571269b4 100644 --- a/PlexRequests.UI/Content/custom.css +++ b/PlexRequests.UI/Content/custom.css @@ -120,3 +120,6 @@ label { background-color: #5cb85c !important; border-color: #5cb85c !important; } +#movieList .mix { + display: none; } + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 37bb1257f..8385aac7b 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index a87a44a97..e1944c04c 100644 --- a/PlexRequests.UI/Content/custom.scss +++ b/PlexRequests.UI/Content/custom.scss @@ -157,3 +157,7 @@ label { background-color: $success-colour $i; border-color: $success-colour $i; } + +#movieList .mix{ + display: none; +} \ No newline at end of file diff --git a/PlexRequests.UI/Content/jquery.mixitup.js b/PlexRequests.UI/Content/jquery.mixitup.js new file mode 100644 index 000000000..b23a60279 --- /dev/null +++ b/PlexRequests.UI/Content/jquery.mixitup.js @@ -0,0 +1,2098 @@ +/**! + * MixItUp v2.1.11 + * + * @copyright Copyright 2015 KunkaLabs Limited. + * @author KunkaLabs Limited. + * @link https://mixitup.kunkalabs.com + * + * @license Commercial use requires a commercial license. + * https://mixitup.kunkalabs.com/licenses/ + * + * Non-commercial use permitted under terms of CC-BY-NC license. + * http://creativecommons.org/licenses/by-nc/3.0/ + */ + +(function($, undf){ + 'use strict'; + + /** + * MixItUp Constructor Function + * @constructor + * @extends jQuery + */ + + $.MixItUp = function(){ + var self = this; + + self._execAction('_constructor', 0); + + $.extend(self, { + + /* Public Properties + ---------------------------------------------------------------------- */ + + selectors: { + target: '.mix', + filter: '.filter', + sort: '.sort' + }, + + animation: { + enable: true, + effects: 'fade scale', + duration: 600, + easing: 'ease', + perspectiveDistance: '3000', + perspectiveOrigin: '50% 50%', + queue: true, + queueLimit: 1, + animateChangeLayout: false, + animateResizeContainer: true, + animateResizeTargets: false, + staggerSequence: false, + reverseOut: false + }, + + callbacks: { + onMixLoad: false, + onMixStart: false, + onMixBusy: false, + onMixEnd: false, + onMixFail: false, + _user: false + }, + + controls: { + enable: true, + live: false, + toggleFilterButtons: false, + toggleLogic: 'or', + activeClass: 'active' + }, + + layout: { + display: 'inline-block', + containerClass: '', + containerClassFail: 'fail' + }, + + load: { + filter: 'all', + sort: false + }, + + /* Private Properties + ---------------------------------------------------------------------- */ + + _$body: null, + _$container: null, + _$targets: null, + _$parent: null, + _$sortButtons: null, + _$filterButtons: null, + + _suckMode: false, + _mixing: false, + _sorting: false, + _clicking: false, + _loading: true, + _changingLayout: false, + _changingClass: false, + _changingDisplay: false, + + _origOrder: [], + _startOrder: [], + _newOrder: [], + _activeFilter: null, + _toggleArray: [], + _toggleString: '', + _activeSort: 'default:asc', + _newSort: null, + _startHeight: null, + _newHeight: null, + _incPadding: true, + _newDisplay: null, + _newClass: null, + _targetsBound: 0, + _targetsDone: 0, + _queue: [], + + _$show: $(), + _$hide: $() + }); + + self._execAction('_constructor', 1); + }; + + /** + * MixItUp Prototype + * @override + */ + + $.MixItUp.prototype = { + constructor: $.MixItUp, + + /* Static Properties + ---------------------------------------------------------------------- */ + + _instances: {}, + _handled: { + _filter: {}, + _sort: {} + }, + _bound: { + _filter: {}, + _sort: {} + }, + _actions: {}, + _filters: {}, + + /* Static Methods + ---------------------------------------------------------------------- */ + + /** + * Extend + * @since 2.1.0 + * @param {object} new properties/methods + * @extends {object} prototype + */ + + extend: function(extension){ + for(var key in extension){ + $.MixItUp.prototype[key] = extension[key]; + } + }, + + /** + * Add Action + * @since 2.1.0 + * @param {string} hook name + * @param {string} namespace + * @param {function} function to execute + * @param {number} priority + * @extends {object} $.MixItUp.prototype._actions + */ + + addAction: function(hook, name, func, priority){ + $.MixItUp.prototype._addHook('_actions', hook, name, func, priority); + }, + + /** + * Add Filter + * @since 2.1.0 + * @param {string} hook name + * @param {string} namespace + * @param {function} function to execute + * @param {number} priority + * @extends {object} $.MixItUp.prototype._filters + */ + + addFilter: function(hook, name, func, priority){ + $.MixItUp.prototype._addHook('_filters', hook, name, func, priority); + }, + + /** + * Add Hook + * @since 2.1.0 + * @param {string} type of hook + * @param {string} hook name + * @param {function} function to execute + * @param {number} priority + * @extends {object} $.MixItUp.prototype._filters + */ + + _addHook: function(type, hook, name, func, priority){ + var collection = $.MixItUp.prototype[type], + obj = {}; + + priority = (priority === 1 || priority === 'post') ? 'post' : 'pre'; + + obj[hook] = {}; + obj[hook][priority] = {}; + obj[hook][priority][name] = func; + + $.extend(true, collection, obj); + }, + + + /* Private Methods + ---------------------------------------------------------------------- */ + + /** + * Initialise + * @since 2.0.0 + * @param {object} domNode + * @param {object} config + */ + + _init: function(domNode, config){ + var self = this; + + self._execAction('_init', 0, arguments); + + config && $.extend(true, self, config); + + self._$body = $('body'); + self._domNode = domNode; + self._$container = $(domNode); + self._$container.addClass(self.layout.containerClass); + self._id = domNode.id; + + self._platformDetect(); + + self._brake = self._getPrefixedCSS('transition', 'none'); + + self._refresh(true); + + self._$parent = self._$targets.parent().length ? self._$targets.parent() : self._$container; + + if(self.load.sort){ + self._newSort = self._parseSort(self.load.sort); + self._newSortString = self.load.sort; + self._activeSort = self.load.sort; + self._sort(); + self._printSort(); + } + + self._activeFilter = self.load.filter === 'all' ? + self.selectors.target : + self.load.filter === 'none' ? + '' : + self.load.filter; + + self.controls.enable && self._bindHandlers(); + + if(self.controls.toggleFilterButtons){ + self._buildToggleArray(); + + for(var i = 0; i < self._toggleArray.length; i++){ + self._updateControls({filter: self._toggleArray[i], sort: self._activeSort}, true); + }; + } else if(self.controls.enable){ + self._updateControls({filter: self._activeFilter, sort: self._activeSort}); + } + + self._filter(); + + self._init = true; + + self._$container.data('mixItUp',self); + + self._execAction('_init', 1, arguments); + + self._buildState(); + + self._$targets.css(self._brake); + + self._goMix(self.animation.enable); + }, + + /** + * Platform Detect + * @since 2.0.0 + */ + + _platformDetect: function(){ + var self = this, + vendorsTrans = ['Webkit', 'Moz', 'O', 'ms'], + vendorsRAF = ['webkit', 'moz'], + chrome = window.navigator.appVersion.match(/Chrome\/(\d+)\./) || false, + ff = typeof InstallTrigger !== 'undefined', + prefix = function(el){ + for (var i = 0; i < vendorsTrans.length; i++){ + if (vendorsTrans[i] + 'Transition' in el.style){ + return { + prefix: '-'+vendorsTrans[i].toLowerCase()+'-', + vendor: vendorsTrans[i] + }; + }; + }; + return 'transition' in el.style ? '' : false; + }, + transPrefix = prefix(self._domNode); + + self._execAction('_platformDetect', 0); + + self._chrome = chrome ? parseInt(chrome[1], 10) : false; + self._ff = ff ? parseInt(window.navigator.userAgent.match(/rv:([^)]+)\)/)[1]) : false; + self._prefix = transPrefix.prefix; + self._vendor = transPrefix.vendor; + self._suckMode = window.atob && self._prefix ? false : true; + + self._suckMode && (self.animation.enable = false); + (self._ff && self._ff <= 4) && (self.animation.enable = false); + + /* Polyfills + ---------------------------------------------------------------------- */ + + /** + * window.requestAnimationFrame + */ + + for(var x = 0; x < vendorsRAF.length && !window.requestAnimationFrame; x++){ + window.requestAnimationFrame = window[vendorsRAF[x]+'RequestAnimationFrame']; + } + + /** + * Object.getPrototypeOf + */ + + if(typeof Object.getPrototypeOf !== 'function'){ + if(typeof 'test'.__proto__ === 'object'){ + Object.getPrototypeOf = function(object){ + return object.__proto__; + }; + } else { + Object.getPrototypeOf = function(object){ + return object.constructor.prototype; + }; + } + } + + /** + * Element.nextElementSibling + */ + + if(self._domNode.nextElementSibling === undf){ + Object.defineProperty(Element.prototype, 'nextElementSibling',{ + get: function(){ + var el = this.nextSibling; + + while(el){ + if(el.nodeType ===1){ + return el; + } + el = el.nextSibling; + } + return null; + } + }); + } + + self._execAction('_platformDetect', 1); + }, + + /** + * Refresh + * @since 2.0.0 + * @param {boolean} init + * @param {boolean} force + */ + + _refresh: function(init, force){ + var self = this; + + self._execAction('_refresh', 0, arguments); + + self._$targets = self._$container.find(self.selectors.target); + + for(var i = 0; i < self._$targets.length; i++){ + var target = self._$targets[i]; + + if(target.dataset === undf || force){ + + target.dataset = {}; + + for(var j = 0; j < target.attributes.length; j++){ + + var attr = target.attributes[j], + name = attr.name, + val = attr.value; + + if(name.indexOf('data-') > -1){ + var dataName = self._helpers._camelCase(name.substring(5,name.length)); + target.dataset[dataName] = val; + } + } + } + + if(target.mixParent === undf){ + target.mixParent = self._id; + } + } + + if( + (self._$targets.length && init) || + (!self._origOrder.length && self._$targets.length) + ){ + self._origOrder = []; + + for(var i = 0; i < self._$targets.length; i++){ + var target = self._$targets[i]; + + self._origOrder.push(target); + } + } + + self._execAction('_refresh', 1, arguments); + }, + + /** + * Bind Handlers + * @since 2.0.0 + */ + + _bindHandlers: function(){ + var self = this, + filters = $.MixItUp.prototype._bound._filter, + sorts = $.MixItUp.prototype._bound._sort; + + self._execAction('_bindHandlers', 0); + + if(self.controls.live){ + self._$body + .on('click.mixItUp.'+self._id, self.selectors.sort, function(){ + self._processClick($(this), 'sort'); + }) + .on('click.mixItUp.'+self._id, self.selectors.filter, function(){ + self._processClick($(this), 'filter'); + }); + } else { + self._$sortButtons = $(self.selectors.sort); + self._$filterButtons = $(self.selectors.filter); + + self._$sortButtons.on('click.mixItUp.'+self._id, function(){ + self._processClick($(this), 'sort'); + }); + + self._$filterButtons.on('click.mixItUp.'+self._id, function(){ + self._processClick($(this), 'filter'); + }); + } + + filters[self.selectors.filter] = (filters[self.selectors.filter] === undf) ? 1 : filters[self.selectors.filter] + 1; + sorts[self.selectors.sort] = (sorts[self.selectors.sort] === undf) ? 1 : sorts[self.selectors.sort] + 1; + + self._execAction('_bindHandlers', 1); + }, + + /** + * Process Click + * @since 2.0.0 + * @param {object} $button + * @param {string} type + */ + + _processClick: function($button, type){ + var self = this, + trackClick = function($button, type, off){ + var proto = $.MixItUp.prototype; + + proto._handled['_'+type][self.selectors[type]] = (proto._handled['_'+type][self.selectors[type]] === undf) ? + 1 : + proto._handled['_'+type][self.selectors[type]] + 1; + + if(proto._handled['_'+type][self.selectors[type]] === proto._bound['_'+type][self.selectors[type]]){ + $button[(off ? 'remove' : 'add')+'Class'](self.controls.activeClass); + delete proto._handled['_'+type][self.selectors[type]]; + } + }; + + self._execAction('_processClick', 0, arguments); + + if(!self._mixing || (self.animation.queue && self._queue.length < self.animation.queueLimit)){ + self._clicking = true; + + if(type === 'sort'){ + var sort = $button.attr('data-sort'); + + if(!$button.hasClass(self.controls.activeClass) || sort.indexOf('random') > -1){ + $(self.selectors.sort).removeClass(self.controls.activeClass); + trackClick($button, type); + self.sort(sort); + } + } + + if(type === 'filter') { + var filter = $button.attr('data-filter'), + ndx, + seperator = self.controls.toggleLogic === 'or' ? ',' : ''; + + if(!self.controls.toggleFilterButtons){ + if(!$button.hasClass(self.controls.activeClass)){ + $(self.selectors.filter).removeClass(self.controls.activeClass); + trackClick($button, type); + self.filter(filter); + } + } else { + self._buildToggleArray(); + + if(!$button.hasClass(self.controls.activeClass)){ + trackClick($button, type); + + self._toggleArray.push(filter); + } else { + trackClick($button, type, true); + ndx = self._toggleArray.indexOf(filter); + self._toggleArray.splice(ndx, 1); + } + + self._toggleArray = $.grep(self._toggleArray,function(n){return(n);}); + + self._toggleString = self._toggleArray.join(seperator); + + self.filter(self._toggleString); + } + } + + self._execAction('_processClick', 1, arguments); + } else { + if(typeof self.callbacks.onMixBusy === 'function'){ + self.callbacks.onMixBusy.call(self._domNode, self._state, self); + } + self._execAction('_processClickBusy', 1, arguments); + } + }, + + /** + * Build Toggle Array + * @since 2.0.0 + */ + + _buildToggleArray: function(){ + var self = this, + activeFilter = self._activeFilter.replace(/\s/g, ''); + + self._execAction('_buildToggleArray', 0, arguments); + + if(self.controls.toggleLogic === 'or'){ + self._toggleArray = activeFilter.split(','); + } else { + self._toggleArray = activeFilter.split('.'); + + !self._toggleArray[0] && self._toggleArray.shift(); + + for(var i = 0, filter; filter = self._toggleArray[i]; i++){ + self._toggleArray[i] = '.'+filter; + } + } + + self._execAction('_buildToggleArray', 1, arguments); + }, + + /** + * Update Controls + * @since 2.0.0 + * @param {object} command + * @param {boolean} multi + */ + + _updateControls: function(command, multi){ + var self = this, + output = { + filter: command.filter, + sort: command.sort + }, + update = function($el, filter){ + try { + (multi && type === 'filter' && !(output.filter === 'none' || output.filter === '')) ? + $el.filter(filter).addClass(self.controls.activeClass) : + $el.removeClass(self.controls.activeClass).filter(filter).addClass(self.controls.activeClass); + } catch(e) {} + }, + type = 'filter', + $el = null; + + self._execAction('_updateControls', 0, arguments); + + (command.filter === undf) && (output.filter = self._activeFilter); + (command.sort === undf) && (output.sort = self._activeSort); + (output.filter === self.selectors.target) && (output.filter = 'all'); + + for(var i = 0; i < 2; i++){ + $el = self.controls.live ? $(self.selectors[type]) : self['_$'+type+'Buttons']; + $el && update($el, '[data-'+type+'="'+output[type]+'"]'); + type = 'sort'; + } + + self._execAction('_updateControls', 1, arguments); + }, + + /** + * Filter (private) + * @since 2.0.0 + */ + + _filter: function(){ + var self = this; + + self._execAction('_filter', 0); + + for(var i = 0; i < self._$targets.length; i++){ + var $target = $(self._$targets[i]); + + if($target.is(self._activeFilter)){ + self._$show = self._$show.add($target); + } else { + self._$hide = self._$hide.add($target); + } + } + + self._execAction('_filter', 1); + }, + + /** + * Sort (private) + * @since 2.0.0 + */ + + _sort: function(){ + var self = this, + arrayShuffle = function(oldArray){ + var newArray = oldArray.slice(), + len = newArray.length, + i = len; + + while(i--){ + var p = parseInt(Math.random()*len); + var t = newArray[i]; + newArray[i] = newArray[p]; + newArray[p] = t; + }; + return newArray; + }; + + self._execAction('_sort', 0); + + self._startOrder = []; + + for(var i = 0; i < self._$targets.length; i++){ + var target = self._$targets[i]; + + self._startOrder.push(target); + } + + switch(self._newSort[0].sortBy){ + case 'default': + self._newOrder = self._origOrder; + break; + case 'random': + self._newOrder = arrayShuffle(self._startOrder); + break; + case 'custom': + self._newOrder = self._newSort[0].order; + break; + default: + self._newOrder = self._startOrder.concat().sort(function(a, b){ + return self._compare(a, b); + }); + } + + self._execAction('_sort', 1); + }, + + /** + * Compare Algorithm + * @since 2.0.0 + * @param {string|number} a + * @param {string|number} b + * @param {number} depth (recursion) + * @return {number} + */ + + _compare: function(a, b, depth){ + depth = depth ? depth : 0; + + var self = this, + order = self._newSort[depth].order, + getData = function(el){ + return el.dataset[self._newSort[depth].sortBy] || 0; + }, + attrA = isNaN(getData(a) * 1) ? getData(a).toLowerCase() : getData(a) * 1, + attrB = isNaN(getData(b) * 1) ? getData(b).toLowerCase() : getData(b) * 1; + + if(attrA < attrB) + return order === 'asc' ? -1 : 1; + if(attrA > attrB) + return order === 'asc' ? 1 : -1; + if(attrA === attrB && self._newSort.length > depth+1) + return self._compare(a, b, depth+1); + + return 0; + }, + + /** + * Print Sort + * @since 2.0.0 + * @param {boolean} reset + */ + + _printSort: function(reset){ + var self = this, + order = reset ? self._startOrder : self._newOrder, + targets = self._$parent[0].querySelectorAll(self.selectors.target), + nextSibling = targets.length ? targets[targets.length -1].nextElementSibling : null, + frag = document.createDocumentFragment(); + + self._execAction('_printSort', 0, arguments); + + for(var i = 0; i < targets.length; i++){ + var target = targets[i], + whiteSpace = target.nextSibling; + + if(target.style.position === 'absolute') continue; + + if(whiteSpace && whiteSpace.nodeName === '#text'){ + self._$parent[0].removeChild(whiteSpace); + } + + self._$parent[0].removeChild(target); + } + + for(var i = 0; i < order.length; i++){ + var el = order[i]; + + if(self._newSort[0].sortBy === 'default' && self._newSort[0].order === 'desc' && !reset){ + var firstChild = frag.firstChild; + frag.insertBefore(el, firstChild); + frag.insertBefore(document.createTextNode(' '), el); + } else { + frag.appendChild(el); + frag.appendChild(document.createTextNode(' ')); + } + } + + nextSibling ? + self._$parent[0].insertBefore(frag, nextSibling) : + self._$parent[0].appendChild(frag); + + self._execAction('_printSort', 1, arguments); + }, + + /** + * Parse Sort + * @since 2.0.0 + * @param {string} sortString + * @return {array} newSort + */ + + _parseSort: function(sortString){ + var self = this, + rules = typeof sortString === 'string' ? sortString.split(' ') : [sortString], + newSort = []; + + for(var i = 0; i < rules.length; i++){ + var rule = typeof sortString === 'string' ? rules[i].split(':') : ['custom', rules[i]], + ruleObj = { + sortBy: self._helpers._camelCase(rule[0]), + order: rule[1] || 'asc' + }; + + newSort.push(ruleObj); + + if(ruleObj.sortBy === 'default' || ruleObj.sortBy === 'random') break; + } + + return self._execFilter('_parseSort', newSort, arguments); + }, + + /** + * Parse Effects + * @since 2.0.0 + * @return {object} effects + */ + + _parseEffects: function(){ + var self = this, + effects = { + opacity: '', + transformIn: '', + transformOut: '', + filter: '' + }, + parse = function(effect, extract, reverse){ + if(self.animation.effects.indexOf(effect) > -1){ + if(extract){ + var propIndex = self.animation.effects.indexOf(effect+'('); + if(propIndex > -1){ + var str = self.animation.effects.substring(propIndex), + match = /\(([^)]+)\)/.exec(str), + val = match[1]; + + return {val: val}; + } + } + return true; + } else { + return false; + } + }, + negate = function(value, invert){ + if(invert){ + return value.charAt(0) === '-' ? value.substr(1, value.length) : '-'+value; + } else { + return value; + } + }, + buildTransform = function(key, invert){ + var transforms = [ + ['scale', '.01'], + ['translateX', '20px'], + ['translateY', '20px'], + ['translateZ', '20px'], + ['rotateX', '90deg'], + ['rotateY', '90deg'], + ['rotateZ', '180deg'], + ]; + + for(var i = 0; i < transforms.length; i++){ + var prop = transforms[i][0], + def = transforms[i][1], + inverted = invert && prop !== 'scale'; + + effects[key] += parse(prop) ? prop+'('+negate(parse(prop, true).val || def, inverted)+') ' : ''; + } + }; + + effects.opacity = parse('fade') ? parse('fade',true).val || '0' : '1'; + + buildTransform('transformIn'); + + self.animation.reverseOut ? buildTransform('transformOut', true) : (effects.transformOut = effects.transformIn); + + effects.transition = {}; + + effects.transition = self._getPrefixedCSS('transition','all '+self.animation.duration+'ms '+self.animation.easing+', opacity '+self.animation.duration+'ms linear'); + + self.animation.stagger = parse('stagger') ? true : false; + self.animation.staggerDuration = parseInt(parse('stagger') ? (parse('stagger',true).val ? parse('stagger',true).val : 100) : 100); + + return self._execFilter('_parseEffects', effects); + }, + + /** + * Build State + * @since 2.0.0 + * @param {boolean} future + * @return {object} futureState + */ + + _buildState: function(future){ + var self = this, + state = {}; + + self._execAction('_buildState', 0); + + state = { + activeFilter: self._activeFilter === '' ? 'none' : self._activeFilter, + activeSort: future && self._newSortString ? self._newSortString : self._activeSort, + fail: !self._$show.length && self._activeFilter !== '', + $targets: self._$targets, + $show: self._$show, + $hide: self._$hide, + totalTargets: self._$targets.length, + totalShow: self._$show.length, + totalHide: self._$hide.length, + display: future && self._newDisplay ? self._newDisplay : self.layout.display + }; + + if(future){ + return self._execFilter('_buildState', state); + } else { + self._state = state; + + self._execAction('_buildState', 1); + } + }, + + /** + * Go Mix + * @since 2.0.0 + * @param {boolean} animate + */ + + _goMix: function(animate){ + var self = this, + phase1 = function(){ + if(self._chrome && (self._chrome === 31)){ + chromeFix(self._$parent[0]); + } + + self._setInter(); + + phase2(); + }, + phase2 = function(){ + var scrollTop = window.pageYOffset, + scrollLeft = window.pageXOffset, + docHeight = document.documentElement.scrollHeight; + + self._getInterMixData(); + + self._setFinal(); + + self._getFinalMixData(); + + (window.pageYOffset !== scrollTop) && window.scrollTo(scrollLeft, scrollTop); + + self._prepTargets(); + + if(window.requestAnimationFrame){ + requestAnimationFrame(phase3); + } else { + setTimeout(function(){ + phase3(); + },20); + } + }, + phase3 = function(){ + self._animateTargets(); + + if(self._targetsBound === 0){ + self._cleanUp(); + } + }, + chromeFix = function(grid){ + var parent = grid.parentElement, + placeholder = document.createElement('div'), + frag = document.createDocumentFragment(); + + parent.insertBefore(placeholder, grid); + frag.appendChild(grid); + parent.replaceChild(grid, placeholder); + }, + futureState = self._buildState(true); + + self._execAction('_goMix', 0, arguments); + + !self.animation.duration && (animate = false); + + self._mixing = true; + + self._$container.removeClass(self.layout.containerClassFail); + + if(typeof self.callbacks.onMixStart === 'function'){ + self.callbacks.onMixStart.call(self._domNode, self._state, futureState, self); + } + + self._$container.trigger('mixStart', [self._state, futureState, self]); + + self._getOrigMixData(); + + if(animate && !self._suckMode){ + + window.requestAnimationFrame ? + requestAnimationFrame(phase1) : + phase1(); + + } else { + self._cleanUp(); + } + + self._execAction('_goMix', 1, arguments); + }, + + /** + * Get Target Data + * @since 2.0.0 + */ + + _getTargetData: function(el, stage){ + var self = this, + elStyle; + + el.dataset[stage+'PosX'] = el.offsetLeft; + el.dataset[stage+'PosY'] = el.offsetTop; + + if(self.animation.animateResizeTargets){ + elStyle = !self._suckMode ? + window.getComputedStyle(el) : + { + marginBottom: '', + marginRight: '' + }; + + el.dataset[stage+'MarginBottom'] = parseInt(elStyle.marginBottom); + el.dataset[stage+'MarginRight'] = parseInt(elStyle.marginRight); + el.dataset[stage+'Width'] = el.offsetWidth; + el.dataset[stage+'Height'] = el.offsetHeight; + } + }, + + /** + * Get Original Mix Data + * @since 2.0.0 + */ + + _getOrigMixData: function(){ + var self = this, + parentStyle = !self._suckMode ? window.getComputedStyle(self._$parent[0]) : {boxSizing: ''}, + parentBS = parentStyle.boxSizing || parentStyle[self._vendor+'BoxSizing']; + + self._incPadding = (parentBS === 'border-box'); + + self._execAction('_getOrigMixData', 0); + + !self._suckMode && (self.effects = self._parseEffects()); + + self._$toHide = self._$hide.filter(':visible'); + self._$toShow = self._$show.filter(':hidden'); + self._$pre = self._$targets.filter(':visible'); + + self._startHeight = self._incPadding ? + self._$parent.outerHeight() : + self._$parent.height(); + + for(var i = 0; i < self._$pre.length; i++){ + var el = self._$pre[i]; + + self._getTargetData(el, 'orig'); + } + + self._execAction('_getOrigMixData', 1); + }, + + /** + * Set Intermediate Positions + * @since 2.0.0 + */ + + _setInter: function(){ + var self = this; + + self._execAction('_setInter', 0); + + if(self._changingLayout && self.animation.animateChangeLayout){ + self._$toShow.css('display',self._newDisplay); + + if(self._changingClass){ + self._$container + .removeClass(self.layout.containerClass) + .addClass(self._newClass); + } + } else { + self._$toShow.css('display', self.layout.display); + } + + self._execAction('_setInter', 1); + }, + + /** + * Get Intermediate Mix Data + * @since 2.0.0 + */ + + _getInterMixData: function(){ + var self = this; + + self._execAction('_getInterMixData', 0); + + for(var i = 0; i < self._$toShow.length; i++){ + var el = self._$toShow[i]; + + self._getTargetData(el, 'inter'); + } + + for(var i = 0; i < self._$pre.length; i++){ + var el = self._$pre[i]; + + self._getTargetData(el, 'inter'); + } + + self._execAction('_getInterMixData', 1); + }, + + /** + * Set Final Positions + * @since 2.0.0 + */ + + _setFinal: function(){ + var self = this; + + self._execAction('_setFinal', 0); + + self._sorting && self._printSort(); + + self._$toHide.removeStyle('display'); + + if(self._changingLayout && self.animation.animateChangeLayout){ + self._$pre.css('display',self._newDisplay); + } + + self._execAction('_setFinal', 1); + }, + + /** + * Get Final Mix Data + * @since 2.0.0 + */ + + _getFinalMixData: function(){ + var self = this; + + self._execAction('_getFinalMixData', 0); + + for(var i = 0; i < self._$toShow.length; i++){ + var el = self._$toShow[i]; + + self._getTargetData(el, 'final'); + } + + for(var i = 0; i < self._$pre.length; i++){ + var el = self._$pre[i]; + + self._getTargetData(el, 'final'); + } + + self._newHeight = self._incPadding ? + self._$parent.outerHeight() : + self._$parent.height(); + + self._sorting && self._printSort(true); + + self._$toShow.removeStyle('display'); + + self._$pre.css('display',self.layout.display); + + if(self._changingClass && self.animation.animateChangeLayout){ + self._$container + .removeClass(self._newClass) + .addClass(self.layout.containerClass); + } + + self._execAction('_getFinalMixData', 1); + }, + + /** + * Prepare Targets + * @since 2.0.0 + */ + + _prepTargets: function(){ + var self = this, + transformCSS = { + _in: self._getPrefixedCSS('transform', self.effects.transformIn), + _out: self._getPrefixedCSS('transform', self.effects.transformOut) + }; + + self._execAction('_prepTargets', 0); + + if(self.animation.animateResizeContainer){ + self._$parent.css('height',self._startHeight+'px'); + } + + for(var i = 0; i < self._$toShow.length; i++){ + var el = self._$toShow[i], + $el = $(el); + + el.style.opacity = self.effects.opacity; + el.style.display = (self._changingLayout && self.animation.animateChangeLayout) ? + self._newDisplay : + self.layout.display; + + $el.css(transformCSS._in); + + if(self.animation.animateResizeTargets){ + el.style.width = el.dataset.finalWidth+'px'; + el.style.height = el.dataset.finalHeight+'px'; + el.style.marginRight = -(el.dataset.finalWidth - el.dataset.interWidth) + (el.dataset.finalMarginRight * 1)+'px'; + el.style.marginBottom = -(el.dataset.finalHeight - el.dataset.interHeight) + (el.dataset.finalMarginBottom * 1)+'px'; + } + } + + for(var i = 0; i < self._$pre.length; i++){ + var el = self._$pre[i], + $el = $(el), + translate = { + x: el.dataset.origPosX - el.dataset.interPosX, + y: el.dataset.origPosY - el.dataset.interPosY + }, + transformCSS = self._getPrefixedCSS('transform','translate('+translate.x+'px,'+translate.y+'px)'); + + $el.css(transformCSS); + + if(self.animation.animateResizeTargets){ + el.style.width = el.dataset.origWidth+'px'; + el.style.height = el.dataset.origHeight+'px'; + + if(el.dataset.origWidth - el.dataset.finalWidth){ + el.style.marginRight = -(el.dataset.origWidth - el.dataset.interWidth) + (el.dataset.origMarginRight * 1)+'px'; + } + + if(el.dataset.origHeight - el.dataset.finalHeight){ + el.style.marginBottom = -(el.dataset.origHeight - el.dataset.interHeight) + (el.dataset.origMarginBottom * 1) +'px'; + } + } + } + + self._execAction('_prepTargets', 1); + }, + + /** + * Animate Targets + * @since 2.0.0 + */ + + _animateTargets: function(){ + var self = this; + + self._execAction('_animateTargets', 0); + + self._targetsDone = 0; + self._targetsBound = 0; + + self._$parent + .css(self._getPrefixedCSS('perspective', self.animation.perspectiveDistance+'px')) + .css(self._getPrefixedCSS('perspective-origin', self.animation.perspectiveOrigin)); + + if(self.animation.animateResizeContainer){ + self._$parent + .css(self._getPrefixedCSS('transition','height '+self.animation.duration+'ms ease')) + .css('height',self._newHeight+'px'); + } + + for(var i = 0; i < self._$toShow.length; i++){ + var el = self._$toShow[i], + $el = $(el), + translate = { + x: el.dataset.finalPosX - el.dataset.interPosX, + y: el.dataset.finalPosY - el.dataset.interPosY + }, + delay = self._getDelay(i), + toShowCSS = {}; + + el.style.opacity = ''; + + for(var j = 0; j < 2; j++){ + var a = j === 0 ? a = self._prefix : ''; + + if(self._ff && self._ff <= 20){ + toShowCSS[a+'transition-property'] = 'all'; + toShowCSS[a+'transition-timing-function'] = self.animation.easing+'ms'; + toShowCSS[a+'transition-duration'] = self.animation.duration+'ms'; + } + + toShowCSS[a+'transition-delay'] = delay+'ms'; + toShowCSS[a+'transform'] = 'translate('+translate.x+'px,'+translate.y+'px)'; + } + + if(self.effects.transform || self.effects.opacity){ + self._bindTargetDone($el); + } + + (self._ff && self._ff <= 20) ? + $el.css(toShowCSS) : + $el.css(self.effects.transition).css(toShowCSS); + } + + for(var i = 0; i < self._$pre.length; i++){ + var el = self._$pre[i], + $el = $(el), + translate = { + x: el.dataset.finalPosX - el.dataset.interPosX, + y: el.dataset.finalPosY - el.dataset.interPosY + }, + delay = self._getDelay(i); + + if(!( + el.dataset.finalPosX === el.dataset.origPosX && + el.dataset.finalPosY === el.dataset.origPosY + )){ + self._bindTargetDone($el); + } + + $el.css(self._getPrefixedCSS('transition', 'all '+self.animation.duration+'ms '+self.animation.easing+' '+delay+'ms')); + $el.css(self._getPrefixedCSS('transform', 'translate('+translate.x+'px,'+translate.y+'px)')); + + if(self.animation.animateResizeTargets){ + if(el.dataset.origWidth - el.dataset.finalWidth && el.dataset.finalWidth * 1){ + el.style.width = el.dataset.finalWidth+'px'; + el.style.marginRight = -(el.dataset.finalWidth - el.dataset.interWidth)+(el.dataset.finalMarginRight * 1)+'px'; + } + + if(el.dataset.origHeight - el.dataset.finalHeight && el.dataset.finalHeight * 1){ + el.style.height = el.dataset.finalHeight+'px'; + el.style.marginBottom = -(el.dataset.finalHeight - el.dataset.interHeight)+(el.dataset.finalMarginBottom * 1) +'px'; + } + } + } + + if(self._changingClass){ + self._$container + .removeClass(self.layout.containerClass) + .addClass(self._newClass); + } + + for(var i = 0; i < self._$toHide.length; i++){ + var el = self._$toHide[i], + $el = $(el), + delay = self._getDelay(i), + toHideCSS = {}; + + for(var j = 0; j<2; j++){ + var a = j === 0 ? a = self._prefix : ''; + + toHideCSS[a+'transition-delay'] = delay+'ms'; + toHideCSS[a+'transform'] = self.effects.transformOut; + toHideCSS.opacity = self.effects.opacity; + } + + $el.css(self.effects.transition).css(toHideCSS); + + if(self.effects.transform || self.effects.opacity){ + self._bindTargetDone($el); + }; + } + + self._execAction('_animateTargets', 1); + + }, + + /** + * Bind Targets TransitionEnd + * @since 2.0.0 + * @param {object} $el + */ + + _bindTargetDone: function($el){ + var self = this, + el = $el[0]; + + self._execAction('_bindTargetDone', 0, arguments); + + if(!el.dataset.bound){ + + el.dataset.bound = true; + self._targetsBound++; + + $el.on('webkitTransitionEnd.mixItUp transitionend.mixItUp',function(e){ + if( + (e.originalEvent.propertyName.indexOf('transform') > -1 || + e.originalEvent.propertyName.indexOf('opacity') > -1) && + $(e.originalEvent.target).is(self.selectors.target) + ){ + $el.off('.mixItUp'); + el.dataset.bound = ''; + self._targetDone(); + } + }); + } + + self._execAction('_bindTargetDone', 1, arguments); + }, + + /** + * Target Done + * @since 2.0.0 + */ + + _targetDone: function(){ + var self = this; + + self._execAction('_targetDone', 0); + + self._targetsDone++; + + (self._targetsDone === self._targetsBound) && self._cleanUp(); + + self._execAction('_targetDone', 1); + }, + + /** + * Clean Up + * @since 2.0.0 + */ + + _cleanUp: function(){ + var self = this, + targetStyles = self.animation.animateResizeTargets ? + 'transform opacity width height margin-bottom margin-right' : + 'transform opacity', + unBrake = function(){ + self._$targets.removeStyle('transition', self._prefix); + }; + + self._execAction('_cleanUp', 0); + + !self._changingLayout ? + self._$show.css('display',self.layout.display) : + self._$show.css('display',self._newDisplay); + + self._$targets.css(self._brake); + + self._$targets + .removeStyle(targetStyles, self._prefix) + .removeAttr('data-inter-pos-x data-inter-pos-y data-final-pos-x data-final-pos-y data-orig-pos-x data-orig-pos-y data-orig-height data-orig-width data-final-height data-final-width data-inter-width data-inter-height data-orig-margin-right data-orig-margin-bottom data-inter-margin-right data-inter-margin-bottom data-final-margin-right data-final-margin-bottom'); + + self._$hide.removeStyle('display'); + + self._$parent.removeStyle('height transition perspective-distance perspective perspective-origin-x perspective-origin-y perspective-origin perspectiveOrigin', self._prefix); + + if(self._sorting){ + self._printSort(); + self._activeSort = self._newSortString; + self._sorting = false; + } + + if(self._changingLayout){ + if(self._changingDisplay){ + self.layout.display = self._newDisplay; + self._changingDisplay = false; + } + + if(self._changingClass){ + self._$parent.removeClass(self.layout.containerClass).addClass(self._newClass); + self.layout.containerClass = self._newClass; + self._changingClass = false; + } + + self._changingLayout = false; + } + + self._refresh(); + + self._buildState(); + + if(self._state.fail){ + self._$container.addClass(self.layout.containerClassFail); + } + + self._$show = $(); + self._$hide = $(); + + if(window.requestAnimationFrame){ + requestAnimationFrame(unBrake); + } + + self._mixing = false; + + if(typeof self.callbacks._user === 'function'){ + self.callbacks._user.call(self._domNode, self._state, self); + } + + if(typeof self.callbacks.onMixEnd === 'function'){ + self.callbacks.onMixEnd.call(self._domNode, self._state, self); + } + + self._$container.trigger('mixEnd', [self._state, self]); + + if(self._state.fail){ + (typeof self.callbacks.onMixFail === 'function') && self.callbacks.onMixFail.call(self._domNode, self._state, self); + self._$container.trigger('mixFail', [self._state, self]); + } + + if(self._loading){ + (typeof self.callbacks.onMixLoad === 'function') && self.callbacks.onMixLoad.call(self._domNode, self._state, self); + self._$container.trigger('mixLoad', [self._state, self]); + } + + if(self._queue.length){ + self._execAction('_queue', 0); + + self.multiMix(self._queue[0][0],self._queue[0][1],self._queue[0][2]); + self._queue.splice(0, 1); + } + + self._execAction('_cleanUp', 1); + + self._loading = false; + }, + + /** + * Get Prefixed CSS + * @since 2.0.0 + * @param {string} property + * @param {string} value + * @param {boolean} prefixValue + * @return {object} styles + */ + + _getPrefixedCSS: function(property, value, prefixValue){ + var self = this, + styles = {}, + prefix = '', + i = -1; + + for(i = 0; i < 2; i++){ + prefix = i === 0 ? self._prefix : ''; + prefixValue ? styles[prefix+property] = prefix+value : styles[prefix+property] = value; + } + + return self._execFilter('_getPrefixedCSS', styles, arguments); + }, + + /** + * Get Delay + * @since 2.0.0 + * @param {number} i + * @return {number} delay + */ + + _getDelay: function(i){ + var self = this, + n = typeof self.animation.staggerSequence === 'function' ? self.animation.staggerSequence.call(self._domNode, i, self._state) : i, + delay = self.animation.stagger ? n * self.animation.staggerDuration : 0; + + return self._execFilter('_getDelay', delay, arguments); + }, + + /** + * Parse MultiMix Arguments + * @since 2.0.0 + * @param {array} args + * @return {object} output + */ + + _parseMultiMixArgs: function(args){ + var self = this, + output = { + command: null, + animate: self.animation.enable, + callback: null + }; + + for(var i = 0; i < args.length; i++){ + var arg = args[i]; + + if(arg !== null){ + if(typeof arg === 'object' || typeof arg === 'string'){ + output.command = arg; + } else if(typeof arg === 'boolean'){ + output.animate = arg; + } else if(typeof arg === 'function'){ + output.callback = arg; + } + } + } + + return self._execFilter('_parseMultiMixArgs', output, arguments); + }, + + /** + * Parse Insert Arguments + * @since 2.0.0 + * @param {array} args + * @return {object} output + */ + + _parseInsertArgs: function(args){ + var self = this, + output = { + index: 0, + $object: $(), + multiMix: {filter: self._state.activeFilter}, + callback: null + }; + + for(var i = 0; i < args.length; i++){ + var arg = args[i]; + + if(typeof arg === 'number'){ + output.index = arg; + } else if(typeof arg === 'object' && arg instanceof $){ + output.$object = arg; + } else if(typeof arg === 'object' && self._helpers._isElement(arg)){ + output.$object = $(arg); + } else if(typeof arg === 'object' && arg !== null){ + output.multiMix = arg; + } else if(typeof arg === 'boolean' && !arg){ + output.multiMix = false; + } else if(typeof arg === 'function'){ + output.callback = arg; + } + } + + return self._execFilter('_parseInsertArgs', output, arguments); + }, + + /** + * Execute Action + * @since 2.0.0 + * @param {string} methodName + * @param {boolean} isPost + * @param {array} args + */ + + _execAction: function(methodName, isPost, args){ + var self = this, + context = isPost ? 'post' : 'pre'; + + if(!self._actions.isEmptyObject && self._actions.hasOwnProperty(methodName)){ + for(var key in self._actions[methodName][context]){ + self._actions[methodName][context][key].call(self, args); + } + } + }, + + /** + * Execute Filter + * @since 2.0.0 + * @param {string} methodName + * @param {mixed} value + * @return {mixed} value + */ + + _execFilter: function(methodName, value, args){ + var self = this; + + if(!self._filters.isEmptyObject && self._filters.hasOwnProperty(methodName)){ + for(var key in self._filters[methodName]){ + return self._filters[methodName][key].call(self, args); + } + } else { + return value; + } + }, + + /* Helpers + ---------------------------------------------------------------------- */ + + _helpers: { + + /** + * CamelCase + * @since 2.0.0 + * @param {string} + * @return {string} + */ + + _camelCase: function(string){ + return string.replace(/-([a-z])/g, function(g){ + return g[1].toUpperCase(); + }); + }, + + /** + * Is Element + * @since 2.1.3 + * @param {object} element to test + * @return {boolean} + */ + + _isElement: function(el){ + if(window.HTMLElement){ + return el instanceof HTMLElement; + } else { + return ( + el !== null && + el.nodeType === 1 && + el.nodeName === 'string' + ); + } + } + }, + + /* Public Methods + ---------------------------------------------------------------------- */ + + /** + * Is Mixing + * @since 2.0.0 + * @return {boolean} + */ + + isMixing: function(){ + var self = this; + + return self._execFilter('isMixing', self._mixing); + }, + + /** + * Filter (public) + * @since 2.0.0 + * @param {array} arguments + */ + + filter: function(){ + var self = this, + args = self._parseMultiMixArgs(arguments); + + self._clicking && (self._toggleString = ''); + + self.multiMix({filter: args.command}, args.animate, args.callback); + }, + + /** + * Sort (public) + * @since 2.0.0 + * @param {array} arguments + */ + + sort: function(){ + var self = this, + args = self._parseMultiMixArgs(arguments); + + self.multiMix({sort: args.command}, args.animate, args.callback); + }, + + /** + * Change Layout (public) + * @since 2.0.0 + * @param {array} arguments + */ + + changeLayout: function(){ + var self = this, + args = self._parseMultiMixArgs(arguments); + + self.multiMix({changeLayout: args.command}, args.animate, args.callback); + }, + + /** + * MultiMix + * @since 2.0.0 + * @param {array} arguments + */ + + multiMix: function(){ + var self = this, + args = self._parseMultiMixArgs(arguments); + + self._execAction('multiMix', 0, arguments); + + if(!self._mixing){ + if(self.controls.enable && !self._clicking){ + self.controls.toggleFilterButtons && self._buildToggleArray(); + self._updateControls(args.command, self.controls.toggleFilterButtons); + } + + (self._queue.length < 2) && (self._clicking = false); + + delete self.callbacks._user; + if(args.callback) self.callbacks._user = args.callback; + + var sort = args.command.sort, + filter = args.command.filter, + changeLayout = args.command.changeLayout; + + self._refresh(); + + if(sort){ + self._newSort = self._parseSort(sort); + self._newSortString = sort; + + self._sorting = true; + self._sort(); + } + + if(filter !== undf){ + filter = (filter === 'all') ? self.selectors.target : filter; + + self._activeFilter = filter; + } + + self._filter(); + + if(changeLayout){ + self._newDisplay = (typeof changeLayout === 'string') ? changeLayout : changeLayout.display || self.layout.display; + self._newClass = changeLayout.containerClass || ''; + + if( + self._newDisplay !== self.layout.display || + self._newClass !== self.layout.containerClass + ){ + self._changingLayout = true; + + self._changingClass = (self._newClass !== self.layout.containerClass); + self._changingDisplay = (self._newDisplay !== self.layout.display); + } + } + + self._$targets.css(self._brake); + + self._goMix(args.animate ^ self.animation.enable ? args.animate : self.animation.enable); + + self._execAction('multiMix', 1, arguments); + + } else { + if(self.animation.queue && self._queue.length < self.animation.queueLimit){ + self._queue.push(arguments); + + (self.controls.enable && !self._clicking) && self._updateControls(args.command); + + self._execAction('multiMixQueue', 1, arguments); + + } else { + if(typeof self.callbacks.onMixBusy === 'function'){ + self.callbacks.onMixBusy.call(self._domNode, self._state, self); + } + self._$container.trigger('mixBusy', [self._state, self]); + + self._execAction('multiMixBusy', 1, arguments); + } + } + }, + + /** + * Insert + * @since 2.0.0 + * @param {array} arguments + */ + + insert: function(){ + var self = this, + args = self._parseInsertArgs(arguments), + callback = (typeof args.callback === 'function') ? args.callback : null, + frag = document.createDocumentFragment(), + target = (function(){ + self._refresh(); + + if(self._$targets.length){ + return (args.index < self._$targets.length || !self._$targets.length) ? + self._$targets[args.index] : + self._$targets[self._$targets.length-1].nextElementSibling; + } else { + return self._$parent[0].children[0]; + } + })(); + + self._execAction('insert', 0, arguments); + + if(args.$object){ + for(var i = 0; i < args.$object.length; i++){ + var el = args.$object[i]; + + frag.appendChild(el); + frag.appendChild(document.createTextNode(' ')); + } + + self._$parent[0].insertBefore(frag, target); + } + + self._execAction('insert', 1, arguments); + + if(typeof args.multiMix === 'object'){ + self.multiMix(args.multiMix, callback); + } + }, + + /** + * Prepend + * @since 2.0.0 + * @param {array} arguments + */ + + prepend: function(){ + var self = this, + args = self._parseInsertArgs(arguments); + + self.insert(0, args.$object, args.multiMix, args.callback); + }, + + /** + * Append + * @since 2.0.0 + * @param {array} arguments + */ + + append: function(){ + var self = this, + args = self._parseInsertArgs(arguments); + + self.insert(self._state.totalTargets, args.$object, args.multiMix, args.callback); + }, + + /** + * Get Option + * @since 2.0.0 + * @param {string} string + * @return {mixed} value + */ + + getOption: function(string){ + var self = this, + getProperty = function(obj, prop){ + var parts = prop.split('.'), + last = parts.pop(), + l = parts.length, + i = 1, + current = parts[0] || prop; + + while((obj = obj[current]) && i < l){ + current = parts[i]; + i++; + } + + if(obj !== undf){ + return obj[last] !== undf ? obj[last] : obj; + } + }; + + return string ? self._execFilter('getOption', getProperty(self, string), arguments) : self; + }, + + /** + * Set Options + * @since 2.0.0 + * @param {object} config + */ + + setOptions: function(config){ + var self = this; + + self._execAction('setOptions', 0, arguments); + + typeof config === 'object' && $.extend(true, self, config); + + self._execAction('setOptions', 1, arguments); + }, + + /** + * Get State + * @since 2.0.0 + * @return {object} state + */ + + getState: function(){ + var self = this; + + return self._execFilter('getState', self._state, self); + }, + + /** + * Force Refresh + * @since 2.1.2 + */ + + forceRefresh: function(){ + var self = this; + + self._refresh(false, true); + }, + + /** + * Destroy + * @since 2.0.0 + * @param {boolean} hideAll + */ + + destroy: function(hideAll){ + var self = this, + filters = $.MixItUp.prototype._bound._filter, + sorts = $.MixItUp.prototype._bound._sort; + + self._execAction('destroy', 0, arguments); + + self._$body + .add($(self.selectors.sort)) + .add($(self.selectors.filter)) + .off('.mixItUp'); + + for(var i = 0; i < self._$targets.length; i++){ + var target = self._$targets[i]; + + hideAll && (target.style.display = ''); + + delete target.mixParent; + } + + self._execAction('destroy', 1, arguments); + + if(filters[self.selectors.filter] && filters[self.selectors.filter] > 1) { + filters[self.selectors.filter]--; + } else if(filters[self.selectors.filter] === 1) { + delete filters[self.selectors.filter]; + } + + if(sorts[self.selectors.sort] && sorts[self.selectors.sort] > 1) { + sorts[self.selectors.sort]--; + } else if(sorts[self.selectors.sort] === 1) { + delete sorts[self.selectors.sort]; + } + + delete $.MixItUp.prototype._instances[self._id]; + } + + }; + + /* jQuery Methods + ---------------------------------------------------------------------- */ + + /** + * jQuery .mixItUp() method + * @since 2.0.0 + * @extends $.fn + */ + + $.fn.mixItUp = function(){ + var args = arguments, + dataReturn = [], + eachReturn, + _instantiate = function(domNode, settings){ + var instance = new $.MixItUp(), + rand = function(){ + return ('00000'+(Math.random()*16777216<<0).toString(16)).substr(-6).toUpperCase(); + }; + + instance._execAction('_instantiate', 0, arguments); + + domNode.id = !domNode.id ? 'MixItUp'+rand() : domNode.id; + + if(!instance._instances[domNode.id]){ + instance._instances[domNode.id] = instance; + instance._init(domNode, settings); + } + + instance._execAction('_instantiate', 1, arguments); + }; + + eachReturn = this.each(function(){ + if(args && typeof args[0] === 'string'){ + var instance = $.MixItUp.prototype._instances[this.id]; + if(args[0] === 'isLoaded'){ + dataReturn.push(instance ? true : false); + } else { + var data = instance[args[0]](args[1], args[2], args[3]); + if(data !== undf)dataReturn.push(data); + } + } else { + _instantiate(this, args[0]); + } + }); + + if(dataReturn.length){ + return dataReturn.length > 1 ? dataReturn : dataReturn[0]; + } else { + return eachReturn; + } + }; + + /** + * jQuery .removeStyle() method + * @since 2.0.0 + * @extends $.fn + */ + + $.fn.removeStyle = function(style, prefix){ + prefix = prefix ? prefix : ''; + + return this.each(function(){ + var el = this, + styles = style.split(' '); + + for(var i = 0; i < styles.length; i++){ + for(var j = 0; j < 4; j++){ + switch (j) { + case 0: + var prop = styles[i]; + break; + case 1: + var prop = $.MixItUp.prototype._helpers._camelCase(prop); + break; + case 2: + var prop = prefix+styles[i]; + break; + case 3: + var prop = $.MixItUp.prototype._helpers._camelCase(prefix+styles[i]); + } + + if( + el.style[prop] !== undf && + typeof el.style[prop] !== 'unknown' && + el.style[prop].length > 0 + ){ + el.style[prop] = ''; + } + + if(!prefix && j === 1)break; + } + } + + if(el.attributes && el.attributes.style && el.attributes.style !== undf && el.attributes.style.value === ''){ + el.attributes.removeNamedItem('style'); + } + }); + }; + +})(jQuery); \ No newline at end of file diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 1a387b86c..1d44e0625 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -13,6 +13,7 @@ var tvimer = 0; movieLoad(); tvLoad(); + // Approve all $('#approveAll').click(function () { $.ajax({ @@ -274,6 +275,15 @@ function movieLoad() { var html = searchTemplate(context); $("#movieList").append(html); }); + $('#movieList').mixItUp({ + layout: { + display: 'block' + }, + load: { + filter: 'all' + } + }); + }); }; @@ -286,6 +296,14 @@ function tvLoad() { var html = searchTemplate(context); $("#tvList").append(html); }); + $('#tvList').mixItUp({ + layout: { + display: 'block' + }, + load: { + filter: 'all' + } + }); }); }; diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 69af5770c..33633179e 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -223,6 +223,9 @@ PreserveNewest + + Always + pace.scss diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index 22354440c..1465935f5 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -23,12 +23,37 @@
+ + @if (Model.SearchForMovies) { - + +
-
-
+ +
+
@@ -37,10 +62,11 @@ @if (Model.SearchForTvShows) { - +
-
-
+ +
+
@@ -52,7 +78,7 @@ + From af6deec3f09fcf5217d047876c111fa7247063f8 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Mon, 21 Mar 2016 19:05:01 +0000 Subject: [PATCH 03/34] Got the filter working on both movie and tv #57 --- PlexRequests.UI/Content/custom.css | 3 +++ PlexRequests.UI/Content/custom.min.css | 2 +- PlexRequests.UI/Content/custom.scss | 3 +++ PlexRequests.UI/Content/requests.js | 25 +++++++++++++-------- PlexRequests.UI/Views/Requests/Index.cshtml | 11 ++++----- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/PlexRequests.UI/Content/custom.css b/PlexRequests.UI/Content/custom.css index c571269b4..a517cc508 100644 --- a/PlexRequests.UI/Content/custom.css +++ b/PlexRequests.UI/Content/custom.css @@ -123,3 +123,6 @@ label { #movieList .mix { display: none; } +#tvList .mix { + display: none; } + diff --git a/PlexRequests.UI/Content/custom.min.css b/PlexRequests.UI/Content/custom.min.css index 8385aac7b..45dc19440 100644 --- a/PlexRequests.UI/Content/custom.min.css +++ b/PlexRequests.UI/Content/custom.min.css @@ -1 +1 @@ -@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;} \ No newline at end of file +@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.btn{border-radius:.25rem !important;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;} \ No newline at end of file diff --git a/PlexRequests.UI/Content/custom.scss b/PlexRequests.UI/Content/custom.scss index e1944c04c..11c81ec37 100644 --- a/PlexRequests.UI/Content/custom.scss +++ b/PlexRequests.UI/Content/custom.scss @@ -160,4 +160,7 @@ label { #movieList .mix{ display: none; +} +#tvList .mix{ + display: none; } \ No newline at end of file diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 1d44e0625..68b942bdb 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -14,6 +14,22 @@ movieLoad(); tvLoad(); +$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + var target = $(e.target).attr('href'); + if (target === "#TvShowTab") { + if (!$('#tvList').mixItUp('isLoaded')) { + $('#tvList').mixItUp({ + layout: { + display: 'block' + }, + load: { + filter: 'all' + } + }); + } + } +}); + // Approve all $('#approveAll').click(function () { $.ajax({ @@ -283,7 +299,6 @@ function movieLoad() { filter: 'all' } }); - }); }; @@ -296,14 +311,6 @@ function tvLoad() { var html = searchTemplate(context); $("#tvList").append(html); }); - $('#tvList').mixItUp({ - layout: { - display: 'block' - }, - load: { - filter: 'all' - } - }); }); }; diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index 1465935f5..5c1f38431 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -22,7 +22,7 @@ -
+
Click Here! + } else { @@ -23,6 +23,16 @@
+ @if (Model.UpdateAvailable) + { +

+ @Model.ReleaseTitle +

+
+ + @Html.Raw(Model.ReleaseNotes) + } +
\ No newline at end of file diff --git a/PlexRequests.UI/packages.config b/PlexRequests.UI/packages.config index f1e5e48fc..9794a322a 100644 --- a/PlexRequests.UI/packages.config +++ b/PlexRequests.UI/packages.config @@ -4,6 +4,7 @@ + From 59cf379d56a5ff7da3bcb78ef45228a7c0e4c1a9 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 22 Mar 2016 13:22:47 +0000 Subject: [PATCH 09/34] Fixed an issue where the table didn't match the model. Fixed #67 --- PlexRequests.Core/UserMapper.cs | 8 ++++---- PlexRequests.Store/UserEntity.cs | 3 ++- PlexRequests.Store/UserRepository.cs | 4 ++-- PlexRequests.UI/Content/requests.js | 3 ++- PlexRequests.UI/Content/search.js | 6 ++++-- PlexRequests.UI/Views/Requests/Index.cshtml | 2 +- PlexRequests.UI/Views/Search/Index.cshtml | 2 +- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index f2f397432..c073661b4 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -73,7 +73,7 @@ namespace PlexRequests.Core var passwordMatch = PasswordHasher.VerifyPassword(password, u.Salt, u.Hash); if (passwordMatch) { - return new Guid(u.User); + return new Guid(u.UserGuid); } } } @@ -93,12 +93,12 @@ namespace PlexRequests.Core var repo = new UserRepository(Db); var salt = PasswordHasher.GenerateSalt(); - var userModel = new UsersModel { UserName = username, User = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt)}; + var userModel = new UsersModel { UserName = username, UserGuid = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt)}; repo.Insert(userModel); - var userRecord = repo.Get(userModel.User); + var userRecord = repo.Get(userModel.UserGuid); - return new Guid(userRecord.User); + return new Guid(userRecord.UserGuid); } public static bool UpdateUser(string username, string oldPassword, string newPassword) diff --git a/PlexRequests.Store/UserEntity.cs b/PlexRequests.Store/UserEntity.cs index 343794896..b3d53f8cc 100644 --- a/PlexRequests.Store/UserEntity.cs +++ b/PlexRequests.Store/UserEntity.cs @@ -32,7 +32,8 @@ namespace PlexRequests.Store { [Key] public int Id { get; set; } - public string User { get; set; } + public string UserName { get; set; } + public string UserGuid { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Store/UserRepository.cs b/PlexRequests.Store/UserRepository.cs index 47bea4ce3..3d431908e 100644 --- a/PlexRequests.Store/UserRepository.cs +++ b/PlexRequests.Store/UserRepository.cs @@ -39,7 +39,7 @@ namespace PlexRequests.Store Config = config; } - private ISqliteConfiguration Config { get; set; } + private ISqliteConfiguration Config { get; } public long Insert(T entity) { using (var cnn = Config.DbConnection()) @@ -65,7 +65,7 @@ namespace PlexRequests.Store { db.Open(); var result = db.GetAll(); - var selected = result.FirstOrDefault(x => x.User == id); + var selected = result.FirstOrDefault(x => x.UserGuid == id); return selected; } } diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 205b471c0..1ce08c10b 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -337,7 +337,8 @@ function buildRequestContext(result, type) { issues: result.issues, otherMessage: result.otherMessage, requestId: result.id, - adminNote: result.adminNotes + adminNote: result.adminNotes, + imdb: result.imdbId }; return context; diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index caeaeb0a8..4e765594b 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -139,7 +139,8 @@ function buildMovieContext(result) { voteCount: result.voteCount, voteAverage: result.voteAverage, year: year, - type: "movie" + type: "movie", + imdb: result.imdbId }; return context; @@ -154,7 +155,8 @@ function buildTvShowContext(result) { title: result.seriesName, overview: result.overview, year: year, - type: "tv" + type: "tv", + imdb: result.imdbId }; return context; } diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index 5c1f38431..2b6653f0a 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -94,7 +94,7 @@
- +

{{title}} ({{year}})

{{status}} diff --git a/PlexRequests.UI/Views/Search/Index.cshtml b/PlexRequests.UI/Views/Search/Index.cshtml index 309435839..7e41c6b4d 100644 --- a/PlexRequests.UI/Views/Search/Index.cshtml +++ b/PlexRequests.UI/Views/Search/Index.cshtml @@ -76,7 +76,7 @@
From f1987ecedb960d150b6912582b5a263cf82b08f2 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 22 Mar 2016 14:16:46 +0000 Subject: [PATCH 10/34] Adding the imdb when requesting --- PlexRequests.UI/Modules/SearchModule.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 92a7d1b36..7d1d0dcab 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -182,6 +182,7 @@ namespace PlexRequests.UI.Modules var movieInfo = movieApi.GetMovieInformation(movieId).Result; Log.Trace("Getting movie info from TheMovieDb"); Log.Trace(movieInfo.DumpJson); +//#if !DEBUG try { if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) @@ -193,6 +194,7 @@ namespace PlexRequests.UI.Modules { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {movieInfo.Title} is in Plex, are you sure it's correctly setup?" }); } +//#endif var model = new RequestedModel { @@ -267,6 +269,7 @@ namespace PlexRequests.UI.Modules var tvApi = new TvMazeApi(); var showInfo = tvApi.ShowLookupByTheTvDbId(showId); +//#if !DEBUG try { if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4))) // Take only the year Format = 2014-01-01 @@ -278,6 +281,7 @@ namespace PlexRequests.UI.Modules { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {showInfo.name} is in Plex, are you sure it's correctly setup?" }); } +//#endif DateTime firstAir; DateTime.TryParse(showInfo.premiered, out firstAir); @@ -295,7 +299,8 @@ namespace PlexRequests.UI.Modules Approved = false, RequestedBy = Session[SessionKeys.UsernameKey].ToString(), Issues = IssueState.None, - LatestTv = latest + LatestTv = latest, + ImdbId = showInfo.externals?.imdb ?? string.Empty }; From 5b90fa9089a5743110710c317cead6154d59fe7b Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 22 Mar 2016 15:36:21 +0000 Subject: [PATCH 11/34] Lowercase logs folder, because you know, linux. #59 --- PlexRequests.UI/NLog.config | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/PlexRequests.UI/NLog.config b/PlexRequests.UI/NLog.config index 4fb8be6cb..4810bdc88 100644 --- a/PlexRequests.UI/NLog.config +++ b/PlexRequests.UI/NLog.config @@ -7,23 +7,27 @@ internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log" > - - - - - - - + + + + \ No newline at end of file From 0585ff73ecd88b0eb1fd43d812e58b5f40a18592 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 22 Mar 2016 17:13:14 +0000 Subject: [PATCH 12/34] Added a notification model to the notifiers. Added the backend work for sending a notification for an issue report #75 --- .../NotificationServiceTests.cs | 10 +- .../Notification/EmailMessageNotification.cs | 113 +++++++++++++----- .../Notification/INotification.cs | 5 +- .../Notification/NotificationModel.cs | 39 ++++++ .../Notification/NotificationService.cs | 4 +- .../Notification/NotificationType.cs | 37 ++++++ .../Notification/PushbulletNotification.cs | 68 +++++++++-- .../PlexRequests.Services.csproj | 2 + PlexRequests.UI/Modules/SearchModule.cs | 15 ++- PlexRequests.UI/NLog.config | 15 +-- PlexRequests.UI/Program.cs | 3 +- 11 files changed, 245 insertions(+), 66 deletions(-) create mode 100644 PlexRequests.Services/Notification/NotificationModel.cs create mode 100644 PlexRequests.Services/Notification/NotificationType.cs diff --git a/PlexRequests.Services.Tests/NotificationServiceTests.cs b/PlexRequests.Services.Tests/NotificationServiceTests.cs index ffbd75bfe..877ddcbbf 100644 --- a/PlexRequests.Services.Tests/NotificationServiceTests.cs +++ b/PlexRequests.Services.Tests/NotificationServiceTests.cs @@ -97,7 +97,7 @@ namespace PlexRequests.Services.Tests { Assert.DoesNotThrow( () => - { NotificationService.Publish(string.Empty, string.Empty); }); + { NotificationService.Publish(new NotificationModel()); }); } [Test] @@ -112,11 +112,11 @@ namespace PlexRequests.Services.Tests NotificationService.Subscribe(notificationMock2.Object); Assert.That(NotificationService.Observers.Count, Is.EqualTo(2)); + var model = new NotificationModel {Title = "abc", Body = "test"}; + NotificationService.Publish(model); - NotificationService.Publish("a","b"); - - notificationMock1.Verify(x => x.Notify("a","b"), Times.Once); - notificationMock2.Verify(x => x.Notify("a","b"), Times.Once); + notificationMock1.Verify(x => x.Notify(model), Times.Once); + notificationMock2.Verify(x => x.Notify(model), Times.Once); } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index 2c5c84158..43d567cd8 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -42,10 +42,12 @@ namespace PlexRequests.Services.Notification EmailNotificationSettings = settings; } - private static Logger Log = LogManager.GetCurrentClassLogger(); + private static readonly Logger Log = LogManager.GetCurrentClassLogger(); private ISettingsService EmailNotificationSettings { get; } + private EmailNotificationSettings Settings => GetConfiguration(); public string NotificationName => "EmailMessageNotification"; - public bool Notify(string title, string requester) + + public bool Notify(NotificationModel model) { var configuration = GetConfiguration(); if (!ValidateConfiguration(configuration)) @@ -53,33 +55,22 @@ namespace PlexRequests.Services.Notification return false; } - var message = new MailMessage + switch (model.NotificationType) { - IsBodyHtml = true, - To = { new MailAddress(configuration.RecipientEmail) }, - Body = $"User {requester} has requested {title}!", - From = new MailAddress(configuration.EmailUsername), - Subject = $"New Request for {title}!" - }; + case NotificationType.NewRequest: + return EmailNewRequest(model); + case NotificationType.Issue: + return EmailIssue(model); + case NotificationType.RequestAvailable: + break; + case NotificationType.RequestApproved: + break; + case NotificationType.AdminNote: + break; + default: + throw new ArgumentOutOfRangeException(); + } - try - { - using (var smtp = new SmtpClient(configuration.EmailHost, configuration.EmailPort)) - { - smtp.Credentials = new NetworkCredential(configuration.EmailUsername, configuration.EmailPassword); - smtp.EnableSsl = configuration.Ssl; - smtp.Send(message); - return true; - } - } - catch (SmtpException smtp) - { - Log.Fatal(smtp); - } - catch (Exception e) - { - Log.Fatal(e); - } return false; } @@ -95,14 +86,76 @@ namespace PlexRequests.Services.Notification { return false; } - if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.EmailUsername) - || string.IsNullOrEmpty(settings.EmailPassword) || string.IsNullOrEmpty(settings.RecipientEmail) - || string.IsNullOrEmpty(settings.EmailPort.ToString())) + if (string.IsNullOrEmpty(settings.EmailHost) || string.IsNullOrEmpty(settings.EmailUsername) || string.IsNullOrEmpty(settings.EmailPassword) || string.IsNullOrEmpty(settings.RecipientEmail) || string.IsNullOrEmpty(settings.EmailPort.ToString())) { return false; } return true; } + + private bool EmailNewRequest(NotificationModel model) + { + var message = new MailMessage + { + IsBodyHtml = true, + To = { new MailAddress(Settings.RecipientEmail) }, + Body = $"Hello! The user '{model.User}' has requested {model.Title}! Please log in to approve this request. Request Date: {model.DateTime.ToString("f")}", + From = new MailAddress(Settings.EmailUsername), + Subject = $"Plex Requests: New request for {model.Title}!" + }; + + try + { + using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort)) + { + smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); + smtp.EnableSsl = Settings.Ssl; + smtp.Send(message); + return true; + } + } + catch (SmtpException smtp) + { + Log.Fatal(smtp); + } + catch (Exception e) + { + Log.Fatal(e); + } + return false; + } + + private bool EmailIssue(NotificationModel model) + { + var message = new MailMessage + { + IsBodyHtml = true, + To = { new MailAddress(Settings.RecipientEmail) }, + Body = $"Hello! The user '{model.User}' has reported a new issue {model.Body} for the title {model.Title}!", + From = new MailAddress(Settings.EmailUsername), + Subject = $"Plex Requests: New issue for {model.Title}!" + }; + + try + { + using (var smtp = new SmtpClient(Settings.EmailHost, Settings.EmailPort)) + { + smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); + smtp.EnableSsl = Settings.Ssl; + smtp.Send(message); + return true; + } + } + catch (SmtpException smtp) + { + Log.Fatal(smtp); + } + catch (Exception e) + { + Log.Fatal(e); + } + return false; + } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/INotification.cs b/PlexRequests.Services/Notification/INotification.cs index 6b85bd178..dd099747d 100644 --- a/PlexRequests.Services/Notification/INotification.cs +++ b/PlexRequests.Services/Notification/INotification.cs @@ -38,9 +38,8 @@ namespace PlexRequests.Services.Notification /// /// Notifies the specified title. /// - /// The title. - /// The requester. + /// The model. /// - bool Notify(string title, string requester); + bool Notify(NotificationModel model); } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationModel.cs b/PlexRequests.Services/Notification/NotificationModel.cs new file mode 100644 index 000000000..264d3e609 --- /dev/null +++ b/PlexRequests.Services/Notification/NotificationModel.cs @@ -0,0 +1,39 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: NotificationModel.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; + +namespace PlexRequests.Services.Notification +{ + public class NotificationModel + { + public string Title { get; set; } + public string Body { get; set; } + public DateTime DateTime { get; set; } + public NotificationType NotificationType { get; set; } + public string User { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationService.cs b/PlexRequests.Services/Notification/NotificationService.cs index 81968c28d..817dcd2d9 100644 --- a/PlexRequests.Services/Notification/NotificationService.cs +++ b/PlexRequests.Services/Notification/NotificationService.cs @@ -44,7 +44,7 @@ namespace PlexRequests.Services.Notification Observers = new Dictionary(); } - public static void Publish(string title, string requester) + public static void Publish(NotificationModel model) { Log.Trace("Notifying all observers: "); Log.Trace(Observers.DumpJson()); @@ -55,7 +55,7 @@ namespace PlexRequests.Services.Notification new Thread(() => { Thread.CurrentThread.IsBackground = true; - notification.Notify(title, requester); + notification.Notify(model); }).Start(); } } diff --git a/PlexRequests.Services/Notification/NotificationType.cs b/PlexRequests.Services/Notification/NotificationType.cs new file mode 100644 index 000000000..bf919fe39 --- /dev/null +++ b/PlexRequests.Services/Notification/NotificationType.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: NotificationType.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Services.Notification +{ + public enum NotificationType + { + NewRequest, + Issue, + RequestAvailable, + RequestApproved, + AdminNote, + } +} diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index 5000919f7..be6af1ef2 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -39,27 +39,59 @@ namespace PlexRequests.Services.Notification public PushbulletNotification(IPushbulletApi pushbulletApi, ISettingsService settings) { PushbulletApi = pushbulletApi; - Settings = settings; + SettingsService = settings; } private IPushbulletApi PushbulletApi { get; } - private ISettingsService Settings { get; } + private ISettingsService SettingsService { get; } + private PushbulletNotificationSettings Settings => GetSettings(); private static Logger Log = LogManager.GetCurrentClassLogger(); public string NotificationName => "PushbulletNotification"; - public bool Notify(string title, string requester) + public bool Notify(NotificationModel model) { - var settings = GetSettings(); - - if (!settings.Enabled) + if (!ValidateConfiguration()) { return false; } - var message = $"{title} has been requested by {requester}"; - var pushTitle = $"Plex Requests: {title}"; + switch (model.NotificationType) + { + case NotificationType.NewRequest: + return PushNewRequest(model); + + case NotificationType.Issue: + return PushIssue(model); + + case NotificationType.RequestAvailable: + break; + case NotificationType.RequestApproved: + break; + case NotificationType.AdminNote: + break; + default: + throw new ArgumentOutOfRangeException(); + } + return false; + + } + + private bool ValidateConfiguration() + { + return !Settings.Enabled && !string.IsNullOrEmpty(Settings.AccessToken); + } + + private PushbulletNotificationSettings GetSettings() + { + return SettingsService.GetSettings(); + } + + private bool PushNewRequest(NotificationModel model) + { + var message = $"{model.Title} has been requested by user: {model.User}"; + var pushTitle = $"Plex Requests: {model.Title} has been requested!"; try { - var result = PushbulletApi.Push(settings.AccessToken, pushTitle, message, settings.DeviceIdentifier); + var result = PushbulletApi.Push(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); if (result != null) { return true; @@ -72,9 +104,23 @@ namespace PlexRequests.Services.Notification return false; } - private PushbulletNotificationSettings GetSettings() + private bool PushIssue(NotificationModel model) { - return Settings.GetSettings(); + var message = $"A new issue: {model.Title} has been reported by user: {model.User} for the title: {model.Body}"; + var pushTitle = $"Plex Requests: A new issue has been reported for {model.Body}"; + try + { + var result = PushbulletApi.Push(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); + if (result != null) + { + return true; + } + } + catch (Exception e) + { + Log.Fatal(e); + } + return false; } } } \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 29ad390d5..0317cf36b 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -79,7 +79,9 @@ + + diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 7d1d0dcab..5fce72960 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -182,7 +182,7 @@ namespace PlexRequests.UI.Modules var movieInfo = movieApi.GetMovieInformation(movieId).Result; Log.Trace("Getting movie info from TheMovieDb"); Log.Trace(movieInfo.DumpJson); -//#if !DEBUG + //#if !DEBUG try { if (CheckIfTitleExistsInPlex(movieInfo.Title, movieInfo.ReleaseDate?.Year.ToString())) @@ -194,7 +194,7 @@ namespace PlexRequests.UI.Modules { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {movieInfo.Title} is in Plex, are you sure it's correctly setup?" }); } -//#endif + //#endif var model = new RequestedModel { @@ -241,7 +241,8 @@ namespace PlexRequests.UI.Modules Log.Debug("Adding movie to database requests"); var id = RequestService.AddRequest(model); - NotificationService.Publish(model.Title, model.RequestedBy); + var notificationModel = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; + NotificationService.Publish(notificationModel); return Response.AsJson(new JsonResponseModel { Result = true }); } @@ -269,7 +270,7 @@ namespace PlexRequests.UI.Modules var tvApi = new TvMazeApi(); var showInfo = tvApi.ShowLookupByTheTvDbId(showId); -//#if !DEBUG + //#if !DEBUG try { if (CheckIfTitleExistsInPlex(showInfo.name, showInfo.premiered?.Substring(0, 4))) // Take only the year Format = 2014-01-01 @@ -281,7 +282,7 @@ namespace PlexRequests.UI.Modules { return Response.AsJson(new JsonResponseModel { Result = false, Message = $"We could not check if {showInfo.name} is in Plex, are you sure it's correctly setup?" }); } -//#endif + //#endif DateTime firstAir; DateTime.TryParse(showInfo.premiered, out firstAir); @@ -344,7 +345,9 @@ namespace PlexRequests.UI.Modules } RequestService.AddRequest(model); - NotificationService.Publish(model.Title, model.RequestedBy); + + var notificationModel = new NotificationModel { Title = model.Title, User = model.RequestedBy, DateTime = DateTime.Now, NotificationType = NotificationType.NewRequest }; + NotificationService.Publish(notificationModel); return Response.AsJson(new { Result = true }); } diff --git a/PlexRequests.UI/NLog.config b/PlexRequests.UI/NLog.config index 4810bdc88..39605a1f7 100644 --- a/PlexRequests.UI/NLog.config +++ b/PlexRequests.UI/NLog.config @@ -13,21 +13,22 @@ layout="${date} ${logger} ${level}: ${message}" /> - + + - + \ No newline at end of file diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index a16a5c393..80f0adebb 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -61,7 +61,6 @@ namespace PlexRequests.UI } port = portResult; } - Log.Trace("Getting product version"); WriteOutVersion(); @@ -126,7 +125,7 @@ namespace PlexRequests.UI { CommandType = CommandType.Text, ConnectionString = connectionString, - DBProvider = "Mono.Data.Sqlite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756", + DBProvider = "Mono.Data.Sqlite.SqliteConnection, Mono.Data.Sqlite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756", Name = "database" }; From c9d2b8fc133ac0296c25f6b1ec9599d99cc1c77c Mon Sep 17 00:00:00 2001 From: Shannon Barrett Date: Tue, 22 Mar 2016 12:32:48 -0500 Subject: [PATCH 13/34] Fixes issue #62 --- PlexRequests.Api.Interfaces/ISickRageApi.cs | 4 ++- PlexRequests.Api.Interfaces/ISonarrApi.cs | 2 +- PlexRequests.Api/Mocks/MockSonarrApi.cs | 2 +- PlexRequests.Api/SickrageApi.cs | 33 +++++++++++++++++++-- PlexRequests.Api/SonarrApi.cs | 15 +++++++++- PlexRequests.Store/RequestedModel.cs | 1 + PlexRequests.UI/Content/search.js | 9 ++++-- PlexRequests.UI/Helpers/TvSender.cs | 4 +-- PlexRequests.UI/Modules/SearchModule.cs | 15 ++++++---- PlexRequests.UI/Views/Search/Index.cshtml | 5 ++-- 10 files changed, 73 insertions(+), 17 deletions(-) diff --git a/PlexRequests.Api.Interfaces/ISickRageApi.cs b/PlexRequests.Api.Interfaces/ISickRageApi.cs index 516b8784f..038caa26e 100644 --- a/PlexRequests.Api.Interfaces/ISickRageApi.cs +++ b/PlexRequests.Api.Interfaces/ISickRageApi.cs @@ -32,9 +32,11 @@ namespace PlexRequests.Api.Interfaces { public interface ISickRageApi { - SickRageTvAdd AddSeries(int tvdbId, bool latest, string quality, string apiKey, + SickRageTvAdd AddSeries(int tvdbId, bool latest, int[] seasons, string quality, string apiKey, Uri baseUrl); SickRagePing Ping(string apiKey, Uri baseUrl); + + SickRageTvAdd AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl); } } \ No newline at end of file diff --git a/PlexRequests.Api.Interfaces/ISonarrApi.cs b/PlexRequests.Api.Interfaces/ISonarrApi.cs index d9e7c61db..7939cd21d 100644 --- a/PlexRequests.Api.Interfaces/ISonarrApi.cs +++ b/PlexRequests.Api.Interfaces/ISonarrApi.cs @@ -36,7 +36,7 @@ namespace PlexRequests.Api.Interfaces List GetProfiles(string apiKey, Uri baseUrl); SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, - bool episodes, string apiKey, Uri baseUrl); + bool episodes, int[] seasons, string apiKey, Uri baseUrl); SystemStatus SystemStatus(string apiKey, Uri baseUrl); } diff --git a/PlexRequests.Api/Mocks/MockSonarrApi.cs b/PlexRequests.Api/Mocks/MockSonarrApi.cs index 8508d5a13..1694c3069 100644 --- a/PlexRequests.Api/Mocks/MockSonarrApi.cs +++ b/PlexRequests.Api/Mocks/MockSonarrApi.cs @@ -43,7 +43,7 @@ namespace PlexRequests.Api.Mocks return obj; } - public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, bool episodes, + public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, bool episodes, int[] seasons, string apiKey, Uri baseUrl) { var json = MockApiData.Sonarr_AddSeriesResult; diff --git a/PlexRequests.Api/SickrageApi.cs b/PlexRequests.Api/SickrageApi.cs index f81693f5d..c589f059c 100644 --- a/PlexRequests.Api/SickrageApi.cs +++ b/PlexRequests.Api/SickrageApi.cs @@ -31,6 +31,7 @@ using System; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.SickRage; +using PlexRequests.Helpers; using RestSharp; namespace PlexRequests.Api @@ -47,13 +48,13 @@ namespace PlexRequests.Api private ApiRequest Api { get; } - public SickRageTvAdd AddSeries(int tvdbId, bool latest, string quality, string apiKey, + public SickRageTvAdd AddSeries(int tvdbId, bool latest, int[] seasons, string quality, string apiKey, Uri baseUrl) { string status; var futureStatus = SickRageStatus.Wanted; - status = latest ? SickRageStatus.Skipped : SickRageStatus.Wanted; + status = latest || seasons.Length > 0 ? SickRageStatus.Skipped : SickRageStatus.Wanted; var request = new RestRequest { @@ -71,6 +72,17 @@ namespace PlexRequests.Api var obj = Api.Execute(request, baseUrl); + if (!latest && seasons.Length > 0 && obj.result != "failure") + { + //handle the seasons requested + foreach (int s in seasons) + { + var result = AddSeason(tvdbId, s, apiKey, baseUrl); + Log.Trace("SickRage adding season results: "); + Log.Trace(result.DumpJson()); + } + } + return obj; } @@ -87,5 +99,22 @@ namespace PlexRequests.Api return obj; } + + public SickRageTvAdd AddSeason(int tvdbId, int season, string apiKey, Uri baseUrl) + { + var request = new RestRequest + { + Resource = "/api/{apiKey}/?cmd=episode.setstatus", + Method = Method.GET + }; + request.AddUrlSegment("apiKey", apiKey); + request.AddQueryParameter("tvdbid", tvdbId.ToString()); + request.AddQueryParameter("season", season.ToString()); + request.AddQueryParameter("status", SickRageStatus.Wanted); + + var obj = Api.Execute(request, baseUrl); + + return obj; + } } } \ No newline at end of file diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index 148f27d23..895c696ec 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -54,7 +54,7 @@ namespace PlexRequests.Api return obj; } - public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, bool episodes, string apiKey, Uri baseUrl) + public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, bool episodes, int[] seasons, string apiKey, Uri baseUrl) { var request = new RestRequest @@ -90,6 +90,19 @@ namespace PlexRequests.Api options.seasons = new List(); options.rootFolderPath = rootPath; + if (seasons.Length > 0) + { + foreach (int s in seasons) + { + var season = new Season + { + seasonNumber = s, + monitored = true + }; + options.seasons.Add(season); + } + } + request.AddHeader("X-Api-Key", apiKey); request.AddJsonBody(options); diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index 247433329..aaed042b1 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -25,6 +25,7 @@ namespace PlexRequests.Store public string OtherMessage { get; set; } public bool LatestTv { get; set; } public string AdminNote { get; set; } + public int[] SeasonList { get; set; } } public enum RequestType diff --git a/PlexRequests.UI/Content/search.js b/PlexRequests.UI/Content/search.js index 4e765594b..14168b5ce 100644 --- a/PlexRequests.UI/Content/search.js +++ b/PlexRequests.UI/Content/search.js @@ -39,9 +39,14 @@ $(document).on("click", ".dropdownTv", function (e) { var $form = $('#form' + buttonId); var data = $form.serialize(); var seasons = $(this).attr("season-select"); - if (seasons === "1") { + if (seasons === "2") { // Send over the latest - data = data + "&latest=true"; + data = data + "&seasons=latest"; + } + if (seasons === "1") { + // Send over the first season + data = data + "&seasons=first"; + } var type = $form.prop('method'); diff --git a/PlexRequests.UI/Helpers/TvSender.cs b/PlexRequests.UI/Helpers/TvSender.cs index 9e63749fc..f43012d36 100644 --- a/PlexRequests.UI/Helpers/TvSender.cs +++ b/PlexRequests.UI/Helpers/TvSender.cs @@ -54,7 +54,7 @@ namespace PlexRequests.UI.Helpers int qualityProfile; int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, - sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.LatestTv, sonarrSettings.ApiKey, + sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.LatestTv, model.SeasonList, sonarrSettings.ApiKey, sonarrSettings.FullUri); Log.Trace("Sonarr Add Result: "); @@ -65,7 +65,7 @@ namespace PlexRequests.UI.Helpers public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) { - var result = SickrageApi.AddSeries(model.ProviderId, model.LatestTv, sickRageSettings.QualityProfile, + var result = SickrageApi.AddSeries(model.ProviderId, model.LatestTv, model.SeasonList, sickRageSettings.QualityProfile, sickRageSettings.ApiKey, sickRageSettings.FullUri); Log.Trace("SickRage Add Result: "); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 7d1d0dcab..a9a5fd446 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -77,7 +77,7 @@ namespace PlexRequests.UI.Modules Get["movie/playing"] = parameters => CurrentlyPlayingMovies(); Post["request/movie"] = parameters => RequestMovie((int)Request.Form.movieId); - Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (bool)Request.Form.latest); + Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (string)Request.Form.seasons); } private TheMovieDbApi MovieApi { get; } private ICouchPotatoApi CouchPotatoApi { get; } @@ -259,7 +259,7 @@ namespace PlexRequests.UI.Modules /// The show identifier. /// if set to true [latest]. /// - private Response RequestTvShow(int showId, bool latest) + private Response RequestTvShow(int showId, string seasons) { if (RequestService.CheckRequest(showId)) { @@ -285,7 +285,7 @@ namespace PlexRequests.UI.Modules DateTime firstAir; DateTime.TryParse(showInfo.premiered, out firstAir); - + var latest = seasons == "latest"; var model = new RequestedModel { ProviderId = showInfo.externals?.thetvdb ?? 0, @@ -302,7 +302,12 @@ namespace PlexRequests.UI.Modules LatestTv = latest, ImdbId = showInfo.externals?.imdb ?? string.Empty }; - + var seasonsList = new List(); + if (seasons == "first") + { + seasonsList.Add(1); + } + model.SeasonList = seasonsList.ToArray(); var settings = PrService.GetSettings(); if (!settings.RequireApproval) @@ -357,7 +362,7 @@ namespace PlexRequests.UI.Modules private Response SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) { - var result = SickrageApi.AddSeries(model.ProviderId, model.LatestTv, sickRageSettings.QualityProfile, + var result = SickrageApi.AddSeries(model.ProviderId, model.LatestTv, model.SeasonList, sickRageSettings.QualityProfile, sickRageSettings.ApiKey, sickRageSettings.FullUri); Log.Trace("SickRage Result: "); diff --git a/PlexRequests.UI/Views/Search/Index.cshtml b/PlexRequests.UI/Views/Search/Index.cshtml index 7e41c6b4d..553ac32d7 100644 --- a/PlexRequests.UI/Views/Search/Index.cshtml +++ b/PlexRequests.UI/Views/Search/Index.cshtml @@ -84,7 +84,7 @@
- + {{#if_eq type "movie"}} {{/if_eq}} @@ -96,7 +96,8 @@
{{/if_eq}} From 55addbee0ac994e057d387c79691acd4714f2422 Mon Sep 17 00:00:00 2001 From: Shannon Barrett Date: Tue, 22 Mar 2016 14:15:14 -0500 Subject: [PATCH 14/34] Working on getting the Sonarr component to work correctly. --- PlexRequests.Api/SonarrApi.cs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index 895c696ec..8735060e4 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -64,23 +64,26 @@ namespace PlexRequests.Api }; var options = new SonarrAddSeries(); - if (episodes) + if (seasons.Length == 0) { - options.addOptions = new AddOptions + if (episodes) { - ignoreEpisodesWithFiles = true, - ignoreEpisodesWithoutFiles = true, - searchForMissingEpisodes = false - }; - } - else - { - options.addOptions = new AddOptions + options.addOptions = new AddOptions + { + ignoreEpisodesWithFiles = true, + ignoreEpisodesWithoutFiles = true, + searchForMissingEpisodes = false + }; + } + else { - ignoreEpisodesWithFiles = false, - searchForMissingEpisodes = true, - ignoreEpisodesWithoutFiles = false - }; + options.addOptions = new AddOptions + { + ignoreEpisodesWithFiles = false, + searchForMissingEpisodes = true, + ignoreEpisodesWithoutFiles = false + }; + } } options.seasonFolder = seasonFolders; options.title = title; From f64ccd73272f44cee9fb2ac08f5a9f47cefdb4b9 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 22 Mar 2016 21:31:52 +0000 Subject: [PATCH 15/34] Resolved #75 --- .../Notification/PushbulletNotification.cs | 14 +++++++++++--- PlexRequests.UI/Modules/RequestsModule.cs | 13 +++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index be6af1ef2..20c4b9f35 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -77,7 +77,15 @@ namespace PlexRequests.Services.Notification private bool ValidateConfiguration() { - return !Settings.Enabled && !string.IsNullOrEmpty(Settings.AccessToken); + if (!Settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(Settings.AccessToken)) + { + return false; + } + return true; } private PushbulletNotificationSettings GetSettings() @@ -106,8 +114,8 @@ namespace PlexRequests.Services.Notification private bool PushIssue(NotificationModel model) { - var message = $"A new issue: {model.Title} has been reported by user: {model.User} for the title: {model.Body}"; - var pushTitle = $"Plex Requests: A new issue has been reported for {model.Body}"; + var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; + var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}"; try { var result = PushbulletApi.Push(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index 4f534e0b9..228a0d752 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion +using System; using System.Collections.Generic; using System.Linq; @@ -37,6 +38,7 @@ using Nancy.Security; using PlexRequests.Api; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Notification; using PlexRequests.Store; using PlexRequests.UI.Models; @@ -166,6 +168,17 @@ namespace PlexRequests.UI.Modules var result = Service.UpdateRequest(originalRequest); + + var model = new NotificationModel + { + User = Session[SessionKeys.UsernameKey].ToString(), + NotificationType = NotificationType.Issue, + Title = originalRequest.Title, + DateTime = DateTime.Now, + Body = issue == IssueState.Other ? comment : issue.Humanize() + }; + NotificationService.Publish(model); + return Response.AsJson(result ? new JsonResponseModel { Result = true } : new JsonResponseModel { Result = false, Message = "Could not add issue, please try again or contact the administrator!" }); From 2d5612a045a62dca1670da45e51aabf7623bf5ec Mon Sep 17 00:00:00 2001 From: tidusjar Date: Tue, 22 Mar 2016 22:17:06 +0000 Subject: [PATCH 16/34] Finished adding pushover support. #44 --- PlexRequests.Api.Interfaces/IPushoverApi.cs | 36 +++++ .../PlexRequests.Api.Interfaces.csproj | 1 + .../Notifications/PushoverResponse.cs | 34 +++++ .../PlexRequests.Api.Models.csproj | 1 + PlexRequests.Api/PlexRequests.Api.csproj | 1 + PlexRequests.Api/PushoverApi.cs | 56 ++++++++ PlexRequests.Core/PlexRequests.Core.csproj | 1 + .../PushoverNotificationSettings.cs | 9 ++ .../Notification/PushoverNotification.cs | 132 ++++++++++++++++++ .../PlexRequests.Services.csproj | 1 + PlexRequests.UI.Tests/AdminModuleTests.cs | 6 + PlexRequests.UI/Bootstrapper.cs | 11 +- PlexRequests.UI/Modules/AdminModule.cs | 45 +++++- PlexRequests.UI/PlexRequests.UI.csproj | 4 + .../Validators/PushoverSettingsValidator.cs | 41 ++++++ .../Views/Admin/PushoverNotifications.cshtml | 74 ++++++++++ PlexRequests.UI/Views/Admin/_Sidebar.cshtml | 9 ++ 17 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 PlexRequests.Api.Interfaces/IPushoverApi.cs create mode 100644 PlexRequests.Api.Models/Notifications/PushoverResponse.cs create mode 100644 PlexRequests.Api/PushoverApi.cs create mode 100644 PlexRequests.Core/SettingModels/PushoverNotificationSettings.cs create mode 100644 PlexRequests.Services/Notification/PushoverNotification.cs create mode 100644 PlexRequests.UI/Validators/PushoverSettingsValidator.cs create mode 100644 PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml diff --git a/PlexRequests.Api.Interfaces/IPushoverApi.cs b/PlexRequests.Api.Interfaces/IPushoverApi.cs new file mode 100644 index 000000000..42e3f2217 --- /dev/null +++ b/PlexRequests.Api.Interfaces/IPushoverApi.cs @@ -0,0 +1,36 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: IPushoverApi.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using PlexRequests.Api.Models.Notifications; + +namespace PlexRequests.Api.Interfaces +{ + public interface IPushoverApi + { + PushoverResponse Push(string accessToken, string message, string userToken); + } +} \ No newline at end of file diff --git a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj index 3ea7be621..7522c8156 100644 --- a/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj +++ b/PlexRequests.Api.Interfaces/PlexRequests.Api.Interfaces.csproj @@ -49,6 +49,7 @@ + diff --git a/PlexRequests.Api.Models/Notifications/PushoverResponse.cs b/PlexRequests.Api.Models/Notifications/PushoverResponse.cs new file mode 100644 index 000000000..94849fba1 --- /dev/null +++ b/PlexRequests.Api.Models/Notifications/PushoverResponse.cs @@ -0,0 +1,34 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PushoverResponse.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Api.Models.Notifications +{ + public class PushoverResponse + { + public int status { get; set; } + public string request { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj index bd3151c55..8a4da4222 100644 --- a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj +++ b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj @@ -50,6 +50,7 @@ + diff --git a/PlexRequests.Api/PlexRequests.Api.csproj b/PlexRequests.Api/PlexRequests.Api.csproj index 2f204c975..6422dfd6f 100644 --- a/PlexRequests.Api/PlexRequests.Api.csproj +++ b/PlexRequests.Api/PlexRequests.Api.csproj @@ -72,6 +72,7 @@ MockApiData.resx + diff --git a/PlexRequests.Api/PushoverApi.cs b/PlexRequests.Api/PushoverApi.cs new file mode 100644 index 000000000..1de5694aa --- /dev/null +++ b/PlexRequests.Api/PushoverApi.cs @@ -0,0 +1,56 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PlexApi.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; +using Nancy.Helpers; +using PlexRequests.Api.Interfaces; +using PlexRequests.Api.Models.Notifications; + +using RestSharp; + +namespace PlexRequests.Api +{ + public class PushoverApi : IPushoverApi + { + public PushoverResponse Push(string accessToken, string message, string userToken) + { + var request = new RestRequest + { + Method = Method.POST, + Resource = "messages.json?token={token}&user={user}&message={message}" + }; + + request.AddUrlSegment("token", accessToken); + request.AddUrlSegment("message", message); + request.AddUrlSegment("user", userToken); + + + var api = new ApiRequest(); + return api.ExecuteJson(request, new Uri("https://api.pushover.net/1")); + } + } +} + diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index e1c4ee1f3..9b698ab96 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -73,6 +73,7 @@ + diff --git a/PlexRequests.Core/SettingModels/PushoverNotificationSettings.cs b/PlexRequests.Core/SettingModels/PushoverNotificationSettings.cs new file mode 100644 index 000000000..ac6c4c435 --- /dev/null +++ b/PlexRequests.Core/SettingModels/PushoverNotificationSettings.cs @@ -0,0 +1,9 @@ +namespace PlexRequests.Core.SettingModels +{ + public class PushoverNotificationSettings : Settings + { + public bool Enabled { get; set; } + public string AccessToken { get; set; } + public string UserToken { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Notification/PushoverNotification.cs b/PlexRequests.Services/Notification/PushoverNotification.cs new file mode 100644 index 000000000..2c3bf6bfe --- /dev/null +++ b/PlexRequests.Services/Notification/PushoverNotification.cs @@ -0,0 +1,132 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: PushbulletNotification.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System; + +using NLog; + +using PlexRequests.Api.Interfaces; +using PlexRequests.Core; +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.Services.Notification +{ + public class PushoverNotification : INotification + { + public PushoverNotification(IPushoverApi pushoverApi, ISettingsService settings) + { + PushoverApi = pushoverApi; + SettingsService = settings; + } + private IPushoverApi PushoverApi { get; } + private ISettingsService SettingsService { get; } + private PushoverNotificationSettings Settings => GetSettings(); + + private static Logger Log = LogManager.GetCurrentClassLogger(); + public string NotificationName => "PushoverNotification"; + public bool Notify(NotificationModel model) + { + if (!ValidateConfiguration()) + { + return false; + } + + switch (model.NotificationType) + { + case NotificationType.NewRequest: + return PushNewRequest(model); + + case NotificationType.Issue: + return PushIssue(model); + + case NotificationType.RequestAvailable: + break; + case NotificationType.RequestApproved: + break; + case NotificationType.AdminNote: + break; + default: + throw new ArgumentOutOfRangeException(); + } + return false; + + } + + private bool ValidateConfiguration() + { + if (!Settings.Enabled) + { + return false; + } + if (string.IsNullOrEmpty(Settings.AccessToken) || string.IsNullOrEmpty(Settings.UserToken)) + { + return false; + } + return true; + } + + private PushoverNotificationSettings GetSettings() + { + return SettingsService.GetSettings(); + } + + private bool PushNewRequest(NotificationModel model) + { + var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}"; + try + { + var result = PushoverApi.Push(Settings.AccessToken, message, Settings.UserToken); + if (result?.status == 1) + { + return true; + } + } + catch (Exception e) + { + Log.Fatal(e); + } + return false; + } + + private bool PushIssue(NotificationModel model) + { + var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; + try + { + var result = PushoverApi.Push(Settings.AccessToken, message, Settings.UserToken); + if (result != null) + { + return true; + } + } + catch (Exception e) + { + Log.Fatal(e); + } + return false; + } + } +} \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index 0317cf36b..e1ba81532 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -82,6 +82,7 @@ + diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs index e42eb4a13..08c84235b 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -55,9 +55,11 @@ namespace PlexRequests.UI.Tests private Mock> SickRageSettingsMock { get; set; } private Mock> EmailMock { get; set; } private Mock> PushbulletSettings { get; set; } + private Mock> PushoverSettings { get; set; } private Mock PlexMock { get; set; } private Mock SonarrApiMock { get; set; } private Mock PushbulletApi { get; set; } + private Mock PushoverApi { get; set; } private Mock CpApi { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; } @@ -83,6 +85,8 @@ namespace PlexRequests.UI.Tests PushbulletSettings = new Mock>(); CpApi = new Mock(); SickRageSettingsMock = new Mock>(); + PushoverSettings = new Mock>(); + PushoverApi = new Mock(); Bootstrapper = new ConfigurableBootstrapper(with => { @@ -99,6 +103,8 @@ namespace PlexRequests.UI.Tests with.Dependency(PushbulletSettings.Object); with.Dependency(CpApi.Object); with.Dependency(SickRageSettingsMock.Object); + with.Dependency(PushoverSettings.Object); + with.Dependency(PushoverApi.Object); with.RootPathProvider(); with.RequestStartup((container, pipelines, context) => { diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 8fd5aad2b..9c07266e9 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -74,6 +74,7 @@ namespace PlexRequests.UI container.Register, SettingsServiceV2>(); container.Register, SettingsServiceV2>(); container.Register, SettingsServiceV2>(); + container.Register, SettingsServiceV2>(); // Repo's container.Register, GenericRepository>(); @@ -88,6 +89,7 @@ namespace PlexRequests.UI // Api's container.Register(); container.Register(); + container.Register(); container.Register(); container.Register(); container.Register(); @@ -137,7 +139,14 @@ namespace PlexRequests.UI var pushbulletSettings = pushbulletService.GetSettings(); if (pushbulletSettings.Enabled) { - NotificationService.Subscribe(new PushbulletNotification(container.Resolve(), container.Resolve>())); + NotificationService.Subscribe(new PushbulletNotification(container.Resolve(), pushbulletService)); + } + + var pushoverService = container.Resolve>(); + var pushoverSettings = pushoverService.GetSettings(); + if (pushoverSettings.Enabled) + { + NotificationService.Subscribe(new PushoverNotification(container.Resolve(), pushoverService)); } } } diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 7876aa41c..d97d06129 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -59,9 +59,11 @@ namespace PlexRequests.UI.Modules private ISettingsService SickRageService { get; } private ISettingsService EmailService { get; } private ISettingsService PushbulletService { get; } + private ISettingsService PushoverService { get; } private IPlexApi PlexApi { get; } private ISonarrApi SonarrApi { get; } - private PushbulletApi PushbulletApi { get; } + private IPushbulletApi PushbulletApi { get; } + private IPushoverApi PushoverApi { get; } private ICouchPotatoApi CpApi { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); @@ -76,7 +78,9 @@ namespace PlexRequests.UI.Modules IPlexApi plexApi, ISettingsService pbSettings, PushbulletApi pbApi, - ICouchPotatoApi cpApi) : base("admin") + ICouchPotatoApi cpApi, + ISettingsService pushoverSettings, + IPushoverApi pushoverApi) : base("admin") { RpService = rpService; CpService = cpService; @@ -90,6 +94,8 @@ namespace PlexRequests.UI.Modules PushbulletApi = pbApi; CpApi = cpApi; SickRageService = sickrage; + PushoverService = pushoverSettings; + PushoverApi = pushoverApi; #if !DEBUG this.RequiresAuthentication(); @@ -126,6 +132,9 @@ namespace PlexRequests.UI.Modules Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); + + Get["/pushovernotification"] = _ => PushoverNotifications(); + Post["/pushovernotification"] = _ => SavePushoverNotifications(); } private Negotiator Authentication() @@ -415,6 +424,38 @@ namespace PlexRequests.UI.Modules : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } + private Negotiator PushoverNotifications() + { + var settings = PushoverService.GetSettings(); + return View["PushoverNotifications", settings]; + } + + private Response SavePushoverNotifications() + { + var settings = this.Bind(); + var valid = this.Validate(settings); + if (!valid.IsValid) + { + return Response.AsJson(valid.SendJsonError()); + } + Log.Trace(settings.DumpJson()); + + var result = PushoverService.SaveSettings(settings); + if (settings.Enabled) + { + NotificationService.Subscribe(new PushoverNotification(PushoverApi, PushoverService)); + } + else + { + NotificationService.UnSubscribe(new PushoverNotification(PushoverApi, PushoverService)); + } + + Log.Info("Saved email settings, result: {0}", result); + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for Pushbullet Notifications!" } + : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); + } + private Response GetCpProfiles() { var settings = this.Bind(); diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 44ec9805c..a62ab2592 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -166,6 +166,7 @@ + @@ -335,6 +336,9 @@ Always + + Always + web.config diff --git a/PlexRequests.UI/Validators/PushoverSettingsValidator.cs b/PlexRequests.UI/Validators/PushoverSettingsValidator.cs new file mode 100644 index 000000000..55a218aa0 --- /dev/null +++ b/PlexRequests.UI/Validators/PushoverSettingsValidator.cs @@ -0,0 +1,41 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: SonarrValidator.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using FluentValidation; + +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.UI.Validators +{ + public class PushoverSettingsValidator : AbstractValidator + { + public PushoverSettingsValidator() + { + RuleFor(request => request.AccessToken).NotEmpty().WithMessage("You must specify a API Token."); + RuleFor(request => request.UserToken).NotEmpty().WithMessage("You must specify a User Token."); + } + } +} \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml new file mode 100644 index 000000000..0877739d0 --- /dev/null +++ b/PlexRequests.UI/Views/Admin/PushoverNotifications.cshtml @@ -0,0 +1,74 @@ +@Html.Partial("_Sidebar") + +
+ +
+ Pushover Notifications + +
+
+ +
+
+ +
+ + Enter your API Key from Pushover. +
+ +
+
+ +
+ + Your user or group key from Pushover. +
+ +
+
+ +
+
+ +
+
+
+ +
+ + \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index 2877729c7..51e181921 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -71,6 +71,15 @@ Pushbullet Notifications } + @if (Context.Request.Path == "/admin/pushovernotification") + { + Pushover Notifications + } + else + { + Pushover Notifications + } + @if (Context.Request.Path == "/admin/status") { Status From 95fad3c33ca5bc5625e6f2766fd7427618001b9c Mon Sep 17 00:00:00 2001 From: Jamie Date: Wed, 23 Mar 2016 09:49:33 +0000 Subject: [PATCH 17/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f6ac4c89..db7bdba52 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ If you feel like donating you can [here!](https://paypal.me/PlexRequestsNet) ###### A massive thanks to everyone below! -[heartisall](https://github.com/heartisall) +[heartisall](https://github.com/heartisall), Stuke00, [shiitake](https://github.com/shiitake) # Sponsors From c7ac8a7d99cde8f01cc07005e02c4015f5acec4a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Wed, 23 Mar 2016 13:43:27 +0000 Subject: [PATCH 18/34] Done most on #59 --- PlexRequests.Core/JsonRequestService.cs | 1 + PlexRequests.Core/PlexRequests.Core.csproj | 1 - PlexRequests.Core/RequestService.cs | 84 - PlexRequests.Core/SettingsServiceV2.cs | 1 + PlexRequests.Core/Setup.cs | 2 +- PlexRequests.Core/UserMapper.cs | 4 +- PlexRequests.Helpers/LoggingHelper.cs | 57 + .../PlexRequests.Helpers.csproj | 4 + PlexRequests.Helpers/packages.config | 1 + PlexRequests.Store/Models/LogEntity.cs | 4 +- PlexRequests.Store/PlexRequests.Store.csproj | 8 +- .../{ => Repository}/GenericRepository.cs | 37 +- .../{ => Repository}/IRepository.cs | 2 +- .../{ => Repository}/IRequestRepository.cs | 2 +- .../{ => Repository}/ISettingsRepository.cs | 2 +- PlexRequests.Store/UserRepository.cs | 2 + PlexRequests.Store/UsersModel.cs | 1 + PlexRequests.UI.Tests/AdminModuleTests.cs | 5 + .../PlexRequests.UI.Tests.csproj | 4 + PlexRequests.UI/Bootstrapper.cs | 2 + PlexRequests.UI/Content/jquery.mixitup.js | 2086 +---------------- PlexRequests.UI/Modules/AdminModule.cs | 35 +- PlexRequests.UI/Modules/ApprovalModule.cs | 13 +- PlexRequests.UI/Modules/LoginModule.cs | 13 +- PlexRequests.UI/NLog.config | 6 +- PlexRequests.UI/PlexRequests.UI.csproj | 7 + PlexRequests.UI/Program.cs | 88 +- PlexRequests.UI/Views/Admin/Logs.cshtml | 118 + PlexRequests.UI/Views/Admin/_Sidebar.cshtml | 9 +- PlexRequests.UI/Views/Shared/_Layout.cshtml | 24 +- PlexRequests.UI/compilerconfig.json | 4 + PlexRequests.UI/packages.config | 1 + 32 files changed, 345 insertions(+), 2283 deletions(-) delete mode 100644 PlexRequests.Core/RequestService.cs rename PlexRequests.Store/{ => Repository}/GenericRepository.cs (77%) rename PlexRequests.Store/{ => Repository}/IRepository.cs (95%) rename PlexRequests.Store/{ => Repository}/IRequestRepository.cs (95%) rename PlexRequests.Store/{ => Repository}/ISettingsRepository.cs (95%) create mode 100644 PlexRequests.UI/Views/Admin/Logs.cshtml diff --git a/PlexRequests.Core/JsonRequestService.cs b/PlexRequests.Core/JsonRequestService.cs index 1a99569d3..533bc1c56 100644 --- a/PlexRequests.Core/JsonRequestService.cs +++ b/PlexRequests.Core/JsonRequestService.cs @@ -32,6 +32,7 @@ using Newtonsoft.Json; using PlexRequests.Store; using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; namespace PlexRequests.Core { diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index e1c4ee1f3..a7903bd4f 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -81,7 +81,6 @@ - diff --git a/PlexRequests.Core/RequestService.cs b/PlexRequests.Core/RequestService.cs deleted file mode 100644 index 7788b6436..000000000 --- a/PlexRequests.Core/RequestService.cs +++ /dev/null @@ -1,84 +0,0 @@ -#region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: RequestService.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ -#endregion - -using System.Collections.Generic; -using System.Linq; -using PlexRequests.Store; - -namespace PlexRequests.Core -{ - public class RequestService : IRequestService - { - public RequestService(IRepository db) - { - Repo = db; - } - - private IRepository Repo { get; set; } - - public long AddRequest(RequestedModel model) - { - return Repo.Insert(model); - } - - public bool CheckRequest(int providerId) - { - return Repo.GetAll().Any(x => x.ProviderId == providerId); - } - - public void DeleteRequest(RequestedModel model) - { - var entity = Repo.Get(model.Id); - Repo.Delete(entity); - } - - public bool UpdateRequest(RequestedModel model) - { - return Repo.Update(model); - } - - /// - /// Updates all the entities. NOTE: we need to Id to be the original entity - /// - /// The model. - /// - public bool BatchUpdate(List model) - { - return Repo.UpdateAll(model); - } - - public RequestedModel Get(int id) - { - return Repo.Get(id); - } - - public IEnumerable GetAll() - { - return Repo.GetAll(); - } - } -} diff --git a/PlexRequests.Core/SettingsServiceV2.cs b/PlexRequests.Core/SettingsServiceV2.cs index 067b3b44b..3fb53cd35 100644 --- a/PlexRequests.Core/SettingsServiceV2.cs +++ b/PlexRequests.Core/SettingsServiceV2.cs @@ -30,6 +30,7 @@ using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Store; using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; namespace PlexRequests.Core { diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 04ce5b9db..c467dfac3 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -75,7 +75,7 @@ namespace PlexRequests.Core { var result = new List(); RequestedModel[] requestedModels; - var repo = new GenericRepository(Db); + var repo = new GenericRepository(Db, new MemoryCacheProvider()); try { var records = repo.GetAll(); diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index c073661b4..6bacbdca9 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -88,12 +88,12 @@ namespace PlexRequests.Core return users.Any(); } - public static Guid? CreateUser(string username, string password) + public static Guid? CreateUser(string username, string password, string[] claims = default(string[])) { var repo = new UserRepository(Db); var salt = PasswordHasher.GenerateSalt(); - var userModel = new UsersModel { UserName = username, UserGuid = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt)}; + var userModel = new UsersModel { UserName = username, UserGuid = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt), Claims = claims}; repo.Insert(userModel); var userRecord = repo.Get(userModel.UserGuid); diff --git a/PlexRequests.Helpers/LoggingHelper.cs b/PlexRequests.Helpers/LoggingHelper.cs index b3ee3dc1b..5487a90df 100644 --- a/PlexRequests.Helpers/LoggingHelper.cs +++ b/PlexRequests.Helpers/LoggingHelper.cs @@ -25,9 +25,14 @@ // ************************************************************************/ #endregion using System; +using System.Data; using Newtonsoft.Json; +using NLog; +using NLog.Config; +using NLog.Targets; + namespace PlexRequests.Helpers { public static class LoggingHelper @@ -55,5 +60,57 @@ namespace PlexRequests.Helpers } return dumpTarget.ToString(); } + + public static void ConfigureLogging(string connectionString) + { + LogManager.ThrowExceptions = true; + // Step 1. Create configuration object + var config = new LoggingConfiguration(); + + // Step 2. Create targets and add them to the configuration + var databaseTarget = new DatabaseTarget + { + CommandType = CommandType.Text, + ConnectionString = connectionString, + DBProvider = "Mono.Data.Sqlite.SqliteConnection, Mono.Data.Sqlite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756", + Name = "database" + }; + + var messageParam = new DatabaseParameterInfo { Name = "@Message", Layout = "${message}" }; + var callsiteParam = new DatabaseParameterInfo { Name = "@Callsite", Layout = "${callsite}" }; + var levelParam = new DatabaseParameterInfo { Name = "@Level", Layout = "${level}" }; + var dateParam = new DatabaseParameterInfo { Name = "@Date", Layout = "${date}" }; + var loggerParam = new DatabaseParameterInfo { Name = "@Logger", Layout = "${logger}" }; + var exceptionParam = new DatabaseParameterInfo { Name = "@Exception", Layout = "${exception:tostring}" }; + + databaseTarget.Parameters.Add(messageParam); + databaseTarget.Parameters.Add(callsiteParam); + databaseTarget.Parameters.Add(levelParam); + databaseTarget.Parameters.Add(dateParam); + databaseTarget.Parameters.Add(loggerParam); + databaseTarget.Parameters.Add(exceptionParam); + + databaseTarget.CommandText = "INSERT INTO Logs (Date,Level,Logger, Message, Callsite, Exception) VALUES(@Date,@Level,@Logger, @Message, @Callsite, @Exception);"; + config.AddTarget("database", databaseTarget); + + // Step 4. Define rules + var rule1 = new LoggingRule("*", LogLevel.Info, databaseTarget); + config.LoggingRules.Add(rule1); + + // Step 5. Activate the configuration + LogManager.Configuration = config; + } + + public static void ReconfigureLogLevel(LogLevel level) + { + foreach (var rule in LogManager.Configuration.LoggingRules) + { + rule.EnableLoggingForLevel(level); + } + + //Call to update existing Loggers created with GetLogger() or + //GetCurrentClassLogger() + LogManager.ReconfigExistingLoggers(); + } } } diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index beaf7c117..6af45a9b5 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -35,6 +35,10 @@ ..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll True + + ..\packages\NLog.4.2.3\lib\net45\NLog.dll + True + diff --git a/PlexRequests.Helpers/packages.config b/PlexRequests.Helpers/packages.config index 47cebb403..dc63c2a11 100644 --- a/PlexRequests.Helpers/packages.config +++ b/PlexRequests.Helpers/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/PlexRequests.Store/Models/LogEntity.cs b/PlexRequests.Store/Models/LogEntity.cs index 694b6ce8d..e0a275c0a 100644 --- a/PlexRequests.Store/Models/LogEntity.cs +++ b/PlexRequests.Store/Models/LogEntity.cs @@ -26,11 +26,13 @@ #endregion using System; +using Dapper.Contrib.Extensions; + namespace PlexRequests.Store.Models { + [Table("Logs")] public class LogEntity : Entity { - public string Username { get; set; } public DateTime Date { get; set; } public string Level { get; set; } public string Logger { get; set; } diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index d9586aba3..7207fd555 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -58,17 +58,17 @@ - - + + - + - + diff --git a/PlexRequests.Store/GenericRepository.cs b/PlexRequests.Store/Repository/GenericRepository.cs similarity index 77% rename from PlexRequests.Store/GenericRepository.cs rename to PlexRequests.Store/Repository/GenericRepository.cs index 331567463..17e7b9c0f 100644 --- a/PlexRequests.Store/GenericRepository.cs +++ b/PlexRequests.Store/Repository/GenericRepository.cs @@ -34,20 +34,23 @@ using NLog; using PlexRequests.Helpers; -namespace PlexRequests.Store +namespace PlexRequests.Store.Repository { public class GenericRepository : IRepository where T : Entity { - public GenericRepository(ISqliteConfiguration config) + private ICacheProvider Cache { get; } + public GenericRepository(ISqliteConfiguration config, ICacheProvider cache) { Config = config; + Cache = cache; } private static Logger Log = LogManager.GetCurrentClassLogger(); - private ISqliteConfiguration Config { get; set; } + private ISqliteConfiguration Config { get; } public long Insert(T entity) { + ResetCache(); using (var cnn = Config.DbConnection()) { cnn.Open(); @@ -57,12 +60,14 @@ namespace PlexRequests.Store public IEnumerable GetAll() { + using (var db = Config.DbConnection()) { db.Open(); var result = db.GetAll(); return result; } + } public T Get(string id) @@ -72,15 +77,23 @@ namespace PlexRequests.Store public T Get(int id) { - using (var db = Config.DbConnection()) - { - db.Open(); - return db.Get(id); - } + var key = "Get" + id; + var item = Cache.GetOrSet( + key, + () => + { + using (var db = Config.DbConnection()) + { + db.Open(); + return db.Get(id); + } + }); + return item; } public void Delete(T entity) { + ResetCache(); using (var db = Config.DbConnection()) { db.Open(); @@ -90,6 +103,7 @@ namespace PlexRequests.Store public bool Update(T entity) { + ResetCache(); Log.Trace("Updating entity"); Log.Trace(entity.DumpJson()); using (var db = Config.DbConnection()) @@ -101,6 +115,7 @@ namespace PlexRequests.Store public bool UpdateAll(IEnumerable entity) { + ResetCache(); Log.Trace("Updating all entities"); var result = new HashSet(); @@ -114,5 +129,11 @@ namespace PlexRequests.Store } return result.All(x => true); } + + private void ResetCache() + { + Cache.Remove("Get"); + Cache.Remove("GetAll"); + } } } diff --git a/PlexRequests.Store/IRepository.cs b/PlexRequests.Store/Repository/IRepository.cs similarity index 95% rename from PlexRequests.Store/IRepository.cs rename to PlexRequests.Store/Repository/IRepository.cs index d88f80aa0..4d301047c 100644 --- a/PlexRequests.Store/IRepository.cs +++ b/PlexRequests.Store/Repository/IRepository.cs @@ -26,7 +26,7 @@ #endregion using System.Collections.Generic; -namespace PlexRequests.Store +namespace PlexRequests.Store.Repository { public interface IRepository { diff --git a/PlexRequests.Store/IRequestRepository.cs b/PlexRequests.Store/Repository/IRequestRepository.cs similarity index 95% rename from PlexRequests.Store/IRequestRepository.cs rename to PlexRequests.Store/Repository/IRequestRepository.cs index e2a07b6b5..809628c51 100644 --- a/PlexRequests.Store/IRequestRepository.cs +++ b/PlexRequests.Store/Repository/IRequestRepository.cs @@ -28,7 +28,7 @@ using System.Collections.Generic; using PlexRequests.Store.Models; -namespace PlexRequests.Store +namespace PlexRequests.Store.Repository { public interface IRequestRepository { diff --git a/PlexRequests.Store/ISettingsRepository.cs b/PlexRequests.Store/Repository/ISettingsRepository.cs similarity index 95% rename from PlexRequests.Store/ISettingsRepository.cs rename to PlexRequests.Store/Repository/ISettingsRepository.cs index c0a8a866a..393da6ef7 100644 --- a/PlexRequests.Store/ISettingsRepository.cs +++ b/PlexRequests.Store/Repository/ISettingsRepository.cs @@ -28,7 +28,7 @@ using System.Collections.Generic; using PlexRequests.Store.Models; -namespace PlexRequests.Store +namespace PlexRequests.Store.Repository { public interface ISettingsRepository { diff --git a/PlexRequests.Store/UserRepository.cs b/PlexRequests.Store/UserRepository.cs index 3d431908e..467e0487c 100644 --- a/PlexRequests.Store/UserRepository.cs +++ b/PlexRequests.Store/UserRepository.cs @@ -30,6 +30,8 @@ using System.Linq; using Dapper.Contrib.Extensions; +using PlexRequests.Store.Repository; + namespace PlexRequests.Store { public class UserRepository : IRepository where T : UserEntity diff --git a/PlexRequests.Store/UsersModel.cs b/PlexRequests.Store/UsersModel.cs index e0376b7ba..cf5eec764 100644 --- a/PlexRequests.Store/UsersModel.cs +++ b/PlexRequests.Store/UsersModel.cs @@ -33,5 +33,6 @@ namespace PlexRequests.Store { public byte[] Hash { get; set; } public byte[] Salt { get; set; } + public string[] Claims { get; set; } } } diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs index e42eb4a13..d505e5261 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -39,6 +39,8 @@ using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Plex; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; using PlexRequests.UI.Models; using PlexRequests.UI.Modules; @@ -59,6 +61,7 @@ namespace PlexRequests.UI.Tests private Mock SonarrApiMock { get; set; } private Mock PushbulletApi { get; set; } private Mock CpApi { get; set; } + private Mock> LogRepo { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; } @@ -83,6 +86,7 @@ namespace PlexRequests.UI.Tests PushbulletSettings = new Mock>(); CpApi = new Mock(); SickRageSettingsMock = new Mock>(); + LogRepo = new Mock>(); Bootstrapper = new ConfigurableBootstrapper(with => { @@ -99,6 +103,7 @@ namespace PlexRequests.UI.Tests with.Dependency(PushbulletSettings.Object); with.Dependency(CpApi.Object); with.Dependency(SickRageSettingsMock.Object); + with.Dependency(LogRepo.Object); with.RootPathProvider(); with.RequestStartup((container, pipelines, context) => { diff --git a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj index 96823f840..e79b01dc4 100644 --- a/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj +++ b/PlexRequests.UI.Tests/PlexRequests.UI.Tests.csproj @@ -117,6 +117,10 @@ {566EFA49-68F8-4716-9693-A6B3F2624DEA} PlexRequests.Services + + {92433867-2B7B-477B-A566-96C382427525} + PlexRequests.Store + {68F5F5F3-B8BB-4911-875F-6F00AAE04EA6} PlexRequests.UI diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 8fd5aad2b..c47cb40c1 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -47,6 +47,7 @@ using PlexRequests.Services; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store; +using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using PlexRequests.UI.Jobs; using TaskFactory = FluentScheduler.TaskFactory; @@ -77,6 +78,7 @@ namespace PlexRequests.UI // Repo's container.Register, GenericRepository>(); + container.Register, GenericRepository>(); container.Register(); container.Register(); diff --git a/PlexRequests.UI/Content/jquery.mixitup.js b/PlexRequests.UI/Content/jquery.mixitup.js index b23a60279..7acfbe489 100644 --- a/PlexRequests.UI/Content/jquery.mixitup.js +++ b/PlexRequests.UI/Content/jquery.mixitup.js @@ -11,2088 +11,4 @@ * Non-commercial use permitted under terms of CC-BY-NC license. * http://creativecommons.org/licenses/by-nc/3.0/ */ - -(function($, undf){ - 'use strict'; - - /** - * MixItUp Constructor Function - * @constructor - * @extends jQuery - */ - - $.MixItUp = function(){ - var self = this; - - self._execAction('_constructor', 0); - - $.extend(self, { - - /* Public Properties - ---------------------------------------------------------------------- */ - - selectors: { - target: '.mix', - filter: '.filter', - sort: '.sort' - }, - - animation: { - enable: true, - effects: 'fade scale', - duration: 600, - easing: 'ease', - perspectiveDistance: '3000', - perspectiveOrigin: '50% 50%', - queue: true, - queueLimit: 1, - animateChangeLayout: false, - animateResizeContainer: true, - animateResizeTargets: false, - staggerSequence: false, - reverseOut: false - }, - - callbacks: { - onMixLoad: false, - onMixStart: false, - onMixBusy: false, - onMixEnd: false, - onMixFail: false, - _user: false - }, - - controls: { - enable: true, - live: false, - toggleFilterButtons: false, - toggleLogic: 'or', - activeClass: 'active' - }, - - layout: { - display: 'inline-block', - containerClass: '', - containerClassFail: 'fail' - }, - - load: { - filter: 'all', - sort: false - }, - - /* Private Properties - ---------------------------------------------------------------------- */ - - _$body: null, - _$container: null, - _$targets: null, - _$parent: null, - _$sortButtons: null, - _$filterButtons: null, - - _suckMode: false, - _mixing: false, - _sorting: false, - _clicking: false, - _loading: true, - _changingLayout: false, - _changingClass: false, - _changingDisplay: false, - - _origOrder: [], - _startOrder: [], - _newOrder: [], - _activeFilter: null, - _toggleArray: [], - _toggleString: '', - _activeSort: 'default:asc', - _newSort: null, - _startHeight: null, - _newHeight: null, - _incPadding: true, - _newDisplay: null, - _newClass: null, - _targetsBound: 0, - _targetsDone: 0, - _queue: [], - - _$show: $(), - _$hide: $() - }); - - self._execAction('_constructor', 1); - }; - - /** - * MixItUp Prototype - * @override - */ - - $.MixItUp.prototype = { - constructor: $.MixItUp, - - /* Static Properties - ---------------------------------------------------------------------- */ - - _instances: {}, - _handled: { - _filter: {}, - _sort: {} - }, - _bound: { - _filter: {}, - _sort: {} - }, - _actions: {}, - _filters: {}, - - /* Static Methods - ---------------------------------------------------------------------- */ - - /** - * Extend - * @since 2.1.0 - * @param {object} new properties/methods - * @extends {object} prototype - */ - - extend: function(extension){ - for(var key in extension){ - $.MixItUp.prototype[key] = extension[key]; - } - }, - - /** - * Add Action - * @since 2.1.0 - * @param {string} hook name - * @param {string} namespace - * @param {function} function to execute - * @param {number} priority - * @extends {object} $.MixItUp.prototype._actions - */ - - addAction: function(hook, name, func, priority){ - $.MixItUp.prototype._addHook('_actions', hook, name, func, priority); - }, - - /** - * Add Filter - * @since 2.1.0 - * @param {string} hook name - * @param {string} namespace - * @param {function} function to execute - * @param {number} priority - * @extends {object} $.MixItUp.prototype._filters - */ - - addFilter: function(hook, name, func, priority){ - $.MixItUp.prototype._addHook('_filters', hook, name, func, priority); - }, - - /** - * Add Hook - * @since 2.1.0 - * @param {string} type of hook - * @param {string} hook name - * @param {function} function to execute - * @param {number} priority - * @extends {object} $.MixItUp.prototype._filters - */ - - _addHook: function(type, hook, name, func, priority){ - var collection = $.MixItUp.prototype[type], - obj = {}; - - priority = (priority === 1 || priority === 'post') ? 'post' : 'pre'; - - obj[hook] = {}; - obj[hook][priority] = {}; - obj[hook][priority][name] = func; - - $.extend(true, collection, obj); - }, - - - /* Private Methods - ---------------------------------------------------------------------- */ - - /** - * Initialise - * @since 2.0.0 - * @param {object} domNode - * @param {object} config - */ - - _init: function(domNode, config){ - var self = this; - - self._execAction('_init', 0, arguments); - - config && $.extend(true, self, config); - - self._$body = $('body'); - self._domNode = domNode; - self._$container = $(domNode); - self._$container.addClass(self.layout.containerClass); - self._id = domNode.id; - - self._platformDetect(); - - self._brake = self._getPrefixedCSS('transition', 'none'); - - self._refresh(true); - - self._$parent = self._$targets.parent().length ? self._$targets.parent() : self._$container; - - if(self.load.sort){ - self._newSort = self._parseSort(self.load.sort); - self._newSortString = self.load.sort; - self._activeSort = self.load.sort; - self._sort(); - self._printSort(); - } - - self._activeFilter = self.load.filter === 'all' ? - self.selectors.target : - self.load.filter === 'none' ? - '' : - self.load.filter; - - self.controls.enable && self._bindHandlers(); - - if(self.controls.toggleFilterButtons){ - self._buildToggleArray(); - - for(var i = 0; i < self._toggleArray.length; i++){ - self._updateControls({filter: self._toggleArray[i], sort: self._activeSort}, true); - }; - } else if(self.controls.enable){ - self._updateControls({filter: self._activeFilter, sort: self._activeSort}); - } - - self._filter(); - - self._init = true; - - self._$container.data('mixItUp',self); - - self._execAction('_init', 1, arguments); - - self._buildState(); - - self._$targets.css(self._brake); - - self._goMix(self.animation.enable); - }, - - /** - * Platform Detect - * @since 2.0.0 - */ - - _platformDetect: function(){ - var self = this, - vendorsTrans = ['Webkit', 'Moz', 'O', 'ms'], - vendorsRAF = ['webkit', 'moz'], - chrome = window.navigator.appVersion.match(/Chrome\/(\d+)\./) || false, - ff = typeof InstallTrigger !== 'undefined', - prefix = function(el){ - for (var i = 0; i < vendorsTrans.length; i++){ - if (vendorsTrans[i] + 'Transition' in el.style){ - return { - prefix: '-'+vendorsTrans[i].toLowerCase()+'-', - vendor: vendorsTrans[i] - }; - }; - }; - return 'transition' in el.style ? '' : false; - }, - transPrefix = prefix(self._domNode); - - self._execAction('_platformDetect', 0); - - self._chrome = chrome ? parseInt(chrome[1], 10) : false; - self._ff = ff ? parseInt(window.navigator.userAgent.match(/rv:([^)]+)\)/)[1]) : false; - self._prefix = transPrefix.prefix; - self._vendor = transPrefix.vendor; - self._suckMode = window.atob && self._prefix ? false : true; - - self._suckMode && (self.animation.enable = false); - (self._ff && self._ff <= 4) && (self.animation.enable = false); - - /* Polyfills - ---------------------------------------------------------------------- */ - - /** - * window.requestAnimationFrame - */ - - for(var x = 0; x < vendorsRAF.length && !window.requestAnimationFrame; x++){ - window.requestAnimationFrame = window[vendorsRAF[x]+'RequestAnimationFrame']; - } - - /** - * Object.getPrototypeOf - */ - - if(typeof Object.getPrototypeOf !== 'function'){ - if(typeof 'test'.__proto__ === 'object'){ - Object.getPrototypeOf = function(object){ - return object.__proto__; - }; - } else { - Object.getPrototypeOf = function(object){ - return object.constructor.prototype; - }; - } - } - - /** - * Element.nextElementSibling - */ - - if(self._domNode.nextElementSibling === undf){ - Object.defineProperty(Element.prototype, 'nextElementSibling',{ - get: function(){ - var el = this.nextSibling; - - while(el){ - if(el.nodeType ===1){ - return el; - } - el = el.nextSibling; - } - return null; - } - }); - } - - self._execAction('_platformDetect', 1); - }, - - /** - * Refresh - * @since 2.0.0 - * @param {boolean} init - * @param {boolean} force - */ - - _refresh: function(init, force){ - var self = this; - - self._execAction('_refresh', 0, arguments); - - self._$targets = self._$container.find(self.selectors.target); - - for(var i = 0; i < self._$targets.length; i++){ - var target = self._$targets[i]; - - if(target.dataset === undf || force){ - - target.dataset = {}; - - for(var j = 0; j < target.attributes.length; j++){ - - var attr = target.attributes[j], - name = attr.name, - val = attr.value; - - if(name.indexOf('data-') > -1){ - var dataName = self._helpers._camelCase(name.substring(5,name.length)); - target.dataset[dataName] = val; - } - } - } - - if(target.mixParent === undf){ - target.mixParent = self._id; - } - } - - if( - (self._$targets.length && init) || - (!self._origOrder.length && self._$targets.length) - ){ - self._origOrder = []; - - for(var i = 0; i < self._$targets.length; i++){ - var target = self._$targets[i]; - - self._origOrder.push(target); - } - } - - self._execAction('_refresh', 1, arguments); - }, - - /** - * Bind Handlers - * @since 2.0.0 - */ - - _bindHandlers: function(){ - var self = this, - filters = $.MixItUp.prototype._bound._filter, - sorts = $.MixItUp.prototype._bound._sort; - - self._execAction('_bindHandlers', 0); - - if(self.controls.live){ - self._$body - .on('click.mixItUp.'+self._id, self.selectors.sort, function(){ - self._processClick($(this), 'sort'); - }) - .on('click.mixItUp.'+self._id, self.selectors.filter, function(){ - self._processClick($(this), 'filter'); - }); - } else { - self._$sortButtons = $(self.selectors.sort); - self._$filterButtons = $(self.selectors.filter); - - self._$sortButtons.on('click.mixItUp.'+self._id, function(){ - self._processClick($(this), 'sort'); - }); - - self._$filterButtons.on('click.mixItUp.'+self._id, function(){ - self._processClick($(this), 'filter'); - }); - } - - filters[self.selectors.filter] = (filters[self.selectors.filter] === undf) ? 1 : filters[self.selectors.filter] + 1; - sorts[self.selectors.sort] = (sorts[self.selectors.sort] === undf) ? 1 : sorts[self.selectors.sort] + 1; - - self._execAction('_bindHandlers', 1); - }, - - /** - * Process Click - * @since 2.0.0 - * @param {object} $button - * @param {string} type - */ - - _processClick: function($button, type){ - var self = this, - trackClick = function($button, type, off){ - var proto = $.MixItUp.prototype; - - proto._handled['_'+type][self.selectors[type]] = (proto._handled['_'+type][self.selectors[type]] === undf) ? - 1 : - proto._handled['_'+type][self.selectors[type]] + 1; - - if(proto._handled['_'+type][self.selectors[type]] === proto._bound['_'+type][self.selectors[type]]){ - $button[(off ? 'remove' : 'add')+'Class'](self.controls.activeClass); - delete proto._handled['_'+type][self.selectors[type]]; - } - }; - - self._execAction('_processClick', 0, arguments); - - if(!self._mixing || (self.animation.queue && self._queue.length < self.animation.queueLimit)){ - self._clicking = true; - - if(type === 'sort'){ - var sort = $button.attr('data-sort'); - - if(!$button.hasClass(self.controls.activeClass) || sort.indexOf('random') > -1){ - $(self.selectors.sort).removeClass(self.controls.activeClass); - trackClick($button, type); - self.sort(sort); - } - } - - if(type === 'filter') { - var filter = $button.attr('data-filter'), - ndx, - seperator = self.controls.toggleLogic === 'or' ? ',' : ''; - - if(!self.controls.toggleFilterButtons){ - if(!$button.hasClass(self.controls.activeClass)){ - $(self.selectors.filter).removeClass(self.controls.activeClass); - trackClick($button, type); - self.filter(filter); - } - } else { - self._buildToggleArray(); - - if(!$button.hasClass(self.controls.activeClass)){ - trackClick($button, type); - - self._toggleArray.push(filter); - } else { - trackClick($button, type, true); - ndx = self._toggleArray.indexOf(filter); - self._toggleArray.splice(ndx, 1); - } - - self._toggleArray = $.grep(self._toggleArray,function(n){return(n);}); - - self._toggleString = self._toggleArray.join(seperator); - - self.filter(self._toggleString); - } - } - - self._execAction('_processClick', 1, arguments); - } else { - if(typeof self.callbacks.onMixBusy === 'function'){ - self.callbacks.onMixBusy.call(self._domNode, self._state, self); - } - self._execAction('_processClickBusy', 1, arguments); - } - }, - - /** - * Build Toggle Array - * @since 2.0.0 - */ - - _buildToggleArray: function(){ - var self = this, - activeFilter = self._activeFilter.replace(/\s/g, ''); - - self._execAction('_buildToggleArray', 0, arguments); - - if(self.controls.toggleLogic === 'or'){ - self._toggleArray = activeFilter.split(','); - } else { - self._toggleArray = activeFilter.split('.'); - - !self._toggleArray[0] && self._toggleArray.shift(); - - for(var i = 0, filter; filter = self._toggleArray[i]; i++){ - self._toggleArray[i] = '.'+filter; - } - } - - self._execAction('_buildToggleArray', 1, arguments); - }, - - /** - * Update Controls - * @since 2.0.0 - * @param {object} command - * @param {boolean} multi - */ - - _updateControls: function(command, multi){ - var self = this, - output = { - filter: command.filter, - sort: command.sort - }, - update = function($el, filter){ - try { - (multi && type === 'filter' && !(output.filter === 'none' || output.filter === '')) ? - $el.filter(filter).addClass(self.controls.activeClass) : - $el.removeClass(self.controls.activeClass).filter(filter).addClass(self.controls.activeClass); - } catch(e) {} - }, - type = 'filter', - $el = null; - - self._execAction('_updateControls', 0, arguments); - - (command.filter === undf) && (output.filter = self._activeFilter); - (command.sort === undf) && (output.sort = self._activeSort); - (output.filter === self.selectors.target) && (output.filter = 'all'); - - for(var i = 0; i < 2; i++){ - $el = self.controls.live ? $(self.selectors[type]) : self['_$'+type+'Buttons']; - $el && update($el, '[data-'+type+'="'+output[type]+'"]'); - type = 'sort'; - } - - self._execAction('_updateControls', 1, arguments); - }, - - /** - * Filter (private) - * @since 2.0.0 - */ - - _filter: function(){ - var self = this; - - self._execAction('_filter', 0); - - for(var i = 0; i < self._$targets.length; i++){ - var $target = $(self._$targets[i]); - - if($target.is(self._activeFilter)){ - self._$show = self._$show.add($target); - } else { - self._$hide = self._$hide.add($target); - } - } - - self._execAction('_filter', 1); - }, - - /** - * Sort (private) - * @since 2.0.0 - */ - - _sort: function(){ - var self = this, - arrayShuffle = function(oldArray){ - var newArray = oldArray.slice(), - len = newArray.length, - i = len; - - while(i--){ - var p = parseInt(Math.random()*len); - var t = newArray[i]; - newArray[i] = newArray[p]; - newArray[p] = t; - }; - return newArray; - }; - - self._execAction('_sort', 0); - - self._startOrder = []; - - for(var i = 0; i < self._$targets.length; i++){ - var target = self._$targets[i]; - - self._startOrder.push(target); - } - - switch(self._newSort[0].sortBy){ - case 'default': - self._newOrder = self._origOrder; - break; - case 'random': - self._newOrder = arrayShuffle(self._startOrder); - break; - case 'custom': - self._newOrder = self._newSort[0].order; - break; - default: - self._newOrder = self._startOrder.concat().sort(function(a, b){ - return self._compare(a, b); - }); - } - - self._execAction('_sort', 1); - }, - - /** - * Compare Algorithm - * @since 2.0.0 - * @param {string|number} a - * @param {string|number} b - * @param {number} depth (recursion) - * @return {number} - */ - - _compare: function(a, b, depth){ - depth = depth ? depth : 0; - - var self = this, - order = self._newSort[depth].order, - getData = function(el){ - return el.dataset[self._newSort[depth].sortBy] || 0; - }, - attrA = isNaN(getData(a) * 1) ? getData(a).toLowerCase() : getData(a) * 1, - attrB = isNaN(getData(b) * 1) ? getData(b).toLowerCase() : getData(b) * 1; - - if(attrA < attrB) - return order === 'asc' ? -1 : 1; - if(attrA > attrB) - return order === 'asc' ? 1 : -1; - if(attrA === attrB && self._newSort.length > depth+1) - return self._compare(a, b, depth+1); - - return 0; - }, - - /** - * Print Sort - * @since 2.0.0 - * @param {boolean} reset - */ - - _printSort: function(reset){ - var self = this, - order = reset ? self._startOrder : self._newOrder, - targets = self._$parent[0].querySelectorAll(self.selectors.target), - nextSibling = targets.length ? targets[targets.length -1].nextElementSibling : null, - frag = document.createDocumentFragment(); - - self._execAction('_printSort', 0, arguments); - - for(var i = 0; i < targets.length; i++){ - var target = targets[i], - whiteSpace = target.nextSibling; - - if(target.style.position === 'absolute') continue; - - if(whiteSpace && whiteSpace.nodeName === '#text'){ - self._$parent[0].removeChild(whiteSpace); - } - - self._$parent[0].removeChild(target); - } - - for(var i = 0; i < order.length; i++){ - var el = order[i]; - - if(self._newSort[0].sortBy === 'default' && self._newSort[0].order === 'desc' && !reset){ - var firstChild = frag.firstChild; - frag.insertBefore(el, firstChild); - frag.insertBefore(document.createTextNode(' '), el); - } else { - frag.appendChild(el); - frag.appendChild(document.createTextNode(' ')); - } - } - - nextSibling ? - self._$parent[0].insertBefore(frag, nextSibling) : - self._$parent[0].appendChild(frag); - - self._execAction('_printSort', 1, arguments); - }, - - /** - * Parse Sort - * @since 2.0.0 - * @param {string} sortString - * @return {array} newSort - */ - - _parseSort: function(sortString){ - var self = this, - rules = typeof sortString === 'string' ? sortString.split(' ') : [sortString], - newSort = []; - - for(var i = 0; i < rules.length; i++){ - var rule = typeof sortString === 'string' ? rules[i].split(':') : ['custom', rules[i]], - ruleObj = { - sortBy: self._helpers._camelCase(rule[0]), - order: rule[1] || 'asc' - }; - - newSort.push(ruleObj); - - if(ruleObj.sortBy === 'default' || ruleObj.sortBy === 'random') break; - } - - return self._execFilter('_parseSort', newSort, arguments); - }, - - /** - * Parse Effects - * @since 2.0.0 - * @return {object} effects - */ - - _parseEffects: function(){ - var self = this, - effects = { - opacity: '', - transformIn: '', - transformOut: '', - filter: '' - }, - parse = function(effect, extract, reverse){ - if(self.animation.effects.indexOf(effect) > -1){ - if(extract){ - var propIndex = self.animation.effects.indexOf(effect+'('); - if(propIndex > -1){ - var str = self.animation.effects.substring(propIndex), - match = /\(([^)]+)\)/.exec(str), - val = match[1]; - - return {val: val}; - } - } - return true; - } else { - return false; - } - }, - negate = function(value, invert){ - if(invert){ - return value.charAt(0) === '-' ? value.substr(1, value.length) : '-'+value; - } else { - return value; - } - }, - buildTransform = function(key, invert){ - var transforms = [ - ['scale', '.01'], - ['translateX', '20px'], - ['translateY', '20px'], - ['translateZ', '20px'], - ['rotateX', '90deg'], - ['rotateY', '90deg'], - ['rotateZ', '180deg'], - ]; - - for(var i = 0; i < transforms.length; i++){ - var prop = transforms[i][0], - def = transforms[i][1], - inverted = invert && prop !== 'scale'; - - effects[key] += parse(prop) ? prop+'('+negate(parse(prop, true).val || def, inverted)+') ' : ''; - } - }; - - effects.opacity = parse('fade') ? parse('fade',true).val || '0' : '1'; - - buildTransform('transformIn'); - - self.animation.reverseOut ? buildTransform('transformOut', true) : (effects.transformOut = effects.transformIn); - - effects.transition = {}; - - effects.transition = self._getPrefixedCSS('transition','all '+self.animation.duration+'ms '+self.animation.easing+', opacity '+self.animation.duration+'ms linear'); - - self.animation.stagger = parse('stagger') ? true : false; - self.animation.staggerDuration = parseInt(parse('stagger') ? (parse('stagger',true).val ? parse('stagger',true).val : 100) : 100); - - return self._execFilter('_parseEffects', effects); - }, - - /** - * Build State - * @since 2.0.0 - * @param {boolean} future - * @return {object} futureState - */ - - _buildState: function(future){ - var self = this, - state = {}; - - self._execAction('_buildState', 0); - - state = { - activeFilter: self._activeFilter === '' ? 'none' : self._activeFilter, - activeSort: future && self._newSortString ? self._newSortString : self._activeSort, - fail: !self._$show.length && self._activeFilter !== '', - $targets: self._$targets, - $show: self._$show, - $hide: self._$hide, - totalTargets: self._$targets.length, - totalShow: self._$show.length, - totalHide: self._$hide.length, - display: future && self._newDisplay ? self._newDisplay : self.layout.display - }; - - if(future){ - return self._execFilter('_buildState', state); - } else { - self._state = state; - - self._execAction('_buildState', 1); - } - }, - - /** - * Go Mix - * @since 2.0.0 - * @param {boolean} animate - */ - - _goMix: function(animate){ - var self = this, - phase1 = function(){ - if(self._chrome && (self._chrome === 31)){ - chromeFix(self._$parent[0]); - } - - self._setInter(); - - phase2(); - }, - phase2 = function(){ - var scrollTop = window.pageYOffset, - scrollLeft = window.pageXOffset, - docHeight = document.documentElement.scrollHeight; - - self._getInterMixData(); - - self._setFinal(); - - self._getFinalMixData(); - - (window.pageYOffset !== scrollTop) && window.scrollTo(scrollLeft, scrollTop); - - self._prepTargets(); - - if(window.requestAnimationFrame){ - requestAnimationFrame(phase3); - } else { - setTimeout(function(){ - phase3(); - },20); - } - }, - phase3 = function(){ - self._animateTargets(); - - if(self._targetsBound === 0){ - self._cleanUp(); - } - }, - chromeFix = function(grid){ - var parent = grid.parentElement, - placeholder = document.createElement('div'), - frag = document.createDocumentFragment(); - - parent.insertBefore(placeholder, grid); - frag.appendChild(grid); - parent.replaceChild(grid, placeholder); - }, - futureState = self._buildState(true); - - self._execAction('_goMix', 0, arguments); - - !self.animation.duration && (animate = false); - - self._mixing = true; - - self._$container.removeClass(self.layout.containerClassFail); - - if(typeof self.callbacks.onMixStart === 'function'){ - self.callbacks.onMixStart.call(self._domNode, self._state, futureState, self); - } - - self._$container.trigger('mixStart', [self._state, futureState, self]); - - self._getOrigMixData(); - - if(animate && !self._suckMode){ - - window.requestAnimationFrame ? - requestAnimationFrame(phase1) : - phase1(); - - } else { - self._cleanUp(); - } - - self._execAction('_goMix', 1, arguments); - }, - - /** - * Get Target Data - * @since 2.0.0 - */ - - _getTargetData: function(el, stage){ - var self = this, - elStyle; - - el.dataset[stage+'PosX'] = el.offsetLeft; - el.dataset[stage+'PosY'] = el.offsetTop; - - if(self.animation.animateResizeTargets){ - elStyle = !self._suckMode ? - window.getComputedStyle(el) : - { - marginBottom: '', - marginRight: '' - }; - - el.dataset[stage+'MarginBottom'] = parseInt(elStyle.marginBottom); - el.dataset[stage+'MarginRight'] = parseInt(elStyle.marginRight); - el.dataset[stage+'Width'] = el.offsetWidth; - el.dataset[stage+'Height'] = el.offsetHeight; - } - }, - - /** - * Get Original Mix Data - * @since 2.0.0 - */ - - _getOrigMixData: function(){ - var self = this, - parentStyle = !self._suckMode ? window.getComputedStyle(self._$parent[0]) : {boxSizing: ''}, - parentBS = parentStyle.boxSizing || parentStyle[self._vendor+'BoxSizing']; - - self._incPadding = (parentBS === 'border-box'); - - self._execAction('_getOrigMixData', 0); - - !self._suckMode && (self.effects = self._parseEffects()); - - self._$toHide = self._$hide.filter(':visible'); - self._$toShow = self._$show.filter(':hidden'); - self._$pre = self._$targets.filter(':visible'); - - self._startHeight = self._incPadding ? - self._$parent.outerHeight() : - self._$parent.height(); - - for(var i = 0; i < self._$pre.length; i++){ - var el = self._$pre[i]; - - self._getTargetData(el, 'orig'); - } - - self._execAction('_getOrigMixData', 1); - }, - - /** - * Set Intermediate Positions - * @since 2.0.0 - */ - - _setInter: function(){ - var self = this; - - self._execAction('_setInter', 0); - - if(self._changingLayout && self.animation.animateChangeLayout){ - self._$toShow.css('display',self._newDisplay); - - if(self._changingClass){ - self._$container - .removeClass(self.layout.containerClass) - .addClass(self._newClass); - } - } else { - self._$toShow.css('display', self.layout.display); - } - - self._execAction('_setInter', 1); - }, - - /** - * Get Intermediate Mix Data - * @since 2.0.0 - */ - - _getInterMixData: function(){ - var self = this; - - self._execAction('_getInterMixData', 0); - - for(var i = 0; i < self._$toShow.length; i++){ - var el = self._$toShow[i]; - - self._getTargetData(el, 'inter'); - } - - for(var i = 0; i < self._$pre.length; i++){ - var el = self._$pre[i]; - - self._getTargetData(el, 'inter'); - } - - self._execAction('_getInterMixData', 1); - }, - - /** - * Set Final Positions - * @since 2.0.0 - */ - - _setFinal: function(){ - var self = this; - - self._execAction('_setFinal', 0); - - self._sorting && self._printSort(); - - self._$toHide.removeStyle('display'); - - if(self._changingLayout && self.animation.animateChangeLayout){ - self._$pre.css('display',self._newDisplay); - } - - self._execAction('_setFinal', 1); - }, - - /** - * Get Final Mix Data - * @since 2.0.0 - */ - - _getFinalMixData: function(){ - var self = this; - - self._execAction('_getFinalMixData', 0); - - for(var i = 0; i < self._$toShow.length; i++){ - var el = self._$toShow[i]; - - self._getTargetData(el, 'final'); - } - - for(var i = 0; i < self._$pre.length; i++){ - var el = self._$pre[i]; - - self._getTargetData(el, 'final'); - } - - self._newHeight = self._incPadding ? - self._$parent.outerHeight() : - self._$parent.height(); - - self._sorting && self._printSort(true); - - self._$toShow.removeStyle('display'); - - self._$pre.css('display',self.layout.display); - - if(self._changingClass && self.animation.animateChangeLayout){ - self._$container - .removeClass(self._newClass) - .addClass(self.layout.containerClass); - } - - self._execAction('_getFinalMixData', 1); - }, - - /** - * Prepare Targets - * @since 2.0.0 - */ - - _prepTargets: function(){ - var self = this, - transformCSS = { - _in: self._getPrefixedCSS('transform', self.effects.transformIn), - _out: self._getPrefixedCSS('transform', self.effects.transformOut) - }; - - self._execAction('_prepTargets', 0); - - if(self.animation.animateResizeContainer){ - self._$parent.css('height',self._startHeight+'px'); - } - - for(var i = 0; i < self._$toShow.length; i++){ - var el = self._$toShow[i], - $el = $(el); - - el.style.opacity = self.effects.opacity; - el.style.display = (self._changingLayout && self.animation.animateChangeLayout) ? - self._newDisplay : - self.layout.display; - - $el.css(transformCSS._in); - - if(self.animation.animateResizeTargets){ - el.style.width = el.dataset.finalWidth+'px'; - el.style.height = el.dataset.finalHeight+'px'; - el.style.marginRight = -(el.dataset.finalWidth - el.dataset.interWidth) + (el.dataset.finalMarginRight * 1)+'px'; - el.style.marginBottom = -(el.dataset.finalHeight - el.dataset.interHeight) + (el.dataset.finalMarginBottom * 1)+'px'; - } - } - - for(var i = 0; i < self._$pre.length; i++){ - var el = self._$pre[i], - $el = $(el), - translate = { - x: el.dataset.origPosX - el.dataset.interPosX, - y: el.dataset.origPosY - el.dataset.interPosY - }, - transformCSS = self._getPrefixedCSS('transform','translate('+translate.x+'px,'+translate.y+'px)'); - - $el.css(transformCSS); - - if(self.animation.animateResizeTargets){ - el.style.width = el.dataset.origWidth+'px'; - el.style.height = el.dataset.origHeight+'px'; - - if(el.dataset.origWidth - el.dataset.finalWidth){ - el.style.marginRight = -(el.dataset.origWidth - el.dataset.interWidth) + (el.dataset.origMarginRight * 1)+'px'; - } - - if(el.dataset.origHeight - el.dataset.finalHeight){ - el.style.marginBottom = -(el.dataset.origHeight - el.dataset.interHeight) + (el.dataset.origMarginBottom * 1) +'px'; - } - } - } - - self._execAction('_prepTargets', 1); - }, - - /** - * Animate Targets - * @since 2.0.0 - */ - - _animateTargets: function(){ - var self = this; - - self._execAction('_animateTargets', 0); - - self._targetsDone = 0; - self._targetsBound = 0; - - self._$parent - .css(self._getPrefixedCSS('perspective', self.animation.perspectiveDistance+'px')) - .css(self._getPrefixedCSS('perspective-origin', self.animation.perspectiveOrigin)); - - if(self.animation.animateResizeContainer){ - self._$parent - .css(self._getPrefixedCSS('transition','height '+self.animation.duration+'ms ease')) - .css('height',self._newHeight+'px'); - } - - for(var i = 0; i < self._$toShow.length; i++){ - var el = self._$toShow[i], - $el = $(el), - translate = { - x: el.dataset.finalPosX - el.dataset.interPosX, - y: el.dataset.finalPosY - el.dataset.interPosY - }, - delay = self._getDelay(i), - toShowCSS = {}; - - el.style.opacity = ''; - - for(var j = 0; j < 2; j++){ - var a = j === 0 ? a = self._prefix : ''; - - if(self._ff && self._ff <= 20){ - toShowCSS[a+'transition-property'] = 'all'; - toShowCSS[a+'transition-timing-function'] = self.animation.easing+'ms'; - toShowCSS[a+'transition-duration'] = self.animation.duration+'ms'; - } - - toShowCSS[a+'transition-delay'] = delay+'ms'; - toShowCSS[a+'transform'] = 'translate('+translate.x+'px,'+translate.y+'px)'; - } - - if(self.effects.transform || self.effects.opacity){ - self._bindTargetDone($el); - } - - (self._ff && self._ff <= 20) ? - $el.css(toShowCSS) : - $el.css(self.effects.transition).css(toShowCSS); - } - - for(var i = 0; i < self._$pre.length; i++){ - var el = self._$pre[i], - $el = $(el), - translate = { - x: el.dataset.finalPosX - el.dataset.interPosX, - y: el.dataset.finalPosY - el.dataset.interPosY - }, - delay = self._getDelay(i); - - if(!( - el.dataset.finalPosX === el.dataset.origPosX && - el.dataset.finalPosY === el.dataset.origPosY - )){ - self._bindTargetDone($el); - } - - $el.css(self._getPrefixedCSS('transition', 'all '+self.animation.duration+'ms '+self.animation.easing+' '+delay+'ms')); - $el.css(self._getPrefixedCSS('transform', 'translate('+translate.x+'px,'+translate.y+'px)')); - - if(self.animation.animateResizeTargets){ - if(el.dataset.origWidth - el.dataset.finalWidth && el.dataset.finalWidth * 1){ - el.style.width = el.dataset.finalWidth+'px'; - el.style.marginRight = -(el.dataset.finalWidth - el.dataset.interWidth)+(el.dataset.finalMarginRight * 1)+'px'; - } - - if(el.dataset.origHeight - el.dataset.finalHeight && el.dataset.finalHeight * 1){ - el.style.height = el.dataset.finalHeight+'px'; - el.style.marginBottom = -(el.dataset.finalHeight - el.dataset.interHeight)+(el.dataset.finalMarginBottom * 1) +'px'; - } - } - } - - if(self._changingClass){ - self._$container - .removeClass(self.layout.containerClass) - .addClass(self._newClass); - } - - for(var i = 0; i < self._$toHide.length; i++){ - var el = self._$toHide[i], - $el = $(el), - delay = self._getDelay(i), - toHideCSS = {}; - - for(var j = 0; j<2; j++){ - var a = j === 0 ? a = self._prefix : ''; - - toHideCSS[a+'transition-delay'] = delay+'ms'; - toHideCSS[a+'transform'] = self.effects.transformOut; - toHideCSS.opacity = self.effects.opacity; - } - - $el.css(self.effects.transition).css(toHideCSS); - - if(self.effects.transform || self.effects.opacity){ - self._bindTargetDone($el); - }; - } - - self._execAction('_animateTargets', 1); - - }, - - /** - * Bind Targets TransitionEnd - * @since 2.0.0 - * @param {object} $el - */ - - _bindTargetDone: function($el){ - var self = this, - el = $el[0]; - - self._execAction('_bindTargetDone', 0, arguments); - - if(!el.dataset.bound){ - - el.dataset.bound = true; - self._targetsBound++; - - $el.on('webkitTransitionEnd.mixItUp transitionend.mixItUp',function(e){ - if( - (e.originalEvent.propertyName.indexOf('transform') > -1 || - e.originalEvent.propertyName.indexOf('opacity') > -1) && - $(e.originalEvent.target).is(self.selectors.target) - ){ - $el.off('.mixItUp'); - el.dataset.bound = ''; - self._targetDone(); - } - }); - } - - self._execAction('_bindTargetDone', 1, arguments); - }, - - /** - * Target Done - * @since 2.0.0 - */ - - _targetDone: function(){ - var self = this; - - self._execAction('_targetDone', 0); - - self._targetsDone++; - - (self._targetsDone === self._targetsBound) && self._cleanUp(); - - self._execAction('_targetDone', 1); - }, - - /** - * Clean Up - * @since 2.0.0 - */ - - _cleanUp: function(){ - var self = this, - targetStyles = self.animation.animateResizeTargets ? - 'transform opacity width height margin-bottom margin-right' : - 'transform opacity', - unBrake = function(){ - self._$targets.removeStyle('transition', self._prefix); - }; - - self._execAction('_cleanUp', 0); - - !self._changingLayout ? - self._$show.css('display',self.layout.display) : - self._$show.css('display',self._newDisplay); - - self._$targets.css(self._brake); - - self._$targets - .removeStyle(targetStyles, self._prefix) - .removeAttr('data-inter-pos-x data-inter-pos-y data-final-pos-x data-final-pos-y data-orig-pos-x data-orig-pos-y data-orig-height data-orig-width data-final-height data-final-width data-inter-width data-inter-height data-orig-margin-right data-orig-margin-bottom data-inter-margin-right data-inter-margin-bottom data-final-margin-right data-final-margin-bottom'); - - self._$hide.removeStyle('display'); - - self._$parent.removeStyle('height transition perspective-distance perspective perspective-origin-x perspective-origin-y perspective-origin perspectiveOrigin', self._prefix); - - if(self._sorting){ - self._printSort(); - self._activeSort = self._newSortString; - self._sorting = false; - } - - if(self._changingLayout){ - if(self._changingDisplay){ - self.layout.display = self._newDisplay; - self._changingDisplay = false; - } - - if(self._changingClass){ - self._$parent.removeClass(self.layout.containerClass).addClass(self._newClass); - self.layout.containerClass = self._newClass; - self._changingClass = false; - } - - self._changingLayout = false; - } - - self._refresh(); - - self._buildState(); - - if(self._state.fail){ - self._$container.addClass(self.layout.containerClassFail); - } - - self._$show = $(); - self._$hide = $(); - - if(window.requestAnimationFrame){ - requestAnimationFrame(unBrake); - } - - self._mixing = false; - - if(typeof self.callbacks._user === 'function'){ - self.callbacks._user.call(self._domNode, self._state, self); - } - - if(typeof self.callbacks.onMixEnd === 'function'){ - self.callbacks.onMixEnd.call(self._domNode, self._state, self); - } - - self._$container.trigger('mixEnd', [self._state, self]); - - if(self._state.fail){ - (typeof self.callbacks.onMixFail === 'function') && self.callbacks.onMixFail.call(self._domNode, self._state, self); - self._$container.trigger('mixFail', [self._state, self]); - } - - if(self._loading){ - (typeof self.callbacks.onMixLoad === 'function') && self.callbacks.onMixLoad.call(self._domNode, self._state, self); - self._$container.trigger('mixLoad', [self._state, self]); - } - - if(self._queue.length){ - self._execAction('_queue', 0); - - self.multiMix(self._queue[0][0],self._queue[0][1],self._queue[0][2]); - self._queue.splice(0, 1); - } - - self._execAction('_cleanUp', 1); - - self._loading = false; - }, - - /** - * Get Prefixed CSS - * @since 2.0.0 - * @param {string} property - * @param {string} value - * @param {boolean} prefixValue - * @return {object} styles - */ - - _getPrefixedCSS: function(property, value, prefixValue){ - var self = this, - styles = {}, - prefix = '', - i = -1; - - for(i = 0; i < 2; i++){ - prefix = i === 0 ? self._prefix : ''; - prefixValue ? styles[prefix+property] = prefix+value : styles[prefix+property] = value; - } - - return self._execFilter('_getPrefixedCSS', styles, arguments); - }, - - /** - * Get Delay - * @since 2.0.0 - * @param {number} i - * @return {number} delay - */ - - _getDelay: function(i){ - var self = this, - n = typeof self.animation.staggerSequence === 'function' ? self.animation.staggerSequence.call(self._domNode, i, self._state) : i, - delay = self.animation.stagger ? n * self.animation.staggerDuration : 0; - - return self._execFilter('_getDelay', delay, arguments); - }, - - /** - * Parse MultiMix Arguments - * @since 2.0.0 - * @param {array} args - * @return {object} output - */ - - _parseMultiMixArgs: function(args){ - var self = this, - output = { - command: null, - animate: self.animation.enable, - callback: null - }; - - for(var i = 0; i < args.length; i++){ - var arg = args[i]; - - if(arg !== null){ - if(typeof arg === 'object' || typeof arg === 'string'){ - output.command = arg; - } else if(typeof arg === 'boolean'){ - output.animate = arg; - } else if(typeof arg === 'function'){ - output.callback = arg; - } - } - } - - return self._execFilter('_parseMultiMixArgs', output, arguments); - }, - - /** - * Parse Insert Arguments - * @since 2.0.0 - * @param {array} args - * @return {object} output - */ - - _parseInsertArgs: function(args){ - var self = this, - output = { - index: 0, - $object: $(), - multiMix: {filter: self._state.activeFilter}, - callback: null - }; - - for(var i = 0; i < args.length; i++){ - var arg = args[i]; - - if(typeof arg === 'number'){ - output.index = arg; - } else if(typeof arg === 'object' && arg instanceof $){ - output.$object = arg; - } else if(typeof arg === 'object' && self._helpers._isElement(arg)){ - output.$object = $(arg); - } else if(typeof arg === 'object' && arg !== null){ - output.multiMix = arg; - } else if(typeof arg === 'boolean' && !arg){ - output.multiMix = false; - } else if(typeof arg === 'function'){ - output.callback = arg; - } - } - - return self._execFilter('_parseInsertArgs', output, arguments); - }, - - /** - * Execute Action - * @since 2.0.0 - * @param {string} methodName - * @param {boolean} isPost - * @param {array} args - */ - - _execAction: function(methodName, isPost, args){ - var self = this, - context = isPost ? 'post' : 'pre'; - - if(!self._actions.isEmptyObject && self._actions.hasOwnProperty(methodName)){ - for(var key in self._actions[methodName][context]){ - self._actions[methodName][context][key].call(self, args); - } - } - }, - - /** - * Execute Filter - * @since 2.0.0 - * @param {string} methodName - * @param {mixed} value - * @return {mixed} value - */ - - _execFilter: function(methodName, value, args){ - var self = this; - - if(!self._filters.isEmptyObject && self._filters.hasOwnProperty(methodName)){ - for(var key in self._filters[methodName]){ - return self._filters[methodName][key].call(self, args); - } - } else { - return value; - } - }, - - /* Helpers - ---------------------------------------------------------------------- */ - - _helpers: { - - /** - * CamelCase - * @since 2.0.0 - * @param {string} - * @return {string} - */ - - _camelCase: function(string){ - return string.replace(/-([a-z])/g, function(g){ - return g[1].toUpperCase(); - }); - }, - - /** - * Is Element - * @since 2.1.3 - * @param {object} element to test - * @return {boolean} - */ - - _isElement: function(el){ - if(window.HTMLElement){ - return el instanceof HTMLElement; - } else { - return ( - el !== null && - el.nodeType === 1 && - el.nodeName === 'string' - ); - } - } - }, - - /* Public Methods - ---------------------------------------------------------------------- */ - - /** - * Is Mixing - * @since 2.0.0 - * @return {boolean} - */ - - isMixing: function(){ - var self = this; - - return self._execFilter('isMixing', self._mixing); - }, - - /** - * Filter (public) - * @since 2.0.0 - * @param {array} arguments - */ - - filter: function(){ - var self = this, - args = self._parseMultiMixArgs(arguments); - - self._clicking && (self._toggleString = ''); - - self.multiMix({filter: args.command}, args.animate, args.callback); - }, - - /** - * Sort (public) - * @since 2.0.0 - * @param {array} arguments - */ - - sort: function(){ - var self = this, - args = self._parseMultiMixArgs(arguments); - - self.multiMix({sort: args.command}, args.animate, args.callback); - }, - - /** - * Change Layout (public) - * @since 2.0.0 - * @param {array} arguments - */ - - changeLayout: function(){ - var self = this, - args = self._parseMultiMixArgs(arguments); - - self.multiMix({changeLayout: args.command}, args.animate, args.callback); - }, - - /** - * MultiMix - * @since 2.0.0 - * @param {array} arguments - */ - - multiMix: function(){ - var self = this, - args = self._parseMultiMixArgs(arguments); - - self._execAction('multiMix', 0, arguments); - - if(!self._mixing){ - if(self.controls.enable && !self._clicking){ - self.controls.toggleFilterButtons && self._buildToggleArray(); - self._updateControls(args.command, self.controls.toggleFilterButtons); - } - - (self._queue.length < 2) && (self._clicking = false); - - delete self.callbacks._user; - if(args.callback) self.callbacks._user = args.callback; - - var sort = args.command.sort, - filter = args.command.filter, - changeLayout = args.command.changeLayout; - - self._refresh(); - - if(sort){ - self._newSort = self._parseSort(sort); - self._newSortString = sort; - - self._sorting = true; - self._sort(); - } - - if(filter !== undf){ - filter = (filter === 'all') ? self.selectors.target : filter; - - self._activeFilter = filter; - } - - self._filter(); - - if(changeLayout){ - self._newDisplay = (typeof changeLayout === 'string') ? changeLayout : changeLayout.display || self.layout.display; - self._newClass = changeLayout.containerClass || ''; - - if( - self._newDisplay !== self.layout.display || - self._newClass !== self.layout.containerClass - ){ - self._changingLayout = true; - - self._changingClass = (self._newClass !== self.layout.containerClass); - self._changingDisplay = (self._newDisplay !== self.layout.display); - } - } - - self._$targets.css(self._brake); - - self._goMix(args.animate ^ self.animation.enable ? args.animate : self.animation.enable); - - self._execAction('multiMix', 1, arguments); - - } else { - if(self.animation.queue && self._queue.length < self.animation.queueLimit){ - self._queue.push(arguments); - - (self.controls.enable && !self._clicking) && self._updateControls(args.command); - - self._execAction('multiMixQueue', 1, arguments); - - } else { - if(typeof self.callbacks.onMixBusy === 'function'){ - self.callbacks.onMixBusy.call(self._domNode, self._state, self); - } - self._$container.trigger('mixBusy', [self._state, self]); - - self._execAction('multiMixBusy', 1, arguments); - } - } - }, - - /** - * Insert - * @since 2.0.0 - * @param {array} arguments - */ - - insert: function(){ - var self = this, - args = self._parseInsertArgs(arguments), - callback = (typeof args.callback === 'function') ? args.callback : null, - frag = document.createDocumentFragment(), - target = (function(){ - self._refresh(); - - if(self._$targets.length){ - return (args.index < self._$targets.length || !self._$targets.length) ? - self._$targets[args.index] : - self._$targets[self._$targets.length-1].nextElementSibling; - } else { - return self._$parent[0].children[0]; - } - })(); - - self._execAction('insert', 0, arguments); - - if(args.$object){ - for(var i = 0; i < args.$object.length; i++){ - var el = args.$object[i]; - - frag.appendChild(el); - frag.appendChild(document.createTextNode(' ')); - } - - self._$parent[0].insertBefore(frag, target); - } - - self._execAction('insert', 1, arguments); - - if(typeof args.multiMix === 'object'){ - self.multiMix(args.multiMix, callback); - } - }, - - /** - * Prepend - * @since 2.0.0 - * @param {array} arguments - */ - - prepend: function(){ - var self = this, - args = self._parseInsertArgs(arguments); - - self.insert(0, args.$object, args.multiMix, args.callback); - }, - - /** - * Append - * @since 2.0.0 - * @param {array} arguments - */ - - append: function(){ - var self = this, - args = self._parseInsertArgs(arguments); - - self.insert(self._state.totalTargets, args.$object, args.multiMix, args.callback); - }, - - /** - * Get Option - * @since 2.0.0 - * @param {string} string - * @return {mixed} value - */ - - getOption: function(string){ - var self = this, - getProperty = function(obj, prop){ - var parts = prop.split('.'), - last = parts.pop(), - l = parts.length, - i = 1, - current = parts[0] || prop; - - while((obj = obj[current]) && i < l){ - current = parts[i]; - i++; - } - - if(obj !== undf){ - return obj[last] !== undf ? obj[last] : obj; - } - }; - - return string ? self._execFilter('getOption', getProperty(self, string), arguments) : self; - }, - - /** - * Set Options - * @since 2.0.0 - * @param {object} config - */ - - setOptions: function(config){ - var self = this; - - self._execAction('setOptions', 0, arguments); - - typeof config === 'object' && $.extend(true, self, config); - - self._execAction('setOptions', 1, arguments); - }, - - /** - * Get State - * @since 2.0.0 - * @return {object} state - */ - - getState: function(){ - var self = this; - - return self._execFilter('getState', self._state, self); - }, - - /** - * Force Refresh - * @since 2.1.2 - */ - - forceRefresh: function(){ - var self = this; - - self._refresh(false, true); - }, - - /** - * Destroy - * @since 2.0.0 - * @param {boolean} hideAll - */ - - destroy: function(hideAll){ - var self = this, - filters = $.MixItUp.prototype._bound._filter, - sorts = $.MixItUp.prototype._bound._sort; - - self._execAction('destroy', 0, arguments); - - self._$body - .add($(self.selectors.sort)) - .add($(self.selectors.filter)) - .off('.mixItUp'); - - for(var i = 0; i < self._$targets.length; i++){ - var target = self._$targets[i]; - - hideAll && (target.style.display = ''); - - delete target.mixParent; - } - - self._execAction('destroy', 1, arguments); - - if(filters[self.selectors.filter] && filters[self.selectors.filter] > 1) { - filters[self.selectors.filter]--; - } else if(filters[self.selectors.filter] === 1) { - delete filters[self.selectors.filter]; - } - - if(sorts[self.selectors.sort] && sorts[self.selectors.sort] > 1) { - sorts[self.selectors.sort]--; - } else if(sorts[self.selectors.sort] === 1) { - delete sorts[self.selectors.sort]; - } - - delete $.MixItUp.prototype._instances[self._id]; - } - - }; - - /* jQuery Methods - ---------------------------------------------------------------------- */ - - /** - * jQuery .mixItUp() method - * @since 2.0.0 - * @extends $.fn - */ - - $.fn.mixItUp = function(){ - var args = arguments, - dataReturn = [], - eachReturn, - _instantiate = function(domNode, settings){ - var instance = new $.MixItUp(), - rand = function(){ - return ('00000'+(Math.random()*16777216<<0).toString(16)).substr(-6).toUpperCase(); - }; - - instance._execAction('_instantiate', 0, arguments); - - domNode.id = !domNode.id ? 'MixItUp'+rand() : domNode.id; - - if(!instance._instances[domNode.id]){ - instance._instances[domNode.id] = instance; - instance._init(domNode, settings); - } - - instance._execAction('_instantiate', 1, arguments); - }; - - eachReturn = this.each(function(){ - if(args && typeof args[0] === 'string'){ - var instance = $.MixItUp.prototype._instances[this.id]; - if(args[0] === 'isLoaded'){ - dataReturn.push(instance ? true : false); - } else { - var data = instance[args[0]](args[1], args[2], args[3]); - if(data !== undf)dataReturn.push(data); - } - } else { - _instantiate(this, args[0]); - } - }); - - if(dataReturn.length){ - return dataReturn.length > 1 ? dataReturn : dataReturn[0]; - } else { - return eachReturn; - } - }; - - /** - * jQuery .removeStyle() method - * @since 2.0.0 - * @extends $.fn - */ - - $.fn.removeStyle = function(style, prefix){ - prefix = prefix ? prefix : ''; - - return this.each(function(){ - var el = this, - styles = style.split(' '); - - for(var i = 0; i < styles.length; i++){ - for(var j = 0; j < 4; j++){ - switch (j) { - case 0: - var prop = styles[i]; - break; - case 1: - var prop = $.MixItUp.prototype._helpers._camelCase(prop); - break; - case 2: - var prop = prefix+styles[i]; - break; - case 3: - var prop = $.MixItUp.prototype._helpers._camelCase(prefix+styles[i]); - } - - if( - el.style[prop] !== undf && - typeof el.style[prop] !== 'unknown' && - el.style[prop].length > 0 - ){ - el.style[prop] = ''; - } - - if(!prefix && j === 1)break; - } - } - - if(el.attributes && el.attributes.style && el.attributes.style !== undf && el.attributes.style.value === ''){ - el.attributes.removeNamedItem('style'); - } - }); - }; - -})(jQuery); \ No newline at end of file +(function(n,t){"use strict";n.MixItUp=function(){var t=this;t._execAction("_constructor",0);n.extend(t,{selectors:{target:".mix",filter:".filter",sort:".sort"},animation:{enable:!0,effects:"fade scale",duration:600,easing:"ease",perspectiveDistance:"3000",perspectiveOrigin:"50% 50%",queue:!0,queueLimit:1,animateChangeLayout:!1,animateResizeContainer:!0,animateResizeTargets:!1,staggerSequence:!1,reverseOut:!1},callbacks:{onMixLoad:!1,onMixStart:!1,onMixBusy:!1,onMixEnd:!1,onMixFail:!1,_user:!1},controls:{enable:!0,live:!1,toggleFilterButtons:!1,toggleLogic:"or",activeClass:"active"},layout:{display:"inline-block",containerClass:"",containerClassFail:"fail"},load:{filter:"all",sort:!1},_$body:null,_$container:null,_$targets:null,_$parent:null,_$sortButtons:null,_$filterButtons:null,_suckMode:!1,_mixing:!1,_sorting:!1,_clicking:!1,_loading:!0,_changingLayout:!1,_changingClass:!1,_changingDisplay:!1,_origOrder:[],_startOrder:[],_newOrder:[],_activeFilter:null,_toggleArray:[],_toggleString:"",_activeSort:"default:asc",_newSort:null,_startHeight:null,_newHeight:null,_incPadding:!0,_newDisplay:null,_newClass:null,_targetsBound:0,_targetsDone:0,_queue:[],_$show:n(),_$hide:n()});t._execAction("_constructor",1)};n.MixItUp.prototype={constructor:n.MixItUp,_instances:{},_handled:{_filter:{},_sort:{}},_bound:{_filter:{},_sort:{}},_actions:{},_filters:{},extend:function(t){for(var i in t)n.MixItUp.prototype[i]=t[i]},addAction:function(t,i,r,u){n.MixItUp.prototype._addHook("_actions",t,i,r,u)},addFilter:function(t,i,r,u){n.MixItUp.prototype._addHook("_filters",t,i,r,u)},_addHook:function(t,i,r,u,f){var o=n.MixItUp.prototype[t],e={};f=f===1||f==="post"?"post":"pre";e[i]={};e[i][f]={};e[i][f][r]=u;n.extend(!0,o,e)},_init:function(t,i){var r=this,u;if(r._execAction("_init",0,arguments),i&&n.extend(!0,r,i),r._$body=n("body"),r._domNode=t,r._$container=n(t),r._$container.addClass(r.layout.containerClass),r._id=t.id,r._platformDetect(),r._brake=r._getPrefixedCSS("transition","none"),r._refresh(!0),r._$parent=r._$targets.parent().length?r._$targets.parent():r._$container,r.load.sort&&(r._newSort=r._parseSort(r.load.sort),r._newSortString=r.load.sort,r._activeSort=r.load.sort,r._sort(),r._printSort()),r._activeFilter=r.load.filter==="all"?r.selectors.target:r.load.filter==="none"?"":r.load.filter,r.controls.enable&&r._bindHandlers(),r.controls.toggleFilterButtons)for(r._buildToggleArray(),u=0;u-1&&(h=r._helpers._camelCase(o.substring(5,o.length)),u.dataset[h]=c)}u.mixParent===t&&(u.mixParent=r._id)}if(r._$targets.length&&n||!r._origOrder.length&&r._$targets.length)for(r._origOrder=[],f=0;f-1)&&(n(u.selectors.sort).removeClass(u.controls.activeClass),f(i,r),u.sort(o))),r==="filter"&&(e=i.attr("data-filter"),h=u.controls.toggleLogic==="or"?",":"",u.controls.toggleFilterButtons?(u._buildToggleArray(),i.hasClass(u.controls.activeClass)?(f(i,r,!0),s=u._toggleArray.indexOf(e),u._toggleArray.splice(s,1)):(f(i,r),u._toggleArray.push(e)),u._toggleArray=n.grep(u._toggleArray,function(n){return n}),u._toggleString=u._toggleArray.join(h),u.filter(u._toggleString)):i.hasClass(u.controls.activeClass)||(n(u.selectors.filter).removeClass(u.controls.activeClass),f(i,r),u.filter(e))),u._execAction("_processClick",1,arguments)):(typeof u.callbacks.onMixBusy=="function"&&u.callbacks.onMixBusy.call(u._domNode,u._state,u),u._execAction("_processClickBusy",1,arguments))},_buildToggleArray:function(){var n=this,i=n._activeFilter.replace(/\s/g,""),t,r;if(n._execAction("_buildToggleArray",0,arguments),n.controls.toggleLogic==="or")n._toggleArray=i.split(",");else for(n._toggleArray=i.split("."),n._toggleArray[0]||n._toggleArray.shift(),t=0;r=n._toggleArray[t];t++)n._toggleArray[t]="."+r;n._execAction("_buildToggleArray",1,arguments)},_updateControls:function(i,r){var u=this,f={filter:i.filter,sort:i.sort},h=function(n,t){try{r&&e==="filter"&&!(f.filter==="none"||f.filter==="")?n.filter(t).addClass(u.controls.activeClass):n.removeClass(u.controls.activeClass).filter(t).addClass(u.controls.activeClass)}catch(i){}},e="filter",o=null,s;for(u._execAction("_updateControls",0,arguments),i.filter===t&&(f.filter=u._activeFilter),i.sort===t&&(f.sort=u._activeSort),f.filter===u.selectors.target&&(f.filter="all"),s=0;s<2;s++)o=u.controls.live?n(u.selectors[e]):u["_$"+e+"Buttons"],o&&h(o,"[data-"+e+'="'+f[e]+'"]'),e="sort";u._execAction("_updateControls",1,arguments)},_filter:function(){var t=this,i,r;for(t._execAction("_filter",0),i=0;ie?o==="asc"?1:-1:f===e&&u._newSort.length>i+1?u._compare(n,t,i+1):0},_printSort:function(n){var t=this,s=n?t._startOrder:t._newOrder,u=t._$parent[0].querySelectorAll(t.selectors.target),h=u.length?u[u.length-1].nextElementSibling:null,r=document.createDocumentFragment(),f,e,i,o,c;for(t._execAction("_printSort",0,arguments),i=0;i-1){if(i&&(r=n.animation.effects.indexOf(t+"("),r>-1)){var u=n.animation.effects.substring(r),f=/\(([^)]+)\)/.exec(u),e=f[1];return{val:e}}return!0}return!1},u=function(n,t){return t?n.charAt(0)==="-"?n.substr(1,n.length):"-"+n:n},r=function(n,r){for(var o=[["scale",".01"],["translateX","20px"],["translateY","20px"],["translateZ","20px"],["rotateX","90deg"],["rotateY","90deg"],["rotateZ","180deg"],],f=0;f-1||u.originalEvent.propertyName.indexOf("opacity")>-1)&&n(u.originalEvent.target).is(i.selectors.target)&&(t.off(".mixItUp"),r.dataset.bound="",i._targetDone())})}i._execAction("_bindTargetDone",1,arguments)},_targetDone:function(){var n=this;n._execAction("_targetDone",0);n._targetsDone++;n._targetsDone===n._targetsBound&&n._cleanUp();n._execAction("_targetDone",1)},_cleanUp:function(){var t=this,i=t.animation.animateResizeTargets?"transform opacity width height margin-bottom margin-right":"transform opacity",r=function(){t._$targets.removeStyle("transition",t._prefix)};t._execAction("_cleanUp",0);t._changingLayout?t._$show.css("display",t._newDisplay):t._$show.css("display",t.layout.display);t._$targets.css(t._brake);t._$targets.removeStyle(i,t._prefix).removeAttr("data-inter-pos-x data-inter-pos-y data-final-pos-x data-final-pos-y data-orig-pos-x data-orig-pos-y data-orig-height data-orig-width data-final-height data-final-width data-inter-width data-inter-height data-orig-margin-right data-orig-margin-bottom data-inter-margin-right data-inter-margin-bottom data-final-margin-right data-final-margin-bottom");t._$hide.removeStyle("display");t._$parent.removeStyle("height transition perspective-distance perspective perspective-origin-x perspective-origin-y perspective-origin perspectiveOrigin",t._prefix);t._sorting&&(t._printSort(),t._activeSort=t._newSortString,t._sorting=!1);t._changingLayout&&(t._changingDisplay&&(t.layout.display=t._newDisplay,t._changingDisplay=!1),t._changingClass&&(t._$parent.removeClass(t.layout.containerClass).addClass(t._newClass),t.layout.containerClass=t._newClass,t._changingClass=!1),t._changingLayout=!1);t._refresh();t._buildState();t._state.fail&&t._$container.addClass(t.layout.containerClassFail);t._$show=n();t._$hide=n();window.requestAnimationFrame&&requestAnimationFrame(r);t._mixing=!1;typeof t.callbacks._user=="function"&&t.callbacks._user.call(t._domNode,t._state,t);typeof t.callbacks.onMixEnd=="function"&&t.callbacks.onMixEnd.call(t._domNode,t._state,t);t._$container.trigger("mixEnd",[t._state,t]);t._state.fail&&(typeof t.callbacks.onMixFail=="function"&&t.callbacks.onMixFail.call(t._domNode,t._state,t),t._$container.trigger("mixFail",[t._state,t]));t._loading&&(typeof t.callbacks.onMixLoad=="function"&&t.callbacks.onMixLoad.call(t._domNode,t._state,t),t._$container.trigger("mixLoad",[t._state,t]));t._queue.length&&(t._execAction("_queue",0),t.multiMix(t._queue[0][0],t._queue[0][1],t._queue[0][2]),t._queue.splice(0,1));t._execAction("_cleanUp",1);t._loading=!1},_getPrefixedCSS:function(n,t,i){for(var f=this,e={},u="",r=-1,r=0;r<2;r++)u=r===0?f._prefix:"",e[u+n]=i?u+t:t;return f._execFilter("_getPrefixedCSS",e,arguments)},_getDelay:function(n){var t=this,i=typeof t.animation.staggerSequence=="function"?t.animation.staggerSequence.call(t._domNode,n,t._state):n,r=t.animation.stagger?i*t.animation.staggerDuration:0;return t._execFilter("_getDelay",r,arguments)},_parseMultiMixArgs:function(n){for(var t,u=this,i={command:null,animate:u.animation.enable,callback:null},r=0;r1?r[i.selectors.filter]--:r[i.selectors.filter]===1&&delete r[i.selectors.filter];u[i.selectors.sort]&&u[i.selectors.sort]>1?u[i.selectors.sort]--:u[i.selectors.sort]===1&&delete u[i.selectors.sort];delete n.MixItUp.prototype._instances[i._id]}};n.fn.mixItUp=function(){var i=arguments,r=[],u,f=function(t,i){var r=new n.MixItUp,u=function(){return("00000"+(Math.random()*16777216<<0).toString(16)).substr(-6).toUpperCase()};r._execAction("_instantiate",0,arguments);t.id=t.id?t.id:"MixItUp"+u();r._instances[t.id]||(r._instances[t.id]=r,r._init(t,i));r._execAction("_instantiate",1,arguments)};return u=this.each(function(){var u,e;i&&typeof i[0]=="string"?(u=n.MixItUp.prototype._instances[this.id],i[0]==="isLoaded"?r.push(u?!0:!1):(e=u[i[0]](i[1],i[2],i[3]),e!==t&&r.push(e))):f(this,i[0])}),r.length?r.length>1?r:r[0]:u};n.fn.removeStyle=function(i,r){return r=r?r:"",this.each(function(){for(var o,f,u=this,s=i.split(" "),e=0;e0&&(u.style[f]=""),!r&&o===1)break}u.attributes&&u.attributes.style&&u.attributes.style!==t&&u.attributes.style.value===""&&u.attributes.removeNamedItem("style")})}})(jQuery); \ No newline at end of file diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index 7876aa41c..d15198709 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -26,6 +26,7 @@ #endregion using System.Dynamic; using System.Linq; +using System.Web.UI.WebControls; using MarkdownSharp; @@ -44,6 +45,8 @@ using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; using PlexRequests.Services.Notification; +using PlexRequests.Store.Models; +using PlexRequests.Store.Repository; using PlexRequests.UI.Helpers; using PlexRequests.UI.Models; @@ -63,6 +66,7 @@ namespace PlexRequests.UI.Modules private ISonarrApi SonarrApi { get; } private PushbulletApi PushbulletApi { get; } private ICouchPotatoApi CpApi { get; } + private IRepository LogsRepo { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); public AdminModule(ISettingsService rpService, @@ -76,7 +80,8 @@ namespace PlexRequests.UI.Modules IPlexApi plexApi, ISettingsService pbSettings, PushbulletApi pbApi, - ICouchPotatoApi cpApi) : base("admin") + ICouchPotatoApi cpApi, + IRepository logsRepo) : base("admin") { RpService = rpService; CpService = cpService; @@ -90,6 +95,7 @@ namespace PlexRequests.UI.Modules PushbulletApi = pbApi; CpApi = cpApi; SickRageService = sickrage; + LogsRepo = logsRepo; #if !DEBUG this.RequiresAuthentication(); @@ -126,6 +132,10 @@ namespace PlexRequests.UI.Modules Get["/pushbulletnotification"] = _ => PushbulletNotifications(); Post["/pushbulletnotification"] = _ => SavePushbulletNotifications(); + + Get["/logs"] = _ => Logs(); + Get["/loglevel"] = _ => GetLogLevels(); + Post["/loglevel"] = _ => UpdateLogLevels(Request.Form.level); } private Negotiator Authentication() @@ -246,8 +256,8 @@ namespace PlexRequests.UI.Modules } var result = CpService.SaveSettings(couchPotatoSettings); - return Response.AsJson(result - ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for CouchPotato!" } + return Response.AsJson(result + ? new JsonResponseModel { Result = true, Message = "Successfully Updated the Settings for CouchPotato!" } : new JsonResponseModel { Result = false, Message = "Could not update the settings, take a look at the logs." }); } @@ -422,5 +432,24 @@ namespace PlexRequests.UI.Modules return Response.AsJson(profiles); } + + private Negotiator Logs() + { + var allLogs = LogsRepo.GetAll().OrderByDescending(x => x.Id).Take(20); + return View["Logs", allLogs]; + } + + private Response GetLogLevels() + { + var levels = LogManager.Configuration.LoggingRules.FirstOrDefault(x => x.NameMatches("database")); + return Response.AsJson(levels.Levels); + } + + private Response UpdateLogLevels(int level) + { + var newLevel = LogLevel.FromOrdinal(level); + LoggingHelper.ReconfigureLogLevel(newLevel); + return Response.AsJson(new JsonResponseModel { Result = true, Message = $"The new log level is now {newLevel}"}); + } } } \ No newline at end of file diff --git a/PlexRequests.UI/Modules/ApprovalModule.cs b/PlexRequests.UI/Modules/ApprovalModule.cs index 10795ceaa..b2c6217bb 100644 --- a/PlexRequests.UI/Modules/ApprovalModule.cs +++ b/PlexRequests.UI/Modules/ApprovalModule.cs @@ -169,18 +169,14 @@ namespace PlexRequests.UI.Modules private Response RequestMovieAndUpdateStatus(RequestedModel request) { - if (!Context.CurrentUser.IsAuthenticated()) - { - return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); - } - var cpSettings = CpService.GetSettings(); var cp = new CouchPotatoApi(); - Log.Info("Adding movie to CP : {0}", request.Title); + Log.Info("Adding movie to CouchPotato : {0}", request.Title); if (!cpSettings.Enabled) { // Approve it request.Approved = true; + Log.Warn("We approved movie: {0} but could not add it to CouchPotato because it has not been setup", request.Title); // Update the record var inserted = Service.UpdateRequest(request); @@ -226,6 +222,11 @@ namespace PlexRequests.UI.Modules /// private Response ApproveAll() { + if (!Context.CurrentUser.IsAuthenticated()) + { + return Response.AsJson(new JsonResponseModel { Result = false, Message = "You are not an Admin, so you cannot approve any requests." }); + } + var requests = Service.GetAll().Where(x => x.Approved == false); var requestedModels = requests as RequestedModel[] ?? requests.ToArray(); if (!requestedModels.Any()) diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 1a10bcc77..36f0619c4 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -51,7 +51,7 @@ namespace PlexRequests.UI.Modules model.AdminExists = adminCreated; return View["Login/Index", model]; } - + }; Get["/logout"] = x => this.LogoutAndRedirect("~/"); @@ -76,7 +76,8 @@ namespace PlexRequests.UI.Modules return this.LoginAndRedirect(userId.Value, expiry); }; - Get["/register"] = x => { + Get["/register"] = x => + { { dynamic model = new ExpandoObject(); model.Errored = Request.Query.error.HasValue; @@ -87,13 +88,13 @@ namespace PlexRequests.UI.Modules Post["/register"] = x => { - var username = (string) Request.Form.Username; + var username = (string)Request.Form.Username; var exists = UserMapper.DoUsersExist(); if (exists) { - return Context.GetRedirect("~/register?error=true&username=" + username); + return Context.GetRedirect("~/register?error=true"); } - var userId = UserMapper.CreateUser(username, Request.Form.Password); + var userId = UserMapper.CreateUser(username, Request.Form.Password, new[] { "Admin" }); Session[SessionKeys.UsernameKey] = username; return this.LoginAndRedirect((Guid)userId); }; @@ -116,7 +117,7 @@ namespace PlexRequests.UI.Modules var newPasswordAgain = Request.Form.NewPasswordAgain; if (!newPassword.Equals(newPasswordAgain)) { - + } var result = UserMapper.UpdateUser(username, oldPass, newPassword); diff --git a/PlexRequests.UI/NLog.config b/PlexRequests.UI/NLog.config index 39605a1f7..9adcfc54b 100644 --- a/PlexRequests.UI/NLog.config +++ b/PlexRequests.UI/NLog.config @@ -13,7 +13,7 @@ layout="${date} ${logger} ${level}: ${message}" /> - - + --> - + \ No newline at end of file diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 44ec9805c..b88556373 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -97,6 +97,10 @@ False ..\Assemblies\Mono.Data.Sqlite.dll + + ..\packages\Mono.Posix.4.0.0.0\lib\net40\Mono.Posix.dll + True + ..\packages\Nancy.1.4.3\lib\net40\Nancy.dll True @@ -335,6 +339,9 @@ Always + + Always + web.config diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index 80f0adebb..f84972699 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -30,6 +30,8 @@ using System.Data; using Microsoft.Owin.Hosting; using Mono.Data.Sqlite; +using Mono.Unix; +using Mono.Unix.Native; using NLog; using NLog.Config; @@ -55,7 +57,7 @@ namespace PlexRequests.UI int portResult; if (!int.TryParse(args[0], out portResult)) { - Console.WriteLine("Incorrect Port format. Press Any Key to shut down."); + Console.WriteLine("Incorrect Port format. Press any key."); Console.ReadLine(); Environment.Exit(1); } @@ -65,7 +67,8 @@ namespace PlexRequests.UI WriteOutVersion(); var s = new Setup(); - s.SetupDb(); + var cn = s.SetupDb(); + ConfigureTargets(cn); if (port == -1) port = GetStartupPort(); @@ -76,18 +79,29 @@ namespace PlexRequests.UI }; try { + using (WebApp.Start(options)) + { + Console.WriteLine($"Request Plex is running on the following: http://+:{port}/"); - using (WebApp.Start(options)) - { - Console.WriteLine($"Request Plex is running on the following port: {port}"); - Console.WriteLine("Press any key to exit"); - Console.ReadLine(); - } - + if (Type.GetType("Mono.Runtime") != null) + { + Log.Info("We are on Mono!"); + // on mono, processes will usually run as daemons - this allows you to listen + // for termination signals (ctrl+c, shutdown, etc) and finalize correctly + UnixSignal.WaitAny( + new[] { new UnixSignal(Signum.SIGINT), new UnixSignal(Signum.SIGTERM), new UnixSignal(Signum.SIGQUIT), new UnixSignal(Signum.SIGHUP) }); + } + else + { + Log.Info("This is not Mono"); + Console.WriteLine("Press any key to exit"); + Console.ReadLine(); + } + } } catch (Exception e) { - var a = e.Message; + Log.Fatal(e); throw; } } @@ -116,59 +130,7 @@ namespace PlexRequests.UI private static void ConfigureTargets(string connectionString) { - LogManager.ThrowExceptions = true; - // Step 1. Create configuration object - var config = new LoggingConfiguration(); - - // Step 2. Create targets and add them to the configuration - var databaseTarget = new DatabaseTarget - { - CommandType = CommandType.Text, - ConnectionString = connectionString, - DBProvider = "Mono.Data.Sqlite.SqliteConnection, Mono.Data.Sqlite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756", - Name = "database" - }; - - - var messageParam = new DatabaseParameterInfo { Name = "@Message", Layout = "${message}" }; - var callsiteParam = new DatabaseParameterInfo { Name = "@Callsite", Layout = "${callsite}" }; - var levelParam = new DatabaseParameterInfo { Name = "@Level", Layout = "${level}" }; - var usernameParam = new DatabaseParameterInfo { Name = "@Username", Layout = "${identity}" }; - var dateParam = new DatabaseParameterInfo { Name = "@Date", Layout = "${date}" }; - var loggerParam = new DatabaseParameterInfo { Name = "@Logger", Layout = "${logger}" }; - var exceptionParam = new DatabaseParameterInfo { Name = "@Exception", Layout = "${exception:tostring}" }; - - databaseTarget.Parameters.Add(messageParam); - databaseTarget.Parameters.Add(callsiteParam); - databaseTarget.Parameters.Add(levelParam); - databaseTarget.Parameters.Add(usernameParam); - databaseTarget.Parameters.Add(dateParam); - databaseTarget.Parameters.Add(loggerParam); - databaseTarget.Parameters.Add(exceptionParam); - - databaseTarget.CommandText = "INSERT INTO Log (Username,Date,Level,Logger, Message, Callsite, Exception) VALUES(@Username,@Date,@Level,@Logger, @Message, @Callsite, @Exception);"; - config.AddTarget("database", databaseTarget); - - // Step 4. Define rules - var rule1 = new LoggingRule("*", LogLevel.Error, databaseTarget); - config.LoggingRules.Add(rule1); - - try - { - - // Step 5. Activate the configuration - LogManager.Configuration = config; - } - catch (Exception) - { - - throw; - } - - // Example usage - Logger logger = LogManager.GetLogger("Example"); - - logger.Error("error log message"); + LoggingHelper.ConfigureLogging(connectionString); } } } diff --git a/PlexRequests.UI/Views/Admin/Logs.cshtml b/PlexRequests.UI/Views/Admin/Logs.cshtml new file mode 100644 index 000000000..c11e9bcbe --- /dev/null +++ b/PlexRequests.UI/Views/Admin/Logs.cshtml @@ -0,0 +1,118 @@ +@Html.Partial("_Sidebar") + +
+
+ Logs + +
+
+ +
+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + @foreach (var m in Model) + { + + + + + + + + + } +
MessageLoggerExceptionCallsiteLog LevelDate
+ @m.Message + + @m.Logger + + @m.Exception + + @m.Callsite + + @m.Level + + @m.Date +
+ + +
+
+ + + + \ No newline at end of file diff --git a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml index 2877729c7..641a857da 100644 --- a/PlexRequests.UI/Views/Admin/_Sidebar.cshtml +++ b/PlexRequests.UI/Views/Admin/_Sidebar.cshtml @@ -70,7 +70,14 @@ { Pushbullet Notifications } - + @if (Context.Request.Path == "/admin/logs") + { + Logs + } + else + { + Logs + } @if (Context.Request.Path == "/admin/status") { Status diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index d4c94f6b9..b96e89b20 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -57,26 +57,26 @@ {
  • Requests
  • } - @if (Context.CurrentUser.IsAuthenticated()) - { - if (Context.Request.Path == "/admin") - { -
  • Admin
  • - } - else - { -
  • Admin
  • - } - }
    +
    + +
    + +
    +
    @@ -66,6 +72,8 @@
    + +
    From 24781fbd1fac9844151b3a174faa93d8df755b0f Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 24 Mar 2016 14:07:01 +0000 Subject: [PATCH 24/34] Finished #49 --- PlexRequests.Core/JsonRequestService.cs | 20 +++---- PlexRequests.Core/Models/UserProperties.cs | 35 ++++++++++++ PlexRequests.Core/PlexRequests.Core.csproj | 1 + PlexRequests.Core/UserMapper.cs | 27 ++++++++-- PlexRequests.Helpers/ByteConverterHelper.cs | 54 +++++++++++++++++++ .../PlexRequests.Helpers.csproj | 1 + PlexRequests.Store/SqlTables.sql | 3 +- PlexRequests.Store/UsersModel.cs | 3 +- PlexRequests.UI/Modules/LoginModule.cs | 17 +++--- .../Views/Login/ChangePassword.cshtml | 32 ++++++++++- PlexRequests.UI/Views/Shared/_Layout.cshtml | 4 +- 11 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 PlexRequests.Core/Models/UserProperties.cs create mode 100644 PlexRequests.Helpers/ByteConverterHelper.cs diff --git a/PlexRequests.Core/JsonRequestService.cs b/PlexRequests.Core/JsonRequestService.cs index 533bc1c56..1504faed6 100644 --- a/PlexRequests.Core/JsonRequestService.cs +++ b/PlexRequests.Core/JsonRequestService.cs @@ -30,6 +30,7 @@ using System.Text; using Newtonsoft.Json; +using PlexRequests.Helpers; using PlexRequests.Store; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; @@ -45,13 +46,13 @@ namespace PlexRequests.Core private IRequestRepository Repo { get; } public long AddRequest(RequestedModel model) { - var entity = new RequestBlobs { Type = model.Type, Content = ReturnBytes(model), ProviderId = model.ProviderId }; + var entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId }; var id = Repo.Insert(entity); // TODO Keep an eye on this, since we are now doing 2 DB update for 1 single request, inserting and then updating model.Id = (int)id; - entity = new RequestBlobs { Type = model.Type, Content = ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id }; + entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = (int)id }; var result = Repo.Update(entity); return result ? id : -1; @@ -71,15 +72,14 @@ namespace PlexRequests.Core public bool UpdateRequest(RequestedModel model) { - var entity = new RequestBlobs { Type = model.Type, Content = ReturnBytes(model), ProviderId = model.ProviderId, Id = model.Id }; + var entity = new RequestBlobs { Type = model.Type, Content = ByteConverterHelper.ReturnBytes(model), ProviderId = model.ProviderId, Id = model.Id }; return Repo.Update(entity); } public RequestedModel Get(int id) { var blob = Repo.Get(id); - var json = Encoding.UTF8.GetString(blob.Content); - var model = JsonConvert.DeserializeObject(json); + var model = ByteConverterHelper.ReturnObject(blob.Content); return model; } @@ -93,16 +93,8 @@ namespace PlexRequests.Core public bool BatchUpdate(List model) { - var entities = model.Select(m => new RequestBlobs { Type = m.Type, Content = ReturnBytes(m), ProviderId = m.ProviderId, Id = m.Id }).ToList(); + var entities = model.Select(m => new RequestBlobs { Type = m.Type, Content = ByteConverterHelper.ReturnBytes(m), ProviderId = m.ProviderId, Id = m.Id }).ToList(); return Repo.UpdateAll(entities); } - - public byte[] ReturnBytes(object obj) - { - var json = JsonConvert.SerializeObject(obj); - var bytes = Encoding.UTF8.GetBytes(json); - - return bytes; - } } } \ No newline at end of file diff --git a/PlexRequests.Core/Models/UserProperties.cs b/PlexRequests.Core/Models/UserProperties.cs new file mode 100644 index 000000000..8cd210d57 --- /dev/null +++ b/PlexRequests.Core/Models/UserProperties.cs @@ -0,0 +1,35 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: UserProperties.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Core.Models +{ + public class UserProperties + { + public string EmailAddress { get; set; } + public bool NotifyOnRelease { get; set; } + public bool NotifyOnApprove { get; set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Core/PlexRequests.Core.csproj b/PlexRequests.Core/PlexRequests.Core.csproj index 05bef9e03..a1e0538f7 100644 --- a/PlexRequests.Core/PlexRequests.Core.csproj +++ b/PlexRequests.Core/PlexRequests.Core.csproj @@ -72,6 +72,7 @@ + diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 6bacbdca9..211145069 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion using System; +using System.Collections.Generic; using System.Linq; using System.Security; @@ -32,6 +33,7 @@ using Nancy; using Nancy.Authentication.Forms; using Nancy.Security; +using PlexRequests.Core.Models; using PlexRequests.Helpers; using PlexRequests.Store; @@ -46,7 +48,7 @@ namespace PlexRequests.Core private static ISqliteConfiguration Db { get; set; } public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context) { - var repo = new UserRepository(Db); + var repo = new UserRepository(Db); var user = repo.Get(identifier.ToString()); @@ -58,6 +60,7 @@ namespace PlexRequests.Core return new UserIdentity { UserName = user.UserName, + Claims = ByteConverterHelper.ReturnObject(user.Claims) }; } @@ -84,7 +87,7 @@ namespace PlexRequests.Core { var repo = new UserRepository(Db); var users = repo.GetAll(); - + return users.Any(); } @@ -93,15 +96,23 @@ namespace PlexRequests.Core var repo = new UserRepository(Db); var salt = PasswordHasher.GenerateSalt(); - var userModel = new UsersModel { UserName = username, UserGuid = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt), Claims = claims}; + var userModel = new UsersModel + { + UserName = username, + UserGuid = Guid.NewGuid().ToString(), + Salt = salt, + Hash = PasswordHasher.ComputeHash(password, salt), + Claims = JsonRequestService.ReturnBytes(claims), + UserProperties = JsonRequestService.ReturnBytes(new UserProperties()) + }; repo.Insert(userModel); var userRecord = repo.Get(userModel.UserGuid); - + return new Guid(userRecord.UserGuid); } - public static bool UpdateUser(string username, string oldPassword, string newPassword) + public static bool UpdatePassword(string username, string oldPassword, string newPassword) { var repo = new UserRepository(Db); var users = repo.GetAll(); @@ -123,5 +134,11 @@ namespace PlexRequests.Core return repo.Update(userToChange); } + + public static IEnumerable GetUsers() + { + var repo = new UserRepository(Db); + return repo.GetAll(); + } } } diff --git a/PlexRequests.Helpers/ByteConverterHelper.cs b/PlexRequests.Helpers/ByteConverterHelper.cs new file mode 100644 index 000000000..87d569592 --- /dev/null +++ b/PlexRequests.Helpers/ByteConverterHelper.cs @@ -0,0 +1,54 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: ByteConverterHelper.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System.Text; + +using Newtonsoft.Json; + +namespace PlexRequests.Helpers +{ + public class ByteConverterHelper + { + public static byte[] ReturnBytes(object obj) + { + var json = JsonConvert.SerializeObject(obj); + var bytes = Encoding.UTF8.GetBytes(json); + + return bytes; + } + + public static T ReturnObject(byte[] bytes) + { + var json = Encoding.UTF8.GetString(bytes); + var model = JsonConvert.DeserializeObject(json); + return model; + } + public static string ReturnFromBytes(byte[] bytes) + { + return Encoding.UTF8.GetString(bytes); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Helpers/PlexRequests.Helpers.csproj b/PlexRequests.Helpers/PlexRequests.Helpers.csproj index 6af45a9b5..ec3c70a8d 100644 --- a/PlexRequests.Helpers/PlexRequests.Helpers.csproj +++ b/PlexRequests.Helpers/PlexRequests.Helpers.csproj @@ -51,6 +51,7 @@ + diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 657c2ea60..f23b3c5fc 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -7,7 +7,8 @@ CREATE TABLE IF NOT EXISTS Users UserName varchar(50) NOT NULL, Salt BLOB NOT NULL, Hash BLOB NOT NULL, - Claims BLOB NOT NULL + Claims BLOB NOT NULL, + UserProperties BLOB ); diff --git a/PlexRequests.Store/UsersModel.cs b/PlexRequests.Store/UsersModel.cs index cf5eec764..8a3753c6c 100644 --- a/PlexRequests.Store/UsersModel.cs +++ b/PlexRequests.Store/UsersModel.cs @@ -33,6 +33,7 @@ namespace PlexRequests.Store { public byte[] Hash { get; set; } public byte[] Salt { get; set; } - public string[] Claims { get; set; } + public byte[] Claims { get; set; } + public byte[] UserProperties { get; set; } } } diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 36f0619c4..5436be973 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -49,7 +49,7 @@ namespace PlexRequests.UI.Modules model.Errored = Request.Query.error.HasValue; var adminCreated = UserMapper.DoUsersExist(); model.AdminExists = adminCreated; - return View["Login/Index", model]; + return View["Index", model]; } }; @@ -82,7 +82,7 @@ namespace PlexRequests.UI.Modules dynamic model = new ExpandoObject(); model.Errored = Request.Query.error.HasValue; - return View["Login/Register", model]; + return View["Register", model]; } }; @@ -109,7 +109,7 @@ namespace PlexRequests.UI.Modules return View["ChangePassword"]; } - private Negotiator ChangePasswordPost() + private Response ChangePasswordPost() { var username = Context.CurrentUser.UserName; var oldPass = Request.Form.OldPassword; @@ -117,11 +117,16 @@ namespace PlexRequests.UI.Modules var newPasswordAgain = Request.Form.NewPasswordAgain; if (!newPassword.Equals(newPasswordAgain)) { - + return Response.AsJson(new JsonResponseModel { Message = "The passwords do not match", Result = false }); } - var result = UserMapper.UpdateUser(username, oldPass, newPassword); - return View["ChangePassword"]; + var result = UserMapper.UpdatePassword(username, oldPass, newPassword); + if (result) + { + return Response.AsJson(new JsonResponseModel { Message = "Password has been changed!", Result = true }); + } + + return Response.AsJson(new JsonResponseModel { Message = "Could not update the password in the database", Result = false }); } } } \ No newline at end of file diff --git a/PlexRequests.UI/Views/Login/ChangePassword.cshtml b/PlexRequests.UI/Views/Login/ChangePassword.cshtml index f87dfa76e..73099f67b 100644 --- a/PlexRequests.UI/Views/Login/ChangePassword.cshtml +++ b/PlexRequests.UI/Views/Login/ChangePassword.cshtml @@ -1,9 +1,37 @@ -
    +
    Old Password New Password New Password again

    - +
    + + \ No newline at end of file diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index b96e89b20..440f12a10 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -72,9 +72,9 @@ } From 7be244c31004063d68e96c4860e5da777c5ec64c Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 24 Mar 2016 14:12:50 +0000 Subject: [PATCH 25/34] fixed the build --- PlexRequests.Core/UserMapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PlexRequests.Core/UserMapper.cs b/PlexRequests.Core/UserMapper.cs index 211145069..a846652c5 100644 --- a/PlexRequests.Core/UserMapper.cs +++ b/PlexRequests.Core/UserMapper.cs @@ -102,8 +102,8 @@ namespace PlexRequests.Core UserGuid = Guid.NewGuid().ToString(), Salt = salt, Hash = PasswordHasher.ComputeHash(password, salt), - Claims = JsonRequestService.ReturnBytes(claims), - UserProperties = JsonRequestService.ReturnBytes(new UserProperties()) + Claims = ByteConverterHelper.ReturnBytes(claims), + UserProperties = ByteConverterHelper.ReturnBytes(new UserProperties()) }; repo.Insert(userModel); From 840deb61610ed6f5b198633e12e5543f4df5792a Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 24 Mar 2016 14:26:16 +0000 Subject: [PATCH 26/34] Updated the readme and added some icons to the navbar --- PlexRequests.UI/Views/Shared/_Layout.cshtml | 22 ++++++++++----------- README.md | 3 +++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/PlexRequests.UI/Views/Shared/_Layout.cshtml b/PlexRequests.UI/Views/Shared/_Layout.cshtml index 440f12a10..5245bdbdd 100644 --- a/PlexRequests.UI/Views/Shared/_Layout.cshtml +++ b/PlexRequests.UI/Views/Shared/_Layout.cshtml @@ -34,28 +34,28 @@ - Plex Requests + Plex Requests
    diff --git a/README.md b/README.md index 770bf7eef..8815a5a07 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ I wanted to write a similar application in .Net! * Secure authentication * [Sonarr](https://sonarr.tv/) integration (SickRage/Sickbeard TBD) * [CouchPotato](https://couchpota.to/) integration +* [SickRage](https://sickrage.github.io/) integration * Email notifications +* Pushbullet notifications +* Pushover notifications #Preview From 15f7572cf576dbf54a6418b194523eb037bb9ede Mon Sep 17 00:00:00 2001 From: Shannon Barrett Date: Thu, 24 Mar 2016 12:03:59 -0500 Subject: [PATCH 27/34] Updated the logic for handling specific seasons in Sonarr and Sickrage --- PlexRequests.Api.Interfaces/ISickRageApi.cs | 2 +- PlexRequests.Api.Interfaces/ISonarrApi.cs | 2 +- .../PlexRequests.Api.Models.csproj | 1 + PlexRequests.Api.Models/Tv/TVMazeShow.cs | 1 + PlexRequests.Api.Models/Tv/TvMazeSeasons.cs | 13 ++++ PlexRequests.Api/Mocks/MockSonarrApi.cs | 2 +- PlexRequests.Api/SickrageApi.cs | 11 ++-- PlexRequests.Api/SonarrApi.cs | 63 +++++++++---------- PlexRequests.Api/TvMazeApi.cs | 22 ++++++- PlexRequests.Core/Setup.cs | 1 - PlexRequests.Store/RequestedModel.cs | 2 +- PlexRequests.UI/Helpers/TvSender.cs | 4 +- PlexRequests.UI/Modules/SearchModule.cs | 45 +++++++------ 13 files changed, 101 insertions(+), 68 deletions(-) create mode 100644 PlexRequests.Api.Models/Tv/TvMazeSeasons.cs diff --git a/PlexRequests.Api.Interfaces/ISickRageApi.cs b/PlexRequests.Api.Interfaces/ISickRageApi.cs index 038caa26e..a5770b56f 100644 --- a/PlexRequests.Api.Interfaces/ISickRageApi.cs +++ b/PlexRequests.Api.Interfaces/ISickRageApi.cs @@ -32,7 +32,7 @@ namespace PlexRequests.Api.Interfaces { public interface ISickRageApi { - SickRageTvAdd AddSeries(int tvdbId, bool latest, int[] seasons, string quality, string apiKey, + SickRageTvAdd AddSeries(int tvdbId, int seasoncount, int[] seasons, string quality, string apiKey, Uri baseUrl); SickRagePing Ping(string apiKey, Uri baseUrl); diff --git a/PlexRequests.Api.Interfaces/ISonarrApi.cs b/PlexRequests.Api.Interfaces/ISonarrApi.cs index 7939cd21d..e4dce0c3a 100644 --- a/PlexRequests.Api.Interfaces/ISonarrApi.cs +++ b/PlexRequests.Api.Interfaces/ISonarrApi.cs @@ -36,7 +36,7 @@ namespace PlexRequests.Api.Interfaces List GetProfiles(string apiKey, Uri baseUrl); SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, - bool episodes, int[] seasons, string apiKey, Uri baseUrl); + int seasonCount, int[] seasons, string apiKey, Uri baseUrl); SystemStatus SystemStatus(string apiKey, Uri baseUrl); } diff --git a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj index bd3151c55..1078563fe 100644 --- a/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj +++ b/PlexRequests.Api.Models/PlexRequests.Api.Models.csproj @@ -66,6 +66,7 @@ + diff --git a/PlexRequests.Api.Models/Tv/TVMazeShow.cs b/PlexRequests.Api.Models/Tv/TVMazeShow.cs index 165658602..faebd6d84 100644 --- a/PlexRequests.Api.Models/Tv/TVMazeShow.cs +++ b/PlexRequests.Api.Models/Tv/TVMazeShow.cs @@ -23,5 +23,6 @@ namespace PlexRequests.Api.Models.Tv public string summary { get; set; } public int updated { get; set; } public Links _links { get; set; } + public int seasonCount { get; set; } } } \ No newline at end of file diff --git a/PlexRequests.Api.Models/Tv/TvMazeSeasons.cs b/PlexRequests.Api.Models/Tv/TvMazeSeasons.cs new file mode 100644 index 000000000..bce31cea5 --- /dev/null +++ b/PlexRequests.Api.Models/Tv/TvMazeSeasons.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PlexRequests.Api.Models.Tv +{ + public class TvMazeSeasons : TvMazeShow + { + public int number { get; set; } + } +} diff --git a/PlexRequests.Api/Mocks/MockSonarrApi.cs b/PlexRequests.Api/Mocks/MockSonarrApi.cs index 1694c3069..dfb816f8a 100644 --- a/PlexRequests.Api/Mocks/MockSonarrApi.cs +++ b/PlexRequests.Api/Mocks/MockSonarrApi.cs @@ -43,7 +43,7 @@ namespace PlexRequests.Api.Mocks return obj; } - public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, bool episodes, int[] seasons, + public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl) { var json = MockApiData.Sonarr_AddSeriesResult; diff --git a/PlexRequests.Api/SickrageApi.cs b/PlexRequests.Api/SickrageApi.cs index c589f059c..fe27968fe 100644 --- a/PlexRequests.Api/SickrageApi.cs +++ b/PlexRequests.Api/SickrageApi.cs @@ -28,6 +28,7 @@ #endregion using System; +using System.Linq; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.SickRage; @@ -48,13 +49,11 @@ namespace PlexRequests.Api private ApiRequest Api { get; } - public SickRageTvAdd AddSeries(int tvdbId, bool latest, int[] seasons, string quality, string apiKey, + public SickRageTvAdd AddSeries(int tvdbId, int seasonCount, int[] seasons, string quality, string apiKey, Uri baseUrl) { - string status; - var futureStatus = SickRageStatus.Wanted; - - status = latest || seasons.Length > 0 ? SickRageStatus.Skipped : SickRageStatus.Wanted; + var futureStatus = seasons.Length > 0 && !seasons.Any(x => x == seasonCount) ? SickRageStatus.Skipped : SickRageStatus.Wanted; + var status = seasons.Length > 0 ? SickRageStatus.Skipped : SickRageStatus.Wanted; var request = new RestRequest { @@ -72,7 +71,7 @@ namespace PlexRequests.Api var obj = Api.Execute(request, baseUrl); - if (!latest && seasons.Length > 0 && obj.result != "failure") + if (seasons.Length > 0 && obj.result != "failure") { //handle the seasons requested foreach (int s in seasons) diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index 8735060e4..166b41497 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -26,7 +26,7 @@ #endregion using System; using System.Collections.Generic; - +using System.Linq; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Sonarr; @@ -54,7 +54,7 @@ namespace PlexRequests.Api return obj; } - public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, bool episodes, int[] seasons, string apiKey, Uri baseUrl) + public SonarrAddSeries AddSeries(int tvdbId, string title, int qualityId, bool seasonFolders, string rootPath, int seasonCount, int[] seasons, string apiKey, Uri baseUrl) { var request = new RestRequest @@ -64,27 +64,28 @@ namespace PlexRequests.Api }; var options = new SonarrAddSeries(); - if (seasons.Length == 0) - { - if (episodes) - { - options.addOptions = new AddOptions - { - ignoreEpisodesWithFiles = true, - ignoreEpisodesWithoutFiles = true, - searchForMissingEpisodes = false - }; - } - else - { - options.addOptions = new AddOptions - { - ignoreEpisodesWithFiles = false, - searchForMissingEpisodes = true, - ignoreEpisodesWithoutFiles = false - }; - } - } + + + //I'm fairly certain we won't need this logic anymore since we're manually adding the seasons + //if (seasons.Length == 0) + //{ + // options.addOptions = new AddOptions + // { + // ignoreEpisodesWithFiles = true, + // ignoreEpisodesWithoutFiles = true, + // searchForMissingEpisodes = false + // }; + //} + //else + //{ + // options.addOptions = new AddOptions + // { + // ignoreEpisodesWithFiles = false, + // ignoreEpisodesWithoutFiles = false, + // searchForMissingEpisodes = true + // }; + //} + options.seasonFolder = seasonFolders; options.title = title; options.qualityProfileId = qualityId; @@ -93,20 +94,16 @@ namespace PlexRequests.Api options.seasons = new List(); options.rootFolderPath = rootPath; - if (seasons.Length > 0) + for (var i = 1; i <= seasonCount; i++) { - foreach (int s in seasons) + var season = new Season { - var season = new Season - { - seasonNumber = s, - monitored = true - }; - options.seasons.Add(season); - } + seasonNumber = i, + monitored = seasons.Length == 0 || seasons.Any(x => x == i) + }; + options.seasons.Add(season); } - request.AddHeader("X-Api-Key", apiKey); request.AddJsonBody(options); diff --git a/PlexRequests.Api/TvMazeApi.cs b/PlexRequests.Api/TvMazeApi.cs index 8b6cb9f37..6043710d7 100644 --- a/PlexRequests.Api/TvMazeApi.cs +++ b/PlexRequests.Api/TvMazeApi.cs @@ -26,7 +26,7 @@ #endregion using System; using System.Collections.Generic; - +using System.Linq; using NLog; using PlexRequests.Api.Models.Tv; @@ -79,7 +79,25 @@ namespace PlexRequests.Api request.AddUrlSegment("id", theTvDbId.ToString()); request.AddHeader("Content-Type", "application/json"); - return Api.Execute(request, new Uri(Uri)); + var obj = Api.Execute(request, new Uri(Uri)); + obj.seasonCount = GetSeasonCount(obj.id); + + return obj; + } + + public int GetSeasonCount(int id) + { + var request = new RestRequest + { + Method = Method.GET, + Resource = "shows/{id}/seasons" + }; + request.AddUrlSegment("id", id.ToString()); + request.AddHeader("Content-Type", "application/json"); + + var obj = Api.Execute>(request, new Uri(Uri)); + var seasons = obj.Select(x => x.number > 0); + return seasons.Count(); } } diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index 04ce5b9db..63afee327 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -110,7 +110,6 @@ namespace PlexRequests.Core Available = r.Available, ImdbId = show.externals.imdb, Issues = r.Issues, - LatestTv = r.LatestTv, OtherMessage = r.OtherMessage, Overview = show.summary.RemoveHtml(), RequestedBy = r.RequestedBy, diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index aaed042b1..5f746b841 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -23,9 +23,9 @@ namespace PlexRequests.Store public bool Available { get; set; } public IssueState Issues { get; set; } public string OtherMessage { get; set; } - public bool LatestTv { get; set; } public string AdminNote { get; set; } public int[] SeasonList { get; set; } + public int SeasonCount { get; set; } } public enum RequestType diff --git a/PlexRequests.UI/Helpers/TvSender.cs b/PlexRequests.UI/Helpers/TvSender.cs index f43012d36..6c2f59ace 100644 --- a/PlexRequests.UI/Helpers/TvSender.cs +++ b/PlexRequests.UI/Helpers/TvSender.cs @@ -54,7 +54,7 @@ namespace PlexRequests.UI.Helpers int qualityProfile; int.TryParse(sonarrSettings.QualityProfile, out qualityProfile); var result = SonarrApi.AddSeries(model.ProviderId, model.Title, qualityProfile, - sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.LatestTv, model.SeasonList, sonarrSettings.ApiKey, + sonarrSettings.SeasonFolders, sonarrSettings.RootPath, model.SeasonCount, model.SeasonList, sonarrSettings.ApiKey, sonarrSettings.FullUri); Log.Trace("Sonarr Add Result: "); @@ -65,7 +65,7 @@ namespace PlexRequests.UI.Helpers public SickRageTvAdd SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) { - var result = SickrageApi.AddSeries(model.ProviderId, model.LatestTv, model.SeasonList, sickRageSettings.QualityProfile, + var result = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, sickRageSettings.QualityProfile, sickRageSettings.ApiKey, sickRageSettings.FullUri); Log.Trace("SickRage Add Result: "); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index a9a5fd446..e1ee9e8b1 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -285,7 +285,6 @@ namespace PlexRequests.UI.Modules DateTime firstAir; DateTime.TryParse(showInfo.premiered, out firstAir); - var latest = seasons == "latest"; var model = new RequestedModel { ProviderId = showInfo.externals?.thetvdb ?? 0, @@ -299,14 +298,20 @@ namespace PlexRequests.UI.Modules Approved = false, RequestedBy = Session[SessionKeys.UsernameKey].ToString(), Issues = IssueState.None, - LatestTv = latest, - ImdbId = showInfo.externals?.imdb ?? string.Empty + ImdbId = showInfo.externals?.imdb ?? string.Empty, + SeasonCount = showInfo.seasonCount }; var seasonsList = new List(); - if (seasons == "first") + switch (seasons) { - seasonsList.Add(1); + case "first": + seasonsList.Add(1); + break; + case "latest": + seasonsList.Add(model.SeasonCount); + break; } + model.SeasonList = seasonsList.ToArray(); var settings = PrService.GetSettings(); @@ -360,23 +365,23 @@ namespace PlexRequests.UI.Modules return result; } - private Response SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) - { - var result = SickrageApi.AddSeries(model.ProviderId, model.LatestTv, model.SeasonList, sickRageSettings.QualityProfile, - sickRageSettings.ApiKey, sickRageSettings.FullUri); + //private Response SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) + //{ + // var result = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, sickRageSettings.QualityProfile, + // sickRageSettings.ApiKey, sickRageSettings.FullUri); - Log.Trace("SickRage Result: "); - Log.Trace(result.DumpJson()); + // Log.Trace("SickRage Result: "); + // Log.Trace(result.DumpJson()); - if (result?.result == "success") - { - model.Approved = true; - Log.Debug("Adding tv to database requests (No approval required & SickRage)"); - RequestService.AddRequest(model); + // if (result?.result == "success") + // { + // model.Approved = true; + // Log.Debug("Adding tv to database requests (No approval required & SickRage)"); + // RequestService.AddRequest(model); - return Response.AsJson(new JsonResponseModel { Result = true }); - } - return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to SickRage! Please check your settings." }); - } + // return Response.AsJson(new JsonResponseModel { Result = true }); + // } + // return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to SickRage! Please check your settings." }); + //} } } \ No newline at end of file From 3fe1f13bd16d2732e22003d8f7ea3989e31e1aa5 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 24 Mar 2016 17:14:45 +0000 Subject: [PATCH 28/34] Refactored the Notification service to how it should have really been done in the first place. --- PlexRequests.Api.Interfaces/IPushbulletApi.cs | 4 +- PlexRequests.Api.Interfaces/IPushoverApi.cs | 3 +- PlexRequests.Api/PushbulletApi.cs | 5 +- PlexRequests.Api/PushoverApi.cs | 7 +- .../NotificationServiceTests.cs | 91 +++++++++++-------- .../INotification.cs | 20 ++-- .../Interfaces/INotificationService.cs | 40 ++++++++ .../Notification/EmailMessageNotification.cs | 44 ++++----- .../Notification/NotificationService.cs | 59 +++++------- .../Notification/PushbulletNotification.cs | 38 ++++---- .../Notification/PushoverNotification.cs | 38 ++++---- .../PlexRequests.Services.csproj | 3 +- PlexRequests.UI.Tests/AdminModuleTests.cs | 4 + PlexRequests.UI/Bootstrapper.cs | 11 ++- PlexRequests.UI/Modules/AdminModule.cs | 7 +- PlexRequests.UI/Modules/RequestsModule.cs | 7 +- PlexRequests.UI/Modules/SearchModule.cs | 5 +- 17 files changed, 220 insertions(+), 166 deletions(-) rename PlexRequests.Services/{Notification => Interfaces}/INotification.cs (75%) create mode 100644 PlexRequests.Services/Interfaces/INotificationService.cs diff --git a/PlexRequests.Api.Interfaces/IPushbulletApi.cs b/PlexRequests.Api.Interfaces/IPushbulletApi.cs index 647280aaf..5df902d4c 100644 --- a/PlexRequests.Api.Interfaces/IPushbulletApi.cs +++ b/PlexRequests.Api.Interfaces/IPushbulletApi.cs @@ -24,6 +24,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion +using System.Threading.Tasks; + using PlexRequests.Api.Models.Notifications; namespace PlexRequests.Api.Interfaces @@ -38,6 +40,6 @@ namespace PlexRequests.Api.Interfaces /// The message. /// The device identifier. /// - PushbulletResponse Push(string accessToken, string title, string message, string deviceIdentifier = default(string)); + Task PushAsync(string accessToken, string title, string message, string deviceIdentifier = default(string)); } } \ No newline at end of file diff --git a/PlexRequests.Api.Interfaces/IPushoverApi.cs b/PlexRequests.Api.Interfaces/IPushoverApi.cs index 42e3f2217..15f93f596 100644 --- a/PlexRequests.Api.Interfaces/IPushoverApi.cs +++ b/PlexRequests.Api.Interfaces/IPushoverApi.cs @@ -24,6 +24,7 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion +using System.Threading.Tasks; using PlexRequests.Api.Models.Notifications; @@ -31,6 +32,6 @@ namespace PlexRequests.Api.Interfaces { public interface IPushoverApi { - PushoverResponse Push(string accessToken, string message, string userToken); + Task PushAsync(string accessToken, string message, string userToken); } } \ No newline at end of file diff --git a/PlexRequests.Api/PushbulletApi.cs b/PlexRequests.Api/PushbulletApi.cs index 2c542348d..1e399e048 100644 --- a/PlexRequests.Api/PushbulletApi.cs +++ b/PlexRequests.Api/PushbulletApi.cs @@ -25,6 +25,7 @@ // ************************************************************************/ #endregion using System; +using System.Threading.Tasks; using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Notifications; @@ -35,7 +36,7 @@ namespace PlexRequests.Api { public class PushbulletApi : IPushbulletApi { - public PushbulletResponse Push(string accessToken, string title, string message, string deviceIdentifier = default(string)) + public async Task PushAsync(string accessToken, string title, string message, string deviceIdentifier = default(string)) { var request = new RestRequest { @@ -56,7 +57,7 @@ namespace PlexRequests.Api request.AddJsonBody(push); var api = new ApiRequest(); - return api.ExecuteJson(request, new Uri("https://api.pushbullet.com/v2/pushes")); + return await Task.Run(() => api.ExecuteJson(request, new Uri("https://api.pushbullet.com/v2/pushes"))); } } } diff --git a/PlexRequests.Api/PushoverApi.cs b/PlexRequests.Api/PushoverApi.cs index 1de5694aa..6d109ca9b 100644 --- a/PlexRequests.Api/PushoverApi.cs +++ b/PlexRequests.Api/PushoverApi.cs @@ -25,7 +25,8 @@ // ************************************************************************/ #endregion using System; -using Nancy.Helpers; +using System.Threading.Tasks; + using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Notifications; @@ -35,7 +36,7 @@ namespace PlexRequests.Api { public class PushoverApi : IPushoverApi { - public PushoverResponse Push(string accessToken, string message, string userToken) + public async Task PushAsync(string accessToken, string message, string userToken) { var request = new RestRequest { @@ -49,7 +50,7 @@ namespace PlexRequests.Api var api = new ApiRequest(); - return api.ExecuteJson(request, new Uri("https://api.pushover.net/1")); + return await Task.Run(() => api.ExecuteJson(request, new Uri("https://api.pushover.net/1"))); } } } diff --git a/PlexRequests.Services.Tests/NotificationServiceTests.cs b/PlexRequests.Services.Tests/NotificationServiceTests.cs index 877ddcbbf..940bcedda 100644 --- a/PlexRequests.Services.Tests/NotificationServiceTests.cs +++ b/PlexRequests.Services.Tests/NotificationServiceTests.cs @@ -1,33 +1,37 @@ #region Copyright -// /************************************************************************ -// Copyright (c) 2016 Jamie Rees -// File: NotificationServiceTests.cs -// Created By: Jamie Rees -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// ************************************************************************/ +/************************************************************************ + Copyright (c) 2016 Jamie Rees + File: NotificationServiceTests.cs + Created By: Jamie Rees + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ************************************************************************/ #endregion +using System; +using System.Threading.Tasks; + using Moq; using NUnit.Framework; +using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; namespace PlexRequests.Services.Tests @@ -35,9 +39,15 @@ namespace PlexRequests.Services.Tests [TestFixture] public class NotificationServiceTests { + public NotificationService NotificationService { get; set; } + + [SetUp] + public void Setup() + { + NotificationService = new NotificationService(); + } [Test] - [Ignore("Need to rework due to static class")] public void SubscribeNewNotifier() { var notificationMock = new Mock(); @@ -49,7 +59,6 @@ namespace PlexRequests.Services.Tests } [Test] - [Ignore("Need to rework due to static class")] public void SubscribeExistingNotifier() { var notificationMock1 = new Mock(); @@ -68,7 +77,6 @@ namespace PlexRequests.Services.Tests } [Test] - [Ignore("Need to rework due to static class")] public void UnSubscribeMissingNotifier() { var notificationMock = new Mock(); @@ -79,7 +87,6 @@ namespace PlexRequests.Services.Tests } [Test] - [Ignore("Need to rework due to static class")] public void UnSubscribeNotifier() { var notificationMock = new Mock(); @@ -92,17 +99,15 @@ namespace PlexRequests.Services.Tests } [Test] - [Ignore("Need to rework due to static class")] public void PublishWithNoObservers() { - Assert.DoesNotThrow( - () => - { NotificationService.Publish(new NotificationModel()); }); + Assert.DoesNotThrowAsync( + async() => + { await NotificationService.Publish(new NotificationModel()); }); } [Test] - [Ignore("Need to rework due to static class")] - public void PublishAllNotifiers() + public async Task PublishAllNotifiers() { var notificationMock1 = new Mock(); var notificationMock2 = new Mock(); @@ -112,11 +117,21 @@ namespace PlexRequests.Services.Tests NotificationService.Subscribe(notificationMock2.Object); Assert.That(NotificationService.Observers.Count, Is.EqualTo(2)); - var model = new NotificationModel {Title = "abc", Body = "test"}; - NotificationService.Publish(model); + var model = new NotificationModel { Title = "abc", Body = "test" }; + await NotificationService.Publish(model); - notificationMock1.Verify(x => x.Notify(model), Times.Once); - notificationMock2.Verify(x => x.Notify(model), Times.Once); + notificationMock1.Verify(x => x.NotifyAsync(model), Times.Once); + notificationMock2.Verify(x => x.NotifyAsync(model), Times.Once); + } + + [Test] + public async Task PublishWithException() + { + var notificationMock = new Mock(); + notificationMock.Setup(x => x.NotifyAsync(It.IsAny())).Throws(); + notificationMock.SetupGet(x => x.NotificationName).Returns("Notification1"); + NotificationService.Subscribe(notificationMock.Object); + await NotificationService.Publish(new NotificationModel()); } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/INotification.cs b/PlexRequests.Services/Interfaces/INotification.cs similarity index 75% rename from PlexRequests.Services/Notification/INotification.cs rename to PlexRequests.Services/Interfaces/INotification.cs index dd099747d..14b09f0e9 100644 --- a/PlexRequests.Services/Notification/INotification.cs +++ b/PlexRequests.Services/Interfaces/INotification.cs @@ -24,22 +24,16 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion -namespace PlexRequests.Services.Notification +using System.Threading.Tasks; + +using PlexRequests.Services.Notification; + +namespace PlexRequests.Services.Interfaces { public interface INotification { - /// - /// Gets the name of the notification. - /// - /// - /// The name of the notification. - /// string NotificationName { get; } - /// - /// Notifies the specified title. - /// - /// The model. - /// - bool Notify(NotificationModel model); + + Task NotifyAsync(NotificationModel model); } } \ No newline at end of file diff --git a/PlexRequests.Services/Interfaces/INotificationService.cs b/PlexRequests.Services/Interfaces/INotificationService.cs new file mode 100644 index 000000000..59db3b509 --- /dev/null +++ b/PlexRequests.Services/Interfaces/INotificationService.cs @@ -0,0 +1,40 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: INotificationService.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +using System.Threading.Tasks; + +using PlexRequests.Services.Notification; + +namespace PlexRequests.Services.Interfaces +{ + public interface INotificationService + { + Task Publish(NotificationModel model); + void Subscribe(INotification notification); + void UnSubscribe(INotification notification); + + } +} \ No newline at end of file diff --git a/PlexRequests.Services/Notification/EmailMessageNotification.cs b/PlexRequests.Services/Notification/EmailMessageNotification.cs index db6bc24b2..472b6a069 100644 --- a/PlexRequests.Services/Notification/EmailMessageNotification.cs +++ b/PlexRequests.Services/Notification/EmailMessageNotification.cs @@ -27,11 +27,13 @@ using System; using System.Net; using System.Net.Mail; +using System.Threading.Tasks; using NLog; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; namespace PlexRequests.Services.Notification { @@ -47,31 +49,35 @@ namespace PlexRequests.Services.Notification private EmailNotificationSettings Settings => GetConfiguration(); public string NotificationName => "EmailMessageNotification"; - public bool Notify(NotificationModel model) + public async Task NotifyAsync(NotificationModel model) { var configuration = GetConfiguration(); if (!ValidateConfiguration(configuration)) { - return false; + return; } switch (model.NotificationType) { case NotificationType.NewRequest: - return EmailNewRequest(model); + await EmailNewRequest(model); + break; case NotificationType.Issue: - return EmailIssue(model); + await EmailIssue(model); + break; case NotificationType.RequestAvailable: - break; + throw new NotImplementedException(); + case NotificationType.RequestApproved: - break; + throw new NotImplementedException(); + case NotificationType.AdminNote: - break; + throw new NotImplementedException(); + default: throw new ArgumentOutOfRangeException(); } - - return false; + } private EmailNotificationSettings GetConfiguration() @@ -94,7 +100,7 @@ namespace PlexRequests.Services.Notification return true; } - private bool EmailNewRequest(NotificationModel model) + private async Task EmailNewRequest(NotificationModel model) { var message = new MailMessage { @@ -111,22 +117,20 @@ namespace PlexRequests.Services.Notification { smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); smtp.EnableSsl = Settings.Ssl; - smtp.Send(message); - return true; + await smtp.SendMailAsync(message).ConfigureAwait(false); } } catch (SmtpException smtp) { - Log.Fatal(smtp); + Log.Error(smtp); } catch (Exception e) { - Log.Fatal(e); + Log.Error(e); } - return false; } - private bool EmailIssue(NotificationModel model) + private async Task EmailIssue(NotificationModel model) { var message = new MailMessage { @@ -143,19 +147,17 @@ namespace PlexRequests.Services.Notification { smtp.Credentials = new NetworkCredential(Settings.EmailUsername, Settings.EmailPassword); smtp.EnableSsl = Settings.Ssl; - smtp.Send(message); - return true; + await smtp.SendMailAsync(message).ConfigureAwait(false); } } catch (SmtpException smtp) { - Log.Fatal(smtp); + Log.Error(smtp); } catch (Exception e) { - Log.Fatal(e); + Log.Error(e); } - return false; } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/NotificationService.cs b/PlexRequests.Services/Notification/NotificationService.cs index 817dcd2d9..116f5aef9 100644 --- a/PlexRequests.Services/Notification/NotificationService.cs +++ b/PlexRequests.Services/Notification/NotificationService.cs @@ -24,64 +24,49 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // ************************************************************************/ #endregion -using System.Collections.Generic; -using System.Threading; +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading.Tasks; using NLog; -using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; namespace PlexRequests.Services.Notification { - public static class NotificationService + public class NotificationService : INotificationService { - private static Logger Log = LogManager.GetCurrentClassLogger(); - public static Dictionary Observers { get; } + public ConcurrentDictionary Observers { get; } = new ConcurrentDictionary(); - static NotificationService() + public async Task Publish(NotificationModel model) { - Observers = new Dictionary(); + var notificationTasks = Observers.Values.Select(notification => NotifyAsync(notification, model)); + + await Task.WhenAll(notificationTasks).ConfigureAwait(false); } - public static void Publish(NotificationModel model) + public void Subscribe(INotification notification) { - Log.Trace("Notifying all observers: "); - Log.Trace(Observers.DumpJson()); - foreach (var observer in Observers) - { - var notification = observer.Value; - - new Thread(() => - { - Thread.CurrentThread.IsBackground = true; - notification.Notify(model); - }).Start(); - } + Observers.TryAdd(notification.NotificationName, notification); } - public static void Subscribe(INotification notification) + public void UnSubscribe(INotification notification) { - INotification notificationValue; - if (Observers.TryGetValue(notification.NotificationName, out notificationValue)) - { - return; - } - - Observers[notification.NotificationName] = notification; + Observers.TryRemove(notification.NotificationName, out notification); } - public static void UnSubscribe(INotification notification) + private static async Task NotifyAsync(INotification notification, NotificationModel model) { - Log.Trace("Unsubscribing Observer {0}", notification.NotificationName); - INotification notificationValue; - if (!Observers.TryGetValue(notification.NotificationName, out notificationValue)) + try { - Log.Trace("Observer {0} doesn't exist to Unsubscribe", notification.NotificationName); - // Observer doesn't exists - return; + await notification.NotifyAsync(model).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Error(ex, $"Notification '{notification.NotificationName}' failed with exception"); } - Observers.Remove(notification.NotificationName); } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/PushbulletNotification.cs b/PlexRequests.Services/Notification/PushbulletNotification.cs index 20c4b9f35..4e2f02144 100644 --- a/PlexRequests.Services/Notification/PushbulletNotification.cs +++ b/PlexRequests.Services/Notification/PushbulletNotification.cs @@ -25,12 +25,14 @@ // ************************************************************************/ #endregion using System; +using System.Threading.Tasks; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; namespace PlexRequests.Services.Notification { @@ -41,27 +43,27 @@ namespace PlexRequests.Services.Notification PushbulletApi = pushbulletApi; SettingsService = settings; } - private IPushbulletApi PushbulletApi { get; } + private IPushbulletApi PushbulletApi { get; } private ISettingsService SettingsService { get; } private PushbulletNotificationSettings Settings => GetSettings(); private static Logger Log = LogManager.GetCurrentClassLogger(); public string NotificationName => "PushbulletNotification"; - public bool Notify(NotificationModel model) + public async Task NotifyAsync(NotificationModel model) { if (!ValidateConfiguration()) { - return false; + return; } switch (model.NotificationType) { case NotificationType.NewRequest: - return PushNewRequest(model); - + await PushNewRequestAsync(model); + break; case NotificationType.Issue: - return PushIssue(model); - + await PushIssueAsync(model); + break; case NotificationType.RequestAvailable: break; case NotificationType.RequestApproved: @@ -71,8 +73,6 @@ namespace PlexRequests.Services.Notification default: throw new ArgumentOutOfRangeException(); } - return false; - } private bool ValidateConfiguration() @@ -93,42 +93,40 @@ namespace PlexRequests.Services.Notification return SettingsService.GetSettings(); } - private bool PushNewRequest(NotificationModel model) + private async Task PushNewRequestAsync(NotificationModel model) { var message = $"{model.Title} has been requested by user: {model.User}"; var pushTitle = $"Plex Requests: {model.Title} has been requested!"; try { - var result = PushbulletApi.Push(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); - if (result != null) + var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); + if (result == null) { - return true; + Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); } } catch (Exception e) { - Log.Fatal(e); + Log.Error(e); } - return false; } - private bool PushIssue(NotificationModel model) + private async Task PushIssueAsync(NotificationModel model) { var message = $"A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; var pushTitle = $"Plex Requests: A new issue has been reported for {model.Title}"; try { - var result = PushbulletApi.Push(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); + var result = await PushbulletApi.PushAsync(Settings.AccessToken, pushTitle, message, Settings.DeviceIdentifier); if (result != null) { - return true; + Log.Error("Pushbullet api returned a null value, the notification did not get pushed"); } } catch (Exception e) { - Log.Fatal(e); + Log.Error(e); } - return false; } } } \ No newline at end of file diff --git a/PlexRequests.Services/Notification/PushoverNotification.cs b/PlexRequests.Services/Notification/PushoverNotification.cs index 2c3bf6bfe..bca0b2c90 100644 --- a/PlexRequests.Services/Notification/PushoverNotification.cs +++ b/PlexRequests.Services/Notification/PushoverNotification.cs @@ -25,12 +25,14 @@ // ************************************************************************/ #endregion using System; +using System.Threading.Tasks; using NLog; using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; namespace PlexRequests.Services.Notification { @@ -47,21 +49,21 @@ namespace PlexRequests.Services.Notification private static Logger Log = LogManager.GetCurrentClassLogger(); public string NotificationName => "PushoverNotification"; - public bool Notify(NotificationModel model) + public async Task NotifyAsync(NotificationModel model) { if (!ValidateConfiguration()) { - return false; + return; } switch (model.NotificationType) { case NotificationType.NewRequest: - return PushNewRequest(model); - + await PushNewRequestAsync(model); + break; case NotificationType.Issue: - return PushIssue(model); - + await PushIssueAsync(model); + break; case NotificationType.RequestAvailable: break; case NotificationType.RequestApproved: @@ -71,8 +73,6 @@ namespace PlexRequests.Services.Notification default: throw new ArgumentOutOfRangeException(); } - return false; - } private bool ValidateConfiguration() @@ -93,40 +93,38 @@ namespace PlexRequests.Services.Notification return SettingsService.GetSettings(); } - private bool PushNewRequest(NotificationModel model) + private async Task PushNewRequestAsync(NotificationModel model) { var message = $"Plex Requests: {model.Title} has been requested by user: {model.User}"; try { - var result = PushoverApi.Push(Settings.AccessToken, message, Settings.UserToken); - if (result?.status == 1) + var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken); + if (result?.status != 1) { - return true; + Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); } } catch (Exception e) { - Log.Fatal(e); + Log.Error(e); } - return false; } - private bool PushIssue(NotificationModel model) + private async Task PushIssueAsync(NotificationModel model) { var message = $"Plex Requests: A new issue: {model.Body} has been reported by user: {model.User} for the title: {model.Title}"; try { - var result = PushoverApi.Push(Settings.AccessToken, message, Settings.UserToken); - if (result != null) + var result = await PushoverApi.PushAsync(Settings.AccessToken, message, Settings.UserToken); + if (result?.status != 1) { - return true; + Log.Error("Pushover api returned a status that was not 1, the notification did not get pushed"); } } catch (Exception e) { - Log.Fatal(e); + Log.Error(e); } - return false; } } } \ No newline at end of file diff --git a/PlexRequests.Services/PlexRequests.Services.csproj b/PlexRequests.Services/PlexRequests.Services.csproj index e1ba81532..bbb334ca9 100644 --- a/PlexRequests.Services/PlexRequests.Services.csproj +++ b/PlexRequests.Services/PlexRequests.Services.csproj @@ -77,7 +77,8 @@ - + + diff --git a/PlexRequests.UI.Tests/AdminModuleTests.cs b/PlexRequests.UI.Tests/AdminModuleTests.cs index 137e49d0b..fc8686086 100644 --- a/PlexRequests.UI.Tests/AdminModuleTests.cs +++ b/PlexRequests.UI.Tests/AdminModuleTests.cs @@ -39,6 +39,7 @@ using PlexRequests.Api.Interfaces; using PlexRequests.Api.Models.Plex; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; using PlexRequests.UI.Models; @@ -64,6 +65,7 @@ namespace PlexRequests.UI.Tests private Mock PushoverApi { get; set; } private Mock CpApi { get; set; } private Mock> LogRepo { get; set; } + private Mock NotificationService { get; set; } private ConfigurableBootstrapper Bootstrapper { get; set; } @@ -91,6 +93,7 @@ namespace PlexRequests.UI.Tests LogRepo = new Mock>(); PushoverSettings = new Mock>(); PushoverApi = new Mock(); + NotificationService = new Mock(); Bootstrapper = new ConfigurableBootstrapper(with => { @@ -110,6 +113,7 @@ namespace PlexRequests.UI.Tests with.Dependency(LogRepo.Object); with.Dependency(PushoverSettings.Object); with.Dependency(PushoverApi.Object); + with.Dependency(NotificationService.Object); with.RootPathProvider(); with.RequestStartup((container, pipelines, context) => { diff --git a/PlexRequests.UI/Bootstrapper.cs b/PlexRequests.UI/Bootstrapper.cs index 46d68c797..5e4f24343 100644 --- a/PlexRequests.UI/Bootstrapper.cs +++ b/PlexRequests.UI/Bootstrapper.cs @@ -96,6 +96,9 @@ namespace PlexRequests.UI container.Register(); container.Register(); + // NotificationService + container.Register().AsSingleton(); + SubscribeAllObservers(container); base.ConfigureRequestContainer(container, context); } @@ -130,25 +133,27 @@ namespace PlexRequests.UI private void SubscribeAllObservers(TinyIoCContainer container) { + var notificationService = container.Resolve(); + var emailSettingsService = container.Resolve>(); var emailSettings = emailSettingsService.GetSettings(); if (emailSettings.Enabled) { - NotificationService.Subscribe(new EmailMessageNotification(emailSettingsService)); + notificationService.Subscribe(new EmailMessageNotification(emailSettingsService)); } var pushbulletService = container.Resolve>(); var pushbulletSettings = pushbulletService.GetSettings(); if (pushbulletSettings.Enabled) { - NotificationService.Subscribe(new PushbulletNotification(container.Resolve(), pushbulletService)); + notificationService.Subscribe(new PushbulletNotification(container.Resolve(), pushbulletService)); } var pushoverService = container.Resolve>(); var pushoverSettings = pushoverService.GetSettings(); if (pushoverSettings.Enabled) { - NotificationService.Subscribe(new PushoverNotification(container.Resolve(), pushoverService)); + notificationService.Subscribe(new PushoverNotification(container.Resolve(), pushoverService)); } } } diff --git a/PlexRequests.UI/Modules/AdminModule.cs b/PlexRequests.UI/Modules/AdminModule.cs index be0d14e05..2ff454f9b 100644 --- a/PlexRequests.UI/Modules/AdminModule.cs +++ b/PlexRequests.UI/Modules/AdminModule.cs @@ -26,7 +26,6 @@ #endregion using System.Dynamic; using System.Linq; -using System.Web.UI.WebControls; using MarkdownSharp; @@ -44,6 +43,7 @@ using PlexRequests.Api.Interfaces; using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; +using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store.Models; using PlexRequests.Store.Repository; @@ -69,6 +69,7 @@ namespace PlexRequests.UI.Modules private IPushoverApi PushoverApi { get; } private ICouchPotatoApi CpApi { get; } private IRepository LogsRepo { get; } + private INotificationService NotificationService { get; } private static Logger Log = LogManager.GetCurrentClassLogger(); public AdminModule(ISettingsService rpService, @@ -85,7 +86,8 @@ namespace PlexRequests.UI.Modules ICouchPotatoApi cpApi, ISettingsService pushoverSettings, IPushoverApi pushoverApi, - IRepository logsRepo) : base("admin") + IRepository logsRepo, + INotificationService notify) : base("admin") { RpService = rpService; CpService = cpService; @@ -102,6 +104,7 @@ namespace PlexRequests.UI.Modules LogsRepo = logsRepo; PushoverService = pushoverSettings; PushoverApi = pushoverApi; + NotificationService = notify; #if !DEBUG this.RequiresAuthentication(); diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index 228a0d752..2f9eb364d 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -26,7 +26,6 @@ #endregion using System; -using System.Collections.Generic; using System.Linq; using Humanizer; @@ -35,9 +34,9 @@ using Nancy; using Nancy.Responses.Negotiation; using Nancy.Security; -using PlexRequests.Api; using PlexRequests.Core; using PlexRequests.Core.SettingModels; +using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; using PlexRequests.Store; using PlexRequests.UI.Models; @@ -47,11 +46,12 @@ namespace PlexRequests.UI.Modules public class RequestsModule : BaseModule { - public RequestsModule(IRequestService service, ISettingsService prSettings, ISettingsService plex) : base("requests") + public RequestsModule(IRequestService service, ISettingsService prSettings, ISettingsService plex, INotificationService notify) : base("requests") { Service = service; PrSettings = prSettings; PlexSettings = plex; + NotificationService = notify; Get["/"] = _ => LoadRequests(); Get["/movies"] = _ => GetMovies(); @@ -67,6 +67,7 @@ namespace PlexRequests.UI.Modules } private IRequestService Service { get; } + private INotificationService NotificationService { get; } private ISettingsService PrSettings { get; } private ISettingsService PlexSettings { get; } diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 5fce72960..5e29f15e9 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -53,7 +53,8 @@ namespace PlexRequests.UI.Modules public SearchModule(ICacheProvider cache, ISettingsService cpSettings, ISettingsService prSettings, IAvailabilityChecker checker, IRequestService request, ISonarrApi sonarrApi, ISettingsService sonarrSettings, - ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi) : base("search") + ISettingsService sickRageService, ICouchPotatoApi cpApi, ISickRageApi srApi, + INotificationService notify) : base("search") { CpService = cpSettings; PrService = prSettings; @@ -67,6 +68,7 @@ namespace PlexRequests.UI.Modules CouchPotatoApi = cpApi; SickRageService = sickRageService; SickrageApi = srApi; + NotificationService = notify; Get["/"] = parameters => RequestLoad(); @@ -80,6 +82,7 @@ namespace PlexRequests.UI.Modules Post["request/tv"] = parameters => RequestTvShow((int)Request.Form.tvId, (bool)Request.Form.latest); } private TheMovieDbApi MovieApi { get; } + private INotificationService NotificationService { get; } private ICouchPotatoApi CouchPotatoApi { get; } private ISonarrApi SonarrApi { get; } private TheTvDbApi TvApi { get; } From 25a8563ee8891a342687f40dd61cf54930e63dc6 Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 24 Mar 2016 19:45:02 +0000 Subject: [PATCH 29/34] Fixed #69 Removed some redundant code --- PlexRequests.Api/SonarrApi.cs | 39 ++++++--------------- PlexRequests.Store/RequestedModel.cs | 1 + PlexRequests.UI/Content/requests.js | 3 +- PlexRequests.UI/Models/RequestViewModel.cs | 1 + PlexRequests.UI/Modules/LoginModule.cs | 7 ++++ PlexRequests.UI/Modules/RequestsModule.cs | 3 +- PlexRequests.UI/Modules/SearchModule.cs | 26 ++++---------- PlexRequests.UI/Program.cs | 5 --- PlexRequests.UI/Views/Requests/Index.cshtml | 3 ++ 9 files changed, 32 insertions(+), 56 deletions(-) diff --git a/PlexRequests.Api/SonarrApi.cs b/PlexRequests.Api/SonarrApi.cs index 166b41497..1d1a57f34 100644 --- a/PlexRequests.Api/SonarrApi.cs +++ b/PlexRequests.Api/SonarrApi.cs @@ -63,36 +63,17 @@ namespace PlexRequests.Api Method = Method.POST }; - var options = new SonarrAddSeries(); - + var options = new SonarrAddSeries + { + seasonFolder = seasonFolders, + title = title, + qualityProfileId = qualityId, + tvdbId = tvdbId, + titleSlug = title, + seasons = new List(), + rootFolderPath = rootPath + }; - //I'm fairly certain we won't need this logic anymore since we're manually adding the seasons - //if (seasons.Length == 0) - //{ - // options.addOptions = new AddOptions - // { - // ignoreEpisodesWithFiles = true, - // ignoreEpisodesWithoutFiles = true, - // searchForMissingEpisodes = false - // }; - //} - //else - //{ - // options.addOptions = new AddOptions - // { - // ignoreEpisodesWithFiles = false, - // ignoreEpisodesWithoutFiles = false, - // searchForMissingEpisodes = true - // }; - //} - - options.seasonFolder = seasonFolders; - options.title = title; - options.qualityProfileId = qualityId; - options.tvdbId = tvdbId; - options.titleSlug = title; - options.seasons = new List(); - options.rootFolderPath = rootPath; for (var i = 1; i <= seasonCount; i++) { diff --git a/PlexRequests.Store/RequestedModel.cs b/PlexRequests.Store/RequestedModel.cs index 5f746b841..18ef216af 100644 --- a/PlexRequests.Store/RequestedModel.cs +++ b/PlexRequests.Store/RequestedModel.cs @@ -26,6 +26,7 @@ namespace PlexRequests.Store public string AdminNote { get; set; } public int[] SeasonList { get; set; } public int SeasonCount { get; set; } + public string SeasonsRequested { get; set; } } public enum RequestType diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js index 961b30a9a..e05cd294d 100644 --- a/PlexRequests.UI/Content/requests.js +++ b/PlexRequests.UI/Content/requests.js @@ -362,7 +362,8 @@ function buildRequestContext(result, type) { otherMessage: result.otherMessage, requestId: result.id, adminNote: result.adminNotes, - imdb: result.imdbId + imdb: result.imdbId, + seriesRequested: result.tvSeriesRequestType }; return context; diff --git a/PlexRequests.UI/Models/RequestViewModel.cs b/PlexRequests.UI/Models/RequestViewModel.cs index 6018b50b1..011b9977d 100644 --- a/PlexRequests.UI/Models/RequestViewModel.cs +++ b/PlexRequests.UI/Models/RequestViewModel.cs @@ -48,5 +48,6 @@ namespace PlexRequests.UI.Models public string Issues { get; set; } public string OtherMessage { get; set; } public string AdminNotes { get; set; } + public string TvSeriesRequestType { get; set; } } } diff --git a/PlexRequests.UI/Modules/LoginModule.cs b/PlexRequests.UI/Modules/LoginModule.cs index 5436be973..578b9cf7f 100644 --- a/PlexRequests.UI/Modules/LoginModule.cs +++ b/PlexRequests.UI/Modules/LoginModule.cs @@ -115,6 +115,13 @@ namespace PlexRequests.UI.Modules var oldPass = Request.Form.OldPassword; var newPassword = Request.Form.NewPassword; var newPasswordAgain = Request.Form.NewPasswordAgain; + + if (string.IsNullOrEmpty(oldPass) || string.IsNullOrEmpty(newPassword) || + string.IsNullOrEmpty(newPasswordAgain)) + { + return Response.AsJson(new JsonResponseModel { Message = "Please fill in all fields", Result = false }); + } + if (!newPassword.Equals(newPasswordAgain)) { return Response.AsJson(new JsonResponseModel { Message = "The passwords do not match", Result = false }); diff --git a/PlexRequests.UI/Modules/RequestsModule.cs b/PlexRequests.UI/Modules/RequestsModule.cs index 2f9eb364d..a951e0db8 100644 --- a/PlexRequests.UI/Modules/RequestsModule.cs +++ b/PlexRequests.UI/Modules/RequestsModule.cs @@ -129,7 +129,8 @@ namespace PlexRequests.UI.Modules Admin = isAdmin, Issues = tv.Issues.Humanize(LetterCasing.Title), OtherMessage = tv.OtherMessage, - AdminNotes = tv.AdminNote + AdminNotes = tv.AdminNote, + TvSeriesRequestType = tv.SeasonsRequested }).ToList(); return Response.AsJson(viewModel); diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs index 67fe62c22..0ea295d40 100644 --- a/PlexRequests.UI/Modules/SearchModule.cs +++ b/PlexRequests.UI/Modules/SearchModule.cs @@ -261,7 +261,7 @@ namespace PlexRequests.UI.Modules /// Requests the tv show. /// /// The show identifier. - /// if set to true [latest]. + /// The seasons. /// private Response RequestTvShow(int showId, string seasons) { @@ -310,9 +310,14 @@ namespace PlexRequests.UI.Modules { case "first": seasonsList.Add(1); + model.SeasonsRequested = "First"; break; case "latest": seasonsList.Add(model.SeasonCount); + model.SeasonsRequested = "Latest"; + break; + default: + model.SeasonsRequested = "All"; break; } @@ -370,24 +375,5 @@ namespace PlexRequests.UI.Modules var result = Checker.IsAvailable(title, year); return result; } - - //private Response SendToSickRage(SickRageSettings sickRageSettings, RequestedModel model) - //{ - // var result = SickrageApi.AddSeries(model.ProviderId, model.SeasonCount, model.SeasonList, sickRageSettings.QualityProfile, - // sickRageSettings.ApiKey, sickRageSettings.FullUri); - - // Log.Trace("SickRage Result: "); - // Log.Trace(result.DumpJson()); - - // if (result?.result == "success") - // { - // model.Approved = true; - // Log.Debug("Adding tv to database requests (No approval required & SickRage)"); - // RequestService.AddRequest(model); - - // return Response.AsJson(new JsonResponseModel { Result = true }); - // } - // return Response.AsJson(new JsonResponseModel { Result = false, Message = "Something went wrong adding the movie to SickRage! Please check your settings." }); - //} } } \ No newline at end of file diff --git a/PlexRequests.UI/Program.cs b/PlexRequests.UI/Program.cs index f84972699..7ca3fa77c 100644 --- a/PlexRequests.UI/Program.cs +++ b/PlexRequests.UI/Program.cs @@ -25,8 +25,6 @@ // ************************************************************************/ #endregion using System; -using System.Data; - using Microsoft.Owin.Hosting; using Mono.Data.Sqlite; @@ -34,9 +32,6 @@ using Mono.Unix; using Mono.Unix.Native; using NLog; -using NLog.Config; -using NLog.Targets; - using PlexRequests.Core; using PlexRequests.Core.SettingModels; using PlexRequests.Helpers; diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml index 2b6653f0a..cd73d64d9 100644 --- a/PlexRequests.UI/Views/Requests/Index.cshtml +++ b/PlexRequests.UI/Views/Requests/Index.cshtml @@ -119,6 +119,9 @@ {{/if_eq}}
    + {{#if_eq type "tv"}} +
    Series Requested: {{seriesRequested}}
    + {{/if_eq}}
    Requested By: {{requestedBy}}
    Requested Date: {{requestedDate}}
    From b96087d0895963a6a05d4d7c441d1c2441dde0be Mon Sep 17 00:00:00 2001 From: tidusjar Date: Thu, 24 Mar 2016 23:16:55 +0000 Subject: [PATCH 30/34] Finished styling on the logger for now. #59 --- PlexRequests.Helpers/LoggingHelper.cs | 56 +- PlexRequests.Store/Models/LogEntity.cs | 4 + PlexRequests.Store/PlexRequests.Store.csproj | 4 + PlexRequests.Store/packages.config | 1 + PlexRequests.UI/Content/bootstrap.css | 4 +- .../Content/dataTables.bootstrap.css | 187 + PlexRequests.UI/Content/datatables.js | 19026 ++++++++++++++++ PlexRequests.UI/Content/datatables.min.js | 234 + .../fonts/glyphicons-halflings-regular.eot | Bin 0 -> 20127 bytes .../fonts/glyphicons-halflings-regular.svg | 288 + .../fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 45404 bytes .../fonts/glyphicons-halflings-regular.woff | Bin 0 -> 23424 bytes .../fonts/glyphicons-halflings-regular.woff2 | Bin 0 -> 18028 bytes PlexRequests.UI/Models/DatatablesModel.cs | 36 + PlexRequests.UI/Modules/AdminModule.cs | 20 +- PlexRequests.UI/PlexRequests.UI.csproj | 25 + PlexRequests.UI/Program.cs | 4 +- .../Views/Admin/CouchPotato.cshtml | 2 +- PlexRequests.UI/Views/Admin/Logs.cshtml | 56 +- PlexRequests.UI/Views/Admin/Sickrage.cshtml | 4 +- PlexRequests.UI/Views/Admin/Sonarr.cshtml | 2 +- 21 files changed, 19906 insertions(+), 47 deletions(-) create mode 100644 PlexRequests.UI/Content/dataTables.bootstrap.css create mode 100644 PlexRequests.UI/Content/datatables.js create mode 100644 PlexRequests.UI/Content/datatables.min.js create mode 100644 PlexRequests.UI/Content/fonts/glyphicons-halflings-regular.eot create mode 100644 PlexRequests.UI/Content/fonts/glyphicons-halflings-regular.svg create mode 100644 PlexRequests.UI/Content/fonts/glyphicons-halflings-regular.ttf create mode 100644 PlexRequests.UI/Content/fonts/glyphicons-halflings-regular.woff create mode 100644 PlexRequests.UI/Content/fonts/glyphicons-halflings-regular.woff2 create mode 100644 PlexRequests.UI/Models/DatatablesModel.cs diff --git a/PlexRequests.Helpers/LoggingHelper.cs b/PlexRequests.Helpers/LoggingHelper.cs index 5487a90df..db7270db3 100644 --- a/PlexRequests.Helpers/LoggingHelper.cs +++ b/PlexRequests.Helpers/LoggingHelper.cs @@ -75,7 +75,7 @@ namespace PlexRequests.Helpers DBProvider = "Mono.Data.Sqlite.SqliteConnection, Mono.Data.Sqlite, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756", Name = "database" }; - + var messageParam = new DatabaseParameterInfo { Name = "@Message", Layout = "${message}" }; var callsiteParam = new DatabaseParameterInfo { Name = "@Callsite", Layout = "${callsite}" }; var levelParam = new DatabaseParameterInfo { Name = "@Level", Layout = "${level}" }; @@ -103,14 +103,64 @@ namespace PlexRequests.Helpers public static void ReconfigureLogLevel(LogLevel level) { + foreach (var rule in LogManager.Configuration.LoggingRules) { - rule.EnableLoggingForLevel(level); + // Remove all levels + rule.DisableLoggingForLevel(LogLevel.Trace); + rule.DisableLoggingForLevel(LogLevel.Info); + rule.DisableLoggingForLevel(LogLevel.Debug); + rule.DisableLoggingForLevel(LogLevel.Warn); + rule.DisableLoggingForLevel(LogLevel.Error); + rule.DisableLoggingForLevel(LogLevel.Fatal); + + + if (level == LogLevel.Trace) + { + rule.EnableLoggingForLevel(LogLevel.Trace); + rule.EnableLoggingForLevel(LogLevel.Info); + rule.EnableLoggingForLevel(LogLevel.Debug); + rule.EnableLoggingForLevel(LogLevel.Warn); + rule.EnableLoggingForLevel(LogLevel.Error); + rule.EnableLoggingForLevel(LogLevel.Fatal); + } + if (level == LogLevel.Info) + { + rule.EnableLoggingForLevel(LogLevel.Info); + rule.EnableLoggingForLevel(LogLevel.Debug); + rule.EnableLoggingForLevel(LogLevel.Warn); + rule.EnableLoggingForLevel(LogLevel.Error); + rule.EnableLoggingForLevel(LogLevel.Fatal); + } + if (level == LogLevel.Debug) + { + rule.EnableLoggingForLevel(LogLevel.Debug); + rule.EnableLoggingForLevel(LogLevel.Warn); + rule.EnableLoggingForLevel(LogLevel.Error); + rule.EnableLoggingForLevel(LogLevel.Fatal); + } + if (level == LogLevel.Warn) + { + rule.EnableLoggingForLevel(LogLevel.Warn); + rule.EnableLoggingForLevel(LogLevel.Error); + rule.EnableLoggingForLevel(LogLevel.Fatal); + } + if (level == LogLevel.Error) + { + rule.EnableLoggingForLevel(LogLevel.Error); + rule.EnableLoggingForLevel(LogLevel.Fatal); + } + if (level == LogLevel.Fatal) + { + rule.EnableLoggingForLevel(LogLevel.Fatal); + } } - //Call to update existing Loggers created with GetLogger() or + + //Call to update existing Loggers created with GetLogger() or //GetCurrentClassLogger() LogManager.ReconfigExistingLoggers(); } } } + diff --git a/PlexRequests.Store/Models/LogEntity.cs b/PlexRequests.Store/Models/LogEntity.cs index e0a275c0a..d1d45cfc1 100644 --- a/PlexRequests.Store/Models/LogEntity.cs +++ b/PlexRequests.Store/Models/LogEntity.cs @@ -27,6 +27,7 @@ using System; using Dapper.Contrib.Extensions; +using Newtonsoft.Json; namespace PlexRequests.Store.Models { @@ -39,5 +40,8 @@ namespace PlexRequests.Store.Models public string Message { get; set; } public string Callsite { get; set; } public string Exception { get; set; } + + [JsonIgnore] + public string DateString { get; set; } } } diff --git a/PlexRequests.Store/PlexRequests.Store.csproj b/PlexRequests.Store/PlexRequests.Store.csproj index 7207fd555..2175ebc06 100644 --- a/PlexRequests.Store/PlexRequests.Store.csproj +++ b/PlexRequests.Store/PlexRequests.Store.csproj @@ -42,6 +42,10 @@ ..\Assemblies\Mono.Data.Sqlite.dll + + ..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll + True + ..\packages\NLog.4.2.3\lib\net45\NLog.dll True diff --git a/PlexRequests.Store/packages.config b/PlexRequests.Store/packages.config index 6ecb34bd0..e6a6a64fa 100644 --- a/PlexRequests.Store/packages.config +++ b/PlexRequests.Store/packages.config @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/PlexRequests.UI/Content/bootstrap.css b/PlexRequests.UI/Content/bootstrap.css index 7b3046590..83e3b6fa3 100644 --- a/PlexRequests.UI/Content/bootstrap.css +++ b/PlexRequests.UI/Content/bootstrap.css @@ -269,8 +269,8 @@ th { } @font-face { font-family: 'Glyphicons Halflings'; - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); + src: url('../Content/fonts/glyphicons-halflings-regular.eot'); + src: url('../Content/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../Content/fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../Content/fonts/glyphicons-halflings-regular.woff') format('woff'), url('../Content/fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../Content/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); } .glyphicon { position: relative; diff --git a/PlexRequests.UI/Content/dataTables.bootstrap.css b/PlexRequests.UI/Content/dataTables.bootstrap.css new file mode 100644 index 000000000..9abe1b5b4 --- /dev/null +++ b/PlexRequests.UI/Content/dataTables.bootstrap.css @@ -0,0 +1,187 @@ +table.dataTable { + clear: both; + margin-top: 6px !important; + margin-bottom: 6px !important; + max-width: none !important; +} +table.dataTable td, +table.dataTable th { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +table.dataTable td.dataTables_empty, +table.dataTable th.dataTables_empty { + text-align: center; +} +table.dataTable.nowrap th, +table.dataTable.nowrap td { + white-space: nowrap; +} + +div.dataTables_wrapper div.dataTables_length label { + font-weight: normal; + text-align: left; + white-space: nowrap; +} +div.dataTables_wrapper div.dataTables_length select { + width: 75px; + display: inline-block; +} +div.dataTables_wrapper div.dataTables_filter { + text-align: right; +} +div.dataTables_wrapper div.dataTables_filter label { + font-weight: normal; + white-space: nowrap; + text-align: left; +} +div.dataTables_wrapper div.dataTables_filter input { + margin-left: 0.5em; + display: inline-block; + width: auto; +} +div.dataTables_wrapper div.dataTables_info { + padding-top: 8px; + white-space: nowrap; +} +div.dataTables_wrapper div.dataTables_paginate { + margin: 0; + white-space: nowrap; + text-align: right; +} +div.dataTables_wrapper div.dataTables_paginate ul.pagination { + margin: 2px 0; + white-space: nowrap; +} +div.dataTables_wrapper div.dataTables_processing { + position: absolute; + top: 50%; + left: 50%; + width: 200px; + margin-left: -100px; + margin-top: -26px; + text-align: center; + padding: 1em 0; +} + +table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, +table.dataTable thead > tr > td.sorting_asc, +table.dataTable thead > tr > td.sorting_desc, +table.dataTable thead > tr > td.sorting { + padding-right: 30px; +} +table.dataTable thead > tr > th:active, +table.dataTable thead > tr > td:active { + outline: none; +} +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + cursor: pointer; + position: relative; +} +table.dataTable thead .sorting:after, +table.dataTable thead .sorting_asc:after, +table.dataTable thead .sorting_desc:after, +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:after { + position: absolute; + bottom: 8px; + right: 8px; + display: block; + font-family: 'Glyphicons Halflings'; + opacity: 0.5; +} +table.dataTable thead .sorting:after { + opacity: 0.2; + content: "\e150"; + /* sort */ +} +table.dataTable thead .sorting_asc:after { + content: "\e155"; + /* sort-by-attributes */ +} +table.dataTable thead .sorting_desc:after { + content: "\e156"; + /* sort-by-attributes-alt */ +} +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:after { + color: #eee; +} + +div.dataTables_scrollHead table.dataTable { + margin-bottom: 0 !important; +} + +div.dataTables_scrollBody table { + border-top: none; + margin-top: 0 !important; + margin-bottom: 0 !important; +} +div.dataTables_scrollBody table thead .sorting:after, +div.dataTables_scrollBody table thead .sorting_asc:after, +div.dataTables_scrollBody table thead .sorting_desc:after { + display: none; +} +div.dataTables_scrollBody table tbody tr:first-child th, +div.dataTables_scrollBody table tbody tr:first-child td { + border-top: none; +} + +div.dataTables_scrollFoot table { + margin-top: 0 !important; + border-top: none; +} + +@media screen and (max-width: 767px) { + div.dataTables_wrapper div.dataTables_length, + div.dataTables_wrapper div.dataTables_filter, + div.dataTables_wrapper div.dataTables_info, + div.dataTables_wrapper div.dataTables_paginate { + text-align: center; + } +} +table.dataTable.table-condensed > thead > tr > th { + padding-right: 20px; +} +table.dataTable.table-condensed .sorting:after, +table.dataTable.table-condensed .sorting_asc:after, +table.dataTable.table-condensed .sorting_desc:after { + top: 6px; + right: 6px; +} + +table.table-bordered.dataTable { + border-collapse: separate !important; +} +table.table-bordered.dataTable th, +table.table-bordered.dataTable td { + border-left-width: 0; +} +table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, +table.table-bordered.dataTable td:last-child, +table.table-bordered.dataTable td:last-child { + border-right-width: 0; +} +table.table-bordered.dataTable tbody th, +table.table-bordered.dataTable tbody td { + border-bottom-width: 0; +} + +div.dataTables_scrollHead table.table-bordered { + border-bottom-width: 0; +} + +div.table-responsive > div.dataTables_wrapper > div.row { + margin: 0; +} +div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { + padding-left: 0; +} +div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { + padding-right: 0; +} diff --git a/PlexRequests.UI/Content/datatables.js b/PlexRequests.UI/Content/datatables.js new file mode 100644 index 000000000..e4f23def4 --- /dev/null +++ b/PlexRequests.UI/Content/datatables.js @@ -0,0 +1,19026 @@ +/* + * This combined file was created by the DataTables downloader builder: + * https://datatables.net/download + * + * To rebuild or modify this file with the latest versions of the included + * software please visit: + * https://datatables.net/download/#bs-3.3.6/dt-1.10.11,r-2.0.2 + * + * Included libraries: + * Bootstrap 3.3.6, DataTables 1.10.11, Responsive 2.0.2 + */ + +/*! + * Bootstrap v3.3.6 (http://getbootstrap.com) + * Copyright 2011-2015 Twitter, Inc. + * Licensed under the MIT license + */ + +if (typeof jQuery === 'undefined') { + throw new Error('Bootstrap\'s JavaScript requires jQuery') +} + ++function ($) { + 'use strict'; + var version = $.fn.jquery.split(' ')[0].split('.') + if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 2)) { + throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3') + } +}(jQuery); + +/* ======================================================================== + * Bootstrap: transition.js v3.3.6 + * http://getbootstrap.com/javascript/#transitions + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) + // ============================================================ + + function transitionEnd() { + var el = document.createElement('bootstrap') + + var transEndEventNames = { + WebkitTransition : 'webkitTransitionEnd', + MozTransition : 'transitionend', + OTransition : 'oTransitionEnd otransitionend', + transition : 'transitionend' + } + + for (var name in transEndEventNames) { + if (el.style[name] !== undefined) { + return { end: transEndEventNames[name] } + } + } + + return false // explicit for ie8 ( ._.) + } + + // http://blog.alexmaccaw.com/css-transitions + $.fn.emulateTransitionEnd = function (duration) { + var called = false + var $el = this + $(this).one('bsTransitionEnd', function () { called = true }) + var callback = function () { if (!called) $($el).trigger($.support.transition.end) } + setTimeout(callback, duration) + return this + } + + $(function () { + $.support.transition = transitionEnd() + + if (!$.support.transition) return + + $.event.special.bsTransitionEnd = { + bindType: $.support.transition.end, + delegateType: $.support.transition.end, + handle: function (e) { + if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) + } + } + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: alert.js v3.3.6 + * http://getbootstrap.com/javascript/#alerts + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // ALERT CLASS DEFINITION + // ====================== + + var dismiss = '[data-dismiss="alert"]' + var Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.VERSION = '3.3.6' + + Alert.TRANSITION_DURATION = 150 + + Alert.prototype.close = function (e) { + var $this = $(this) + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = $(selector) + + if (e) e.preventDefault() + + if (!$parent.length) { + $parent = $this.closest('.alert') + } + + $parent.trigger(e = $.Event('close.bs.alert')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + // detach from parent, fire event then clean up data + $parent.detach().trigger('closed.bs.alert').remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent + .one('bsTransitionEnd', removeElement) + .emulateTransitionEnd(Alert.TRANSITION_DURATION) : + removeElement() + } + + + // ALERT PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.alert') + + if (!data) $this.data('bs.alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.alert + + $.fn.alert = Plugin + $.fn.alert.Constructor = Alert + + + // ALERT NO CONFLICT + // ================= + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + // ALERT DATA-API + // ============== + + $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: button.js v3.3.6 + * http://getbootstrap.com/javascript/#buttons + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // BUTTON PUBLIC CLASS DEFINITION + // ============================== + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Button.DEFAULTS, options) + this.isLoading = false + } + + Button.VERSION = '3.3.6' + + Button.DEFAULTS = { + loadingText: 'loading...' + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + var $el = this.$element + var val = $el.is('input') ? 'val' : 'html' + var data = $el.data() + + state += 'Text' + + if (data.resetText == null) $el.data('resetText', $el[val]()) + + // push to event loop to allow forms to submit + setTimeout($.proxy(function () { + $el[val](data[state] == null ? this.options[state] : data[state]) + + if (state == 'loadingText') { + this.isLoading = true + $el.addClass(d).attr(d, d) + } else if (this.isLoading) { + this.isLoading = false + $el.removeClass(d).removeAttr(d) + } + }, this), 0) + } + + Button.prototype.toggle = function () { + var changed = true + var $parent = this.$element.closest('[data-toggle="buttons"]') + + if ($parent.length) { + var $input = this.$element.find('input') + if ($input.prop('type') == 'radio') { + if ($input.prop('checked')) changed = false + $parent.find('.active').removeClass('active') + this.$element.addClass('active') + } else if ($input.prop('type') == 'checkbox') { + if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false + this.$element.toggleClass('active') + } + $input.prop('checked', this.$element.hasClass('active')) + if (changed) $input.trigger('change') + } else { + this.$element.attr('aria-pressed', !this.$element.hasClass('active')) + this.$element.toggleClass('active') + } + } + + + // BUTTON PLUGIN DEFINITION + // ======================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.button') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.button', (data = new Button(this, options))) + + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + var old = $.fn.button + + $.fn.button = Plugin + $.fn.button.Constructor = Button + + + // BUTTON NO CONFLICT + // ================== + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + // BUTTON DATA-API + // =============== + + $(document) + .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + Plugin.call($btn, 'toggle') + if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() + }) + .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { + $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: carousel.js v3.3.6 + * http://getbootstrap.com/javascript/#carousel + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // CAROUSEL CLASS DEFINITION + // ========================= + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.paused = null + this.sliding = null + this.interval = null + this.$active = null + this.$items = null + + this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) + + this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element + .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) + .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) + } + + Carousel.VERSION = '3.3.6' + + Carousel.TRANSITION_DURATION = 600 + + Carousel.DEFAULTS = { + interval: 5000, + pause: 'hover', + wrap: true, + keyboard: true + } + + Carousel.prototype.keydown = function (e) { + if (/input|textarea/i.test(e.target.tagName)) return + switch (e.which) { + case 37: this.prev(); break + case 39: this.next(); break + default: return + } + + e.preventDefault() + } + + Carousel.prototype.cycle = function (e) { + e || (this.paused = false) + + this.interval && clearInterval(this.interval) + + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + + return this + } + + Carousel.prototype.getItemIndex = function (item) { + this.$items = item.parent().children('.item') + return this.$items.index(item || this.$active) + } + + Carousel.prototype.getItemForDirection = function (direction, active) { + var activeIndex = this.getItemIndex(active) + var willWrap = (direction == 'prev' && activeIndex === 0) + || (direction == 'next' && activeIndex == (this.$items.length - 1)) + if (willWrap && !this.options.wrap) return active + var delta = direction == 'prev' ? -1 : 1 + var itemIndex = (activeIndex + delta) % this.$items.length + return this.$items.eq(itemIndex) + } + + Carousel.prototype.to = function (pos) { + var that = this + var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" + if (activeIndex == pos) return this.pause().cycle() + + return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) + } + + Carousel.prototype.pause = function (e) { + e || (this.paused = true) + + if (this.$element.find('.next, .prev').length && $.support.transition) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + + this.interval = clearInterval(this.interval) + + return this + } + + Carousel.prototype.next = function () { + if (this.sliding) return + return this.slide('next') + } + + Carousel.prototype.prev = function () { + if (this.sliding) return + return this.slide('prev') + } + + Carousel.prototype.slide = function (type, next) { + var $active = this.$element.find('.item.active') + var $next = next || this.getItemForDirection(type, $active) + var isCycling = this.interval + var direction = type == 'next' ? 'left' : 'right' + var that = this + + if ($next.hasClass('active')) return (this.sliding = false) + + var relatedTarget = $next[0] + var slideEvent = $.Event('slide.bs.carousel', { + relatedTarget: relatedTarget, + direction: direction + }) + this.$element.trigger(slideEvent) + if (slideEvent.isDefaultPrevented()) return + + this.sliding = true + + isCycling && this.pause() + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) + $nextIndicator && $nextIndicator.addClass('active') + } + + var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" + if ($.support.transition && this.$element.hasClass('slide')) { + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + $active + .one('bsTransitionEnd', function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { + that.$element.trigger(slidEvent) + }, 0) + }) + .emulateTransitionEnd(Carousel.TRANSITION_DURATION) + } else { + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger(slidEvent) + } + + isCycling && this.cycle() + + return this + } + + + // CAROUSEL PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.carousel') + var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) + var action = typeof option == 'string' ? option : options.slide + + if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + var old = $.fn.carousel + + $.fn.carousel = Plugin + $.fn.carousel.Constructor = Carousel + + + // CAROUSEL NO CONFLICT + // ==================== + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + + // CAROUSEL DATA-API + // ================= + + var clickHandler = function (e) { + var href + var $this = $(this) + var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + if (!$target.hasClass('carousel')) return + var options = $.extend({}, $target.data(), $this.data()) + var slideIndex = $this.attr('data-slide-to') + if (slideIndex) options.interval = false + + Plugin.call($target, options) + + if (slideIndex) { + $target.data('bs.carousel').to(slideIndex) + } + + e.preventDefault() + } + + $(document) + .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) + .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) + + $(window).on('load', function () { + $('[data-ride="carousel"]').each(function () { + var $carousel = $(this) + Plugin.call($carousel, $carousel.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: collapse.js v3.3.6 + * http://getbootstrap.com/javascript/#collapse + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // COLLAPSE PUBLIC CLASS DEFINITION + // ================================ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, Collapse.DEFAULTS, options) + this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + + '[data-toggle="collapse"][data-target="#' + element.id + '"]') + this.transitioning = null + + if (this.options.parent) { + this.$parent = this.getParent() + } else { + this.addAriaAndCollapsedClass(this.$element, this.$trigger) + } + + if (this.options.toggle) this.toggle() + } + + Collapse.VERSION = '3.3.6' + + Collapse.TRANSITION_DURATION = 350 + + Collapse.DEFAULTS = { + toggle: true + } + + Collapse.prototype.dimension = function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + Collapse.prototype.show = function () { + if (this.transitioning || this.$element.hasClass('in')) return + + var activesData + var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') + + if (actives && actives.length) { + activesData = actives.data('bs.collapse') + if (activesData && activesData.transitioning) return + } + + var startEvent = $.Event('show.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + if (actives && actives.length) { + Plugin.call(actives, 'hide') + activesData || actives.data('bs.collapse', null) + } + + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + .addClass('collapsing')[dimension](0) + .attr('aria-expanded', true) + + this.$trigger + .removeClass('collapsed') + .attr('aria-expanded', true) + + this.transitioning = 1 + + var complete = function () { + this.$element + .removeClass('collapsing') + .addClass('collapse in')[dimension]('') + this.transitioning = 0 + this.$element + .trigger('shown.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + var scrollSize = $.camelCase(['scroll', dimension].join('-')) + + this.$element + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) + } + + Collapse.prototype.hide = function () { + if (this.transitioning || !this.$element.hasClass('in')) return + + var startEvent = $.Event('hide.bs.collapse') + this.$element.trigger(startEvent) + if (startEvent.isDefaultPrevented()) return + + var dimension = this.dimension() + + this.$element[dimension](this.$element[dimension]())[0].offsetHeight + + this.$element + .addClass('collapsing') + .removeClass('collapse in') + .attr('aria-expanded', false) + + this.$trigger + .addClass('collapsed') + .attr('aria-expanded', false) + + this.transitioning = 1 + + var complete = function () { + this.transitioning = 0 + this.$element + .removeClass('collapsing') + .addClass('collapse') + .trigger('hidden.bs.collapse') + } + + if (!$.support.transition) return complete.call(this) + + this.$element + [dimension](0) + .one('bsTransitionEnd', $.proxy(complete, this)) + .emulateTransitionEnd(Collapse.TRANSITION_DURATION) + } + + Collapse.prototype.toggle = function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + Collapse.prototype.getParent = function () { + return $(this.options.parent) + .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') + .each($.proxy(function (i, element) { + var $element = $(element) + this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) + }, this)) + .end() + } + + Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { + var isOpen = $element.hasClass('in') + + $element.attr('aria-expanded', isOpen) + $trigger + .toggleClass('collapsed', !isOpen) + .attr('aria-expanded', isOpen) + } + + function getTargetFromTrigger($trigger) { + var href + var target = $trigger.attr('data-target') + || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + + return $(target) + } + + + // COLLAPSE PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.collapse') + var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false + if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.collapse + + $.fn.collapse = Plugin + $.fn.collapse.Constructor = Collapse + + + // COLLAPSE NO CONFLICT + // ==================== + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + // COLLAPSE DATA-API + // ================= + + $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { + var $this = $(this) + + if (!$this.attr('data-target')) e.preventDefault() + + var $target = getTargetFromTrigger($this) + var data = $target.data('bs.collapse') + var option = data ? 'toggle' : $this.data() + + Plugin.call($target, option) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.6 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.6' + + function getParent($this) { + var selector = $this.attr('data-target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + var $parent = selector && $(selector) + + return $parent && $parent.length ? $parent : $this.parent() + } + + function clearMenus(e) { + if (e && e.which === 3) return + $(backdrop).remove() + $(toggle).each(function () { + var $this = $(this) + var $parent = getParent($this) + var relatedTarget = { relatedTarget: this } + + if (!$parent.hasClass('open')) return + + if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return + + $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this.attr('aria-expanded', 'false') + $parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget)) + }) + } + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $(document.createElement('div')) + .addClass('dropdown-backdrop') + .insertAfter($(this)) + .on('click', clearMenus) + } + + var relatedTarget = { relatedTarget: this } + $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) + + if (e.isDefaultPrevented()) return + + $this + .trigger('focus') + .attr('aria-expanded', 'true') + + $parent + .toggleClass('open') + .trigger($.Event('shown.bs.dropdown', relatedTarget)) + } + + return false + } + + Dropdown.prototype.keydown = function (e) { + if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return + + var $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + if (!isActive && e.which != 27 || isActive && e.which == 27) { + if (e.which == 27) $parent.find(toggle).trigger('focus') + return $this.trigger('click') + } + + var desc = ' li:not(.disabled):visible a' + var $items = $parent.find('.dropdown-menu' + desc) + + if (!$items.length) return + + var index = $items.index(e.target) + + if (e.which == 38 && index > 0) index-- // up + if (e.which == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items.eq(index).trigger('focus') + } + + + // DROPDOWN PLUGIN DEFINITION + // ========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.dropdown') + + if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + var old = $.fn.dropdown + + $.fn.dropdown = Plugin + $.fn.dropdown.Constructor = Dropdown + + + // DROPDOWN NO CONFLICT + // ==================== + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + // APPLY TO STANDARD DROPDOWN ELEMENTS + // =================================== + + $(document) + .on('click.bs.dropdown.data-api', clearMenus) + .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) + .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) + .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: modal.js v3.3.6 + * http://getbootstrap.com/javascript/#modals + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // MODAL CLASS DEFINITION + // ====================== + + var Modal = function (element, options) { + this.options = options + this.$body = $(document.body) + this.$element = $(element) + this.$dialog = this.$element.find('.modal-dialog') + this.$backdrop = null + this.isShown = null + this.originalBodyPad = null + this.scrollbarWidth = 0 + this.ignoreBackdropClick = false + + if (this.options.remote) { + this.$element + .find('.modal-content') + .load(this.options.remote, $.proxy(function () { + this.$element.trigger('loaded.bs.modal') + }, this)) + } + } + + Modal.VERSION = '3.3.6' + + Modal.TRANSITION_DURATION = 300 + Modal.BACKDROP_TRANSITION_DURATION = 150 + + Modal.DEFAULTS = { + backdrop: true, + keyboard: true, + show: true + } + + Modal.prototype.toggle = function (_relatedTarget) { + return this.isShown ? this.hide() : this.show(_relatedTarget) + } + + Modal.prototype.show = function (_relatedTarget) { + var that = this + var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.checkScrollbar() + this.setScrollbar() + this.$body.addClass('modal-open') + + this.escape() + this.resize() + + this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) + + this.$dialog.on('mousedown.dismiss.bs.modal', function () { + that.$element.one('mouseup.dismiss.bs.modal', function (e) { + if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true + }) + }) + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(that.$body) // don't move modals dom position + } + + that.$element + .show() + .scrollTop(0) + + that.adjustDialog() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element.addClass('in') + + that.enforceFocus() + + var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) + + transition ? + that.$dialog // wait for modal to slide in + .one('bsTransitionEnd', function () { + that.$element.trigger('focus').trigger(e) + }) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + that.$element.trigger('focus').trigger(e) + }) + } + + Modal.prototype.hide = function (e) { + if (e) e.preventDefault() + + e = $.Event('hide.bs.modal') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + this.resize() + + $(document).off('focusin.bs.modal') + + this.$element + .removeClass('in') + .off('click.dismiss.bs.modal') + .off('mouseup.dismiss.bs.modal') + + this.$dialog.off('mousedown.dismiss.bs.modal') + + $.support.transition && this.$element.hasClass('fade') ? + this.$element + .one('bsTransitionEnd', $.proxy(this.hideModal, this)) + .emulateTransitionEnd(Modal.TRANSITION_DURATION) : + this.hideModal() + } + + Modal.prototype.enforceFocus = function () { + $(document) + .off('focusin.bs.modal') // guard against infinite focus loop + .on('focusin.bs.modal', $.proxy(function (e) { + if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { + this.$element.trigger('focus') + } + }, this)) + } + + Modal.prototype.escape = function () { + if (this.isShown && this.options.keyboard) { + this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { + e.which == 27 && this.hide() + }, this)) + } else if (!this.isShown) { + this.$element.off('keydown.dismiss.bs.modal') + } + } + + Modal.prototype.resize = function () { + if (this.isShown) { + $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) + } else { + $(window).off('resize.bs.modal') + } + } + + Modal.prototype.hideModal = function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.$body.removeClass('modal-open') + that.resetAdjustments() + that.resetScrollbar() + that.$element.trigger('hidden.bs.modal') + }) + } + + Modal.prototype.removeBackdrop = function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + Modal.prototype.backdrop = function (callback) { + var that = this + var animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $(document.createElement('div')) + .addClass('modal-backdrop ' + animate) + .appendTo(this.$body) + + this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { + if (this.ignoreBackdropClick) { + this.ignoreBackdropClick = false + return + } + if (e.target !== e.currentTarget) return + this.options.backdrop == 'static' + ? this.$element[0].focus() + : this.hide() + }, this)) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop + .one('bsTransitionEnd', callback) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + var callbackRemove = function () { + that.removeBackdrop() + callback && callback() + } + $.support.transition && this.$element.hasClass('fade') ? + this.$backdrop + .one('bsTransitionEnd', callbackRemove) + .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : + callbackRemove() + + } else if (callback) { + callback() + } + } + + // these following methods are used to handle overflowing modals + + Modal.prototype.handleUpdate = function () { + this.adjustDialog() + } + + Modal.prototype.adjustDialog = function () { + var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight + + this.$element.css({ + paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', + paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' + }) + } + + Modal.prototype.resetAdjustments = function () { + this.$element.css({ + paddingLeft: '', + paddingRight: '' + }) + } + + Modal.prototype.checkScrollbar = function () { + var fullWindowWidth = window.innerWidth + if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 + var documentElementRect = document.documentElement.getBoundingClientRect() + fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) + } + this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth + this.scrollbarWidth = this.measureScrollbar() + } + + Modal.prototype.setScrollbar = function () { + var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) + this.originalBodyPad = document.body.style.paddingRight || '' + if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) + } + + Modal.prototype.resetScrollbar = function () { + this.$body.css('padding-right', this.originalBodyPad) + } + + Modal.prototype.measureScrollbar = function () { // thx walsh + var scrollDiv = document.createElement('div') + scrollDiv.className = 'modal-scrollbar-measure' + this.$body.append(scrollDiv) + var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth + this.$body[0].removeChild(scrollDiv) + return scrollbarWidth + } + + + // MODAL PLUGIN DEFINITION + // ======================= + + function Plugin(option, _relatedTarget) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.modal') + var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) + + if (!data) $this.data('bs.modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option](_relatedTarget) + else if (options.show) data.show(_relatedTarget) + }) + } + + var old = $.fn.modal + + $.fn.modal = Plugin + $.fn.modal.Constructor = Modal + + + // MODAL NO CONFLICT + // ================= + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + // MODAL DATA-API + // ============== + + $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + var href = $this.attr('href') + var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 + var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) + + if ($this.is('a')) e.preventDefault() + + $target.one('show.bs.modal', function (showEvent) { + if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown + $target.one('hidden.bs.modal', function () { + $this.is(':visible') && $this.trigger('focus') + }) + }) + Plugin.call($target, option, this) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tooltip.js v3.3.6 + * http://getbootstrap.com/javascript/#tooltip + * Inspired by the original jQuery.tipsy by Jason Frame + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TOOLTIP PUBLIC CLASS DEFINITION + // =============================== + + var Tooltip = function (element, options) { + this.type = null + this.options = null + this.enabled = null + this.timeout = null + this.hoverState = null + this.$element = null + this.inState = null + + this.init('tooltip', element, options) + } + + Tooltip.VERSION = '3.3.6' + + Tooltip.TRANSITION_DURATION = 150 + + Tooltip.DEFAULTS = { + animation: true, + placement: 'top', + selector: false, + template: '', + trigger: 'hover focus', + title: '', + delay: 0, + html: false, + container: false, + viewport: { + selector: 'body', + padding: 0 + } + } + + Tooltip.prototype.init = function (type, element, options) { + this.enabled = true + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) + this.inState = { click: false, hover: false, focus: false } + + if (this.$element[0] instanceof document.constructor && !this.options.selector) { + throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') + } + + var triggers = this.options.trigger.split(' ') + + for (var i = triggers.length; i--;) { + var trigger = triggers[i] + + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' + var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' + + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + Tooltip.prototype.getDefaults = function () { + return Tooltip.DEFAULTS + } + + Tooltip.prototype.getOptions = function (options) { + options = $.extend({}, this.getDefaults(), this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay, + hide: options.delay + } + } + + return options + } + + Tooltip.prototype.getDelegateOptions = function () { + var options = {} + var defaults = this.getDefaults() + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }) + + return options + } + + Tooltip.prototype.enter = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true + } + + if (self.tip().hasClass('in') || self.hoverState == 'in') { + self.hoverState = 'in' + return + } + + clearTimeout(self.timeout) + + self.hoverState = 'in' + + if (!self.options.delay || !self.options.delay.show) return self.show() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + Tooltip.prototype.isInStateTrue = function () { + for (var key in this.inState) { + if (this.inState[key]) return true + } + + return false + } + + Tooltip.prototype.leave = function (obj) { + var self = obj instanceof this.constructor ? + obj : $(obj.currentTarget).data('bs.' + this.type) + + if (!self) { + self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) + $(obj.currentTarget).data('bs.' + this.type, self) + } + + if (obj instanceof $.Event) { + self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false + } + + if (self.isInStateTrue()) return + + clearTimeout(self.timeout) + + self.hoverState = 'out' + + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.timeout = setTimeout(function () { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + Tooltip.prototype.show = function () { + var e = $.Event('show.bs.' + this.type) + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + + var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) + if (e.isDefaultPrevented() || !inDom) return + var that = this + + var $tip = this.tip() + + var tipId = this.getUID(this.type) + + this.setContent() + $tip.attr('id', tipId) + this.$element.attr('aria-describedby', tipId) + + if (this.options.animation) $tip.addClass('fade') + + var placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + var autoToken = /\s?auto?\s?/i + var autoPlace = autoToken.test(placement) + if (autoPlace) placement = placement.replace(autoToken, '') || 'top' + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + .addClass(placement) + .data('bs.' + this.type, this) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + this.$element.trigger('inserted.bs.' + this.type) + + var pos = this.getPosition() + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (autoPlace) { + var orgPlacement = placement + var viewportDim = this.getPosition(this.$viewport) + + placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : + placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : + placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : + placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : + placement + + $tip + .removeClass(orgPlacement) + .addClass(placement) + } + + var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) + + this.applyPlacement(calculatedOffset, placement) + + var complete = function () { + var prevHoverState = that.hoverState + that.$element.trigger('shown.bs.' + that.type) + that.hoverState = null + + if (prevHoverState == 'out') that.leave(that) + } + + $.support.transition && this.$tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + } + } + + Tooltip.prototype.applyPlacement = function (offset, placement) { + var $tip = this.tip() + var width = $tip[0].offsetWidth + var height = $tip[0].offsetHeight + + // manually read margins because getBoundingClientRect includes difference + var marginTop = parseInt($tip.css('margin-top'), 10) + var marginLeft = parseInt($tip.css('margin-left'), 10) + + // we must check for NaN for ie 8/9 + if (isNaN(marginTop)) marginTop = 0 + if (isNaN(marginLeft)) marginLeft = 0 + + offset.top += marginTop + offset.left += marginLeft + + // $.fn.offset doesn't round pixel values + // so we use setOffset directly with our own function B-0 + $.offset.setOffset($tip[0], $.extend({ + using: function (props) { + $tip.css({ + top: Math.round(props.top), + left: Math.round(props.left) + }) + } + }, offset), 0) + + $tip.addClass('in') + + // check to see if placing tip in new offset caused the tip to resize itself + var actualWidth = $tip[0].offsetWidth + var actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + offset.top = offset.top + height - actualHeight + } + + var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) + + if (delta.left) offset.left += delta.left + else offset.top += delta.top + + var isVertical = /top|bottom/.test(placement) + var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight + var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' + + $tip.offset(offset) + this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) + } + + Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { + this.arrow() + .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') + .css(isVertical ? 'top' : 'left', '') + } + + Tooltip.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + Tooltip.prototype.hide = function (callback) { + var that = this + var $tip = $(this.$tip) + var e = $.Event('hide.bs.' + this.type) + + function complete() { + if (that.hoverState != 'in') $tip.detach() + that.$element + .removeAttr('aria-describedby') + .trigger('hidden.bs.' + that.type) + callback && callback() + } + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + $.support.transition && $tip.hasClass('fade') ? + $tip + .one('bsTransitionEnd', complete) + .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : + complete() + + this.hoverState = null + + return this + } + + Tooltip.prototype.fixTitle = function () { + var $e = this.$element + if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + Tooltip.prototype.hasContent = function () { + return this.getTitle() + } + + Tooltip.prototype.getPosition = function ($element) { + $element = $element || this.$element + + var el = $element[0] + var isBody = el.tagName == 'BODY' + + var elRect = el.getBoundingClientRect() + if (elRect.width == null) { + // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 + elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) + } + var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() + var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } + var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null + + return $.extend({}, elRect, scroll, outerDims, elOffset) + } + + Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { + return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : + placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : + /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } + + } + + Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { + var delta = { top: 0, left: 0 } + if (!this.$viewport) return delta + + var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 + var viewportDimensions = this.getPosition(this.$viewport) + + if (/right|left/.test(placement)) { + var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll + var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight + if (topEdgeOffset < viewportDimensions.top) { // top overflow + delta.top = viewportDimensions.top - topEdgeOffset + } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow + delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset + } + } else { + var leftEdgeOffset = pos.left - viewportPadding + var rightEdgeOffset = pos.left + viewportPadding + actualWidth + if (leftEdgeOffset < viewportDimensions.left) { // left overflow + delta.left = viewportDimensions.left - leftEdgeOffset + } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow + delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset + } + } + + return delta + } + + Tooltip.prototype.getTitle = function () { + var title + var $e = this.$element + var o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + Tooltip.prototype.getUID = function (prefix) { + do prefix += ~~(Math.random() * 1000000) + while (document.getElementById(prefix)) + return prefix + } + + Tooltip.prototype.tip = function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + if (this.$tip.length != 1) { + throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') + } + } + return this.$tip + } + + Tooltip.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) + } + + Tooltip.prototype.enable = function () { + this.enabled = true + } + + Tooltip.prototype.disable = function () { + this.enabled = false + } + + Tooltip.prototype.toggleEnabled = function () { + this.enabled = !this.enabled + } + + Tooltip.prototype.toggle = function (e) { + var self = this + if (e) { + self = $(e.currentTarget).data('bs.' + this.type) + if (!self) { + self = new this.constructor(e.currentTarget, this.getDelegateOptions()) + $(e.currentTarget).data('bs.' + this.type, self) + } + } + + if (e) { + self.inState.click = !self.inState.click + if (self.isInStateTrue()) self.enter(self) + else self.leave(self) + } else { + self.tip().hasClass('in') ? self.leave(self) : self.enter(self) + } + } + + Tooltip.prototype.destroy = function () { + var that = this + clearTimeout(this.timeout) + this.hide(function () { + that.$element.off('.' + that.type).removeData('bs.' + that.type) + if (that.$tip) { + that.$tip.detach() + } + that.$tip = null + that.$arrow = null + that.$viewport = null + }) + } + + + // TOOLTIP PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tooltip') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tooltip + + $.fn.tooltip = Plugin + $.fn.tooltip.Constructor = Tooltip + + + // TOOLTIP NO CONFLICT + // =================== + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: popover.js v3.3.6 + * http://getbootstrap.com/javascript/#popovers + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // POPOVER PUBLIC CLASS DEFINITION + // =============================== + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') + + Popover.VERSION = '3.3.6' + + Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { + placement: 'right', + trigger: 'click', + content: '', + template: '' + }) + + + // NOTE: POPOVER EXTENDS tooltip.js + // ================================ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) + + Popover.prototype.constructor = Popover + + Popover.prototype.getDefaults = function () { + return Popover.DEFAULTS + } + + Popover.prototype.setContent = function () { + var $tip = this.tip() + var title = this.getTitle() + var content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events + this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' + ](content) + + $tip.removeClass('fade top bottom left right in') + + // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do + // this manually by checking the contents. + if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() + } + + Popover.prototype.hasContent = function () { + return this.getTitle() || this.getContent() + } + + Popover.prototype.getContent = function () { + var $e = this.$element + var o = this.options + + return $e.attr('data-content') + || (typeof o.content == 'function' ? + o.content.call($e[0]) : + o.content) + } + + Popover.prototype.arrow = function () { + return (this.$arrow = this.$arrow || this.tip().find('.arrow')) + } + + + // POPOVER PLUGIN DEFINITION + // ========================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.popover') + var options = typeof option == 'object' && option + + if (!data && /destroy|hide/.test(option)) return + if (!data) $this.data('bs.popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.popover + + $.fn.popover = Plugin + $.fn.popover.Constructor = Popover + + + // POPOVER NO CONFLICT + // =================== + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(jQuery); + +/* ======================================================================== + * Bootstrap: scrollspy.js v3.3.6 + * http://getbootstrap.com/javascript/#scrollspy + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // SCROLLSPY CLASS DEFINITION + // ========================== + + function ScrollSpy(element, options) { + this.$body = $(document.body) + this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) + this.options = $.extend({}, ScrollSpy.DEFAULTS, options) + this.selector = (this.options.target || '') + ' .nav li > a' + this.offsets = [] + this.targets = [] + this.activeTarget = null + this.scrollHeight = 0 + + this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) + this.refresh() + this.process() + } + + ScrollSpy.VERSION = '3.3.6' + + ScrollSpy.DEFAULTS = { + offset: 10 + } + + ScrollSpy.prototype.getScrollHeight = function () { + return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) + } + + ScrollSpy.prototype.refresh = function () { + var that = this + var offsetMethod = 'offset' + var offsetBase = 0 + + this.offsets = [] + this.targets = [] + this.scrollHeight = this.getScrollHeight() + + if (!$.isWindow(this.$scrollElement[0])) { + offsetMethod = 'position' + offsetBase = this.$scrollElement.scrollTop() + } + + this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + var href = $el.data('target') || $el.attr('href') + var $href = /^#./.test(href) && $(href) + + return ($href + && $href.length + && $href.is(':visible') + && [[$href[offsetMethod]().top + offsetBase, href]]) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + that.offsets.push(this[0]) + that.targets.push(this[1]) + }) + } + + ScrollSpy.prototype.process = function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + var scrollHeight = this.getScrollHeight() + var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() + var offsets = this.offsets + var targets = this.targets + var activeTarget = this.activeTarget + var i + + if (this.scrollHeight != scrollHeight) { + this.refresh() + } + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) + } + + if (activeTarget && scrollTop < offsets[0]) { + this.activeTarget = null + return this.clear() + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) + && this.activate(targets[i]) + } + } + + ScrollSpy.prototype.activate = function (target) { + this.activeTarget = target + + this.clear() + + var selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + var active = $(selector) + .parents('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active + .closest('li.dropdown') + .addClass('active') + } + + active.trigger('activate.bs.scrollspy') + } + + ScrollSpy.prototype.clear = function () { + $(this.selector) + .parentsUntil(this.options.target, '.active') + .removeClass('active') + } + + + // SCROLLSPY PLUGIN DEFINITION + // =========================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.scrollspy') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.scrollspy + + $.fn.scrollspy = Plugin + $.fn.scrollspy.Constructor = ScrollSpy + + + // SCROLLSPY NO CONFLICT + // ===================== + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + // SCROLLSPY DATA-API + // ================== + + $(window).on('load.bs.scrollspy.data-api', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + Plugin.call($spy, $spy.data()) + }) + }) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: tab.js v3.3.6 + * http://getbootstrap.com/javascript/#tabs + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // TAB CLASS DEFINITION + // ==================== + + var Tab = function (element) { + // jscs:disable requireDollarBeforejQueryAssignment + this.element = $(element) + // jscs:enable requireDollarBeforejQueryAssignment + } + + Tab.VERSION = '3.3.6' + + Tab.TRANSITION_DURATION = 150 + + Tab.prototype.show = function () { + var $this = this.element + var $ul = $this.closest('ul:not(.dropdown-menu)') + var selector = $this.data('target') + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 + } + + if ($this.parent('li').hasClass('active')) return + + var $previous = $ul.find('.active:last a') + var hideEvent = $.Event('hide.bs.tab', { + relatedTarget: $this[0] + }) + var showEvent = $.Event('show.bs.tab', { + relatedTarget: $previous[0] + }) + + $previous.trigger(hideEvent) + $this.trigger(showEvent) + + if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return + + var $target = $(selector) + + this.activate($this.closest('li'), $ul) + this.activate($target, $target.parent(), function () { + $previous.trigger({ + type: 'hidden.bs.tab', + relatedTarget: $this[0] + }) + $this.trigger({ + type: 'shown.bs.tab', + relatedTarget: $previous[0] + }) + }) + } + + Tab.prototype.activate = function (element, container, callback) { + var $active = container.find('> .active') + var transition = callback + && $.support.transition + && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', false) + + element + .addClass('active') + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if (element.parent('.dropdown-menu').length) { + element + .closest('li.dropdown') + .addClass('active') + .end() + .find('[data-toggle="tab"]') + .attr('aria-expanded', true) + } + + callback && callback() + } + + $active.length && transition ? + $active + .one('bsTransitionEnd', next) + .emulateTransitionEnd(Tab.TRANSITION_DURATION) : + next() + + $active.removeClass('in') + } + + + // TAB PLUGIN DEFINITION + // ===================== + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.tab') + + if (!data) $this.data('bs.tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.tab + + $.fn.tab = Plugin + $.fn.tab.Constructor = Tab + + + // TAB NO CONFLICT + // =============== + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + // TAB DATA-API + // ============ + + var clickHandler = function (e) { + e.preventDefault() + Plugin.call($(this), 'show') + } + + $(document) + .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) + .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) + +}(jQuery); + +/* ======================================================================== + * Bootstrap: affix.js v3.3.6 + * http://getbootstrap.com/javascript/#affix + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // AFFIX CLASS DEFINITION + // ====================== + + var Affix = function (element, options) { + this.options = $.extend({}, Affix.DEFAULTS, options) + + this.$target = $(this.options.target) + .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) + + this.$element = $(element) + this.affixed = null + this.unpin = null + this.pinnedOffset = null + + this.checkPosition() + } + + Affix.VERSION = '3.3.6' + + Affix.RESET = 'affix affix-top affix-bottom' + + Affix.DEFAULTS = { + offset: 0, + target: window + } + + Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { + var scrollTop = this.$target.scrollTop() + var position = this.$element.offset() + var targetHeight = this.$target.height() + + if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false + + if (this.affixed == 'bottom') { + if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' + return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' + } + + var initializing = this.affixed == null + var colliderTop = initializing ? scrollTop : position.top + var colliderHeight = initializing ? targetHeight : height + + if (offsetTop != null && scrollTop <= offsetTop) return 'top' + if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' + + return false + } + + Affix.prototype.getPinnedOffset = function () { + if (this.pinnedOffset) return this.pinnedOffset + this.$element.removeClass(Affix.RESET).addClass('affix') + var scrollTop = this.$target.scrollTop() + var position = this.$element.offset() + return (this.pinnedOffset = position.top - scrollTop) + } + + Affix.prototype.checkPositionWithEventLoop = function () { + setTimeout($.proxy(this.checkPosition, this), 1) + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var height = this.$element.height() + var offset = this.options.offset + var offsetTop = offset.top + var offsetBottom = offset.bottom + var scrollHeight = Math.max($(document).height(), $(document.body).height()) + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) + + var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) + + if (this.affixed != affix) { + if (this.unpin != null) this.$element.css('top', '') + + var affixType = 'affix' + (affix ? '-' + affix : '') + var e = $.Event(affixType + '.bs.affix') + + this.$element.trigger(e) + + if (e.isDefaultPrevented()) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null + + this.$element + .removeClass(Affix.RESET) + .addClass(affixType) + .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') + } + + if (affix == 'bottom') { + this.$element.offset({ + top: scrollHeight - height - offsetBottom + }) + } + } + + + // AFFIX PLUGIN DEFINITION + // ======================= + + function Plugin(option) { + return this.each(function () { + var $this = $(this) + var data = $this.data('bs.affix') + var options = typeof option == 'object' && option + + if (!data) $this.data('bs.affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + var old = $.fn.affix + + $.fn.affix = Plugin + $.fn.affix.Constructor = Affix + + + // AFFIX NO CONFLICT + // ================= + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + // AFFIX DATA-API + // ============== + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + var data = $spy.data() + + data.offset = data.offset || {} + + if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom + if (data.offsetTop != null) data.offset.top = data.offsetTop + + Plugin.call($spy, data) + }) + }) + +}(jQuery); + + +/*! DataTables 1.10.11 + * ©2008-2015 SpryMedia Ltd - datatables.net/license + */ + +/** + * @summary DataTables + * @description Paginate, search and order HTML tables + * @version 1.10.11 + * @file jquery.dataTables.js + * @author SpryMedia Ltd (www.sprymedia.co.uk) + * @contact www.sprymedia.co.uk/contact + * @copyright Copyright 2008-2015 SpryMedia Ltd. + * + * This source file is free software, available under the following license: + * MIT license - http://datatables.net/license + * + * This source file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. + * + * For details please refer to: http://www.datatables.net + */ + +/*jslint evil: true, undef: true, browser: true */ +/*globals $,require,jQuery,define,_selector_run,_selector_opts,_selector_first,_selector_row_indexes,_ext,_Api,_api_register,_api_registerPlural,_re_new_lines,_re_html,_re_formatted_numeric,_re_escape_regex,_empty,_intVal,_numToDecimal,_isNumber,_isHtml,_htmlNumeric,_pluck,_pluck_order,_range,_stripHtml,_unique,_fnBuildAjax,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnAjaxDataSrc,_fnAddColumn,_fnColumnOptions,_fnAdjustColumnSizing,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnVisbleColumns,_fnGetColumns,_fnColumnTypes,_fnApplyColumnDefs,_fnHungarianMap,_fnCamelToHungarian,_fnLanguageCompat,_fnBrowserDetect,_fnAddData,_fnAddTr,_fnNodeToDataIndex,_fnNodeToColumnIndex,_fnGetCellData,_fnSetCellData,_fnSplitObjNotation,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnGetDataMaster,_fnClearTable,_fnDeleteIndex,_fnInvalidate,_fnGetRowElements,_fnCreateTr,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAddOptionsHtml,_fnDetectHeader,_fnGetUniqueThs,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnFilterCreateSearch,_fnEscapeRegex,_fnFilterData,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnInfoMacros,_fnInitialise,_fnInitComplete,_fnLengthChange,_fnFeatureHtmlLength,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnFeatureHtmlTable,_fnScrollDraw,_fnApplyToChildren,_fnCalculateColumnWidths,_fnThrottle,_fnConvertToWidth,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnSortFlatten,_fnSort,_fnSortAria,_fnSortListener,_fnSortAttachListener,_fnSortingClasses,_fnSortData,_fnSaveState,_fnLoadState,_fnSettingsFromNode,_fnLog,_fnMap,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnLengthOverflow,_fnRenderer,_fnDataSource,_fnRowAttributes*/ + +(function( factory ) { + "use strict"; + + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window + require('jquery') : + require('jquery')( root ); + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } +} +(function( $, window, document, undefined ) { + "use strict"; + + /** + * DataTables is a plug-in for the jQuery Javascript library. It is a highly + * flexible tool, based upon the foundations of progressive enhancement, + * which will add advanced interaction controls to any HTML table. For a + * full list of features please refer to + * [DataTables.net](href="http://datatables.net). + * + * Note that the `DataTable` object is not a global variable but is aliased + * to `jQuery.fn.DataTable` and `jQuery.fn.dataTable` through which it may + * be accessed. + * + * @class + * @param {object} [init={}] Configuration object for DataTables. Options + * are defined by {@link DataTable.defaults} + * @requires jQuery 1.7+ + * + * @example + * // Basic initialisation + * $(document).ready( function { + * $('#example').dataTable(); + * } ); + * + * @example + * // Initialisation with configuration options - in this case, disable + * // pagination and sorting. + * $(document).ready( function { + * $('#example').dataTable( { + * "paginate": false, + * "sort": false + * } ); + * } ); + */ + var DataTable; + + + /* + * It is useful to have variables which are scoped locally so only the + * DataTables functions can access them and they don't leak into global space. + * At the same time these functions are often useful over multiple files in the + * core and API, so we list, or at least document, all variables which are used + * by DataTables as private variables here. This also ensures that there is no + * clashing of variable names and that they can easily referenced for reuse. + */ + + + // Defined else where + // _selector_run + // _selector_opts + // _selector_first + // _selector_row_indexes + + var _ext; // DataTable.ext + var _Api; // DataTable.Api + var _api_register; // DataTable.Api.register + var _api_registerPlural; // DataTable.Api.registerPlural + + var _re_dic = {}; + var _re_new_lines = /[\r\n]/g; + var _re_html = /<.*?>/g; + var _re_date_start = /^[\w\+\-]/; + var _re_date_end = /[\w\+\-]$/; + + // Escape regular expression special characters + var _re_escape_regex = new RegExp( '(\\' + [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ].join('|\\') + ')', 'g' ); + + // http://en.wikipedia.org/wiki/Foreign_exchange_market + // - \u20BD - Russian ruble. + // - \u20a9 - South Korean Won + // - \u20BA - Turkish Lira + // - \u20B9 - Indian Rupee + // - R - Brazil (R$) and South Africa + // - fr - Swiss Franc + // - kr - Swedish krona, Norwegian krone and Danish krone + // - \u2009 is thin space and \u202F is narrow no-break space, both used in many + // standards as thousands separators. + var _re_formatted_numeric = /[',$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfk]/gi; + + + var _empty = function ( d ) { + return !d || d === true || d === '-' ? true : false; + }; + + + var _intVal = function ( s ) { + var integer = parseInt( s, 10 ); + return !isNaN(integer) && isFinite(s) ? integer : null; + }; + + // Convert from a formatted number with characters other than `.` as the + // decimal place, to a Javascript number + var _numToDecimal = function ( num, decimalPoint ) { + // Cache created regular expressions for speed as this function is called often + if ( ! _re_dic[ decimalPoint ] ) { + _re_dic[ decimalPoint ] = new RegExp( _fnEscapeRegex( decimalPoint ), 'g' ); + } + return typeof num === 'string' && decimalPoint !== '.' ? + num.replace( /\./g, '' ).replace( _re_dic[ decimalPoint ], '.' ) : + num; + }; + + + var _isNumber = function ( d, decimalPoint, formatted ) { + var strType = typeof d === 'string'; + + // If empty return immediately so there must be a number if it is a + // formatted string (this stops the string "k", or "kr", etc being detected + // as a formatted number for currency + if ( _empty( d ) ) { + return true; + } + + if ( decimalPoint && strType ) { + d = _numToDecimal( d, decimalPoint ); + } + + if ( formatted && strType ) { + d = d.replace( _re_formatted_numeric, '' ); + } + + return !isNaN( parseFloat(d) ) && isFinite( d ); + }; + + + // A string without HTML in it can be considered to be HTML still + var _isHtml = function ( d ) { + return _empty( d ) || typeof d === 'string'; + }; + + + var _htmlNumeric = function ( d, decimalPoint, formatted ) { + if ( _empty( d ) ) { + return true; + } + + var html = _isHtml( d ); + return ! html ? + null : + _isNumber( _stripHtml( d ), decimalPoint, formatted ) ? + true : + null; + }; + + + var _pluck = function ( a, prop, prop2 ) { + var out = []; + var i=0, ien=a.length; + + // Could have the test in the loop for slightly smaller code, but speed + // is essential here + if ( prop2 !== undefined ) { + for ( ; i') + .css( { + position: 'fixed', + top: 0, + left: 0, + height: 1, + width: 1, + overflow: 'hidden' + } ) + .append( + $('
    ') + .css( { + position: 'absolute', + top: 1, + left: 1, + width: 100, + overflow: 'scroll' + } ) + .append( + $('
    ') + .css( { + width: '100%', + height: 10 + } ) + ) + ) + .appendTo( 'body' ); + + var outer = n.children(); + var inner = outer.children(); + + // Numbers below, in order, are: + // inner.offsetWidth, inner.clientWidth, outer.offsetWidth, outer.clientWidth + // + // IE6 XP: 100 100 100 83 + // IE7 Vista: 100 100 100 83 + // IE 8+ Windows: 83 83 100 83 + // Evergreen Windows: 83 83 100 83 + // Evergreen Mac with scrollbars: 85 85 100 85 + // Evergreen Mac without scrollbars: 100 100 100 100 + + // Get scrollbar width + browser.barWidth = outer[0].offsetWidth - outer[0].clientWidth; + + // IE6/7 will oversize a width 100% element inside a scrolling element, to + // include the width of the scrollbar, while other browsers ensure the inner + // element is contained without forcing scrolling + browser.bScrollOversize = inner[0].offsetWidth === 100 && outer[0].clientWidth !== 100; + + // In rtl text layout, some browsers (most, but not all) will place the + // scrollbar on the left, rather than the right. + browser.bScrollbarLeft = Math.round( inner.offset().left ) !== 1; + + // IE8- don't provide height and width for getBoundingClientRect + browser.bBounding = n[0].getBoundingClientRect().width ? true : false; + + n.remove(); + } + + $.extend( settings.oBrowser, DataTable.__browser ); + settings.oScroll.iBarWidth = DataTable.__browser.barWidth; + } + + + /** + * Array.prototype reduce[Right] method, used for browsers which don't support + * JS 1.6. Done this way to reduce code size, since we iterate either way + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnReduce ( that, fn, init, start, end, inc ) + { + var + i = start, + value, + isSet = false; + + if ( init !== undefined ) { + value = init; + isSet = true; + } + + while ( i !== end ) { + if ( ! that.hasOwnProperty(i) ) { + continue; + } + + value = isSet ? + fn( value, that[i], i, that ) : + that[i]; + + isSet = true; + i += inc; + } + + return value; + } + + /** + * Add a column to the list used for the table with default values + * @param {object} oSettings dataTables settings object + * @param {node} nTh The th element for this column + * @memberof DataTable#oApi + */ + function _fnAddColumn( oSettings, nTh ) + { + // Add column to aoColumns array + var oDefaults = DataTable.defaults.column; + var iCol = oSettings.aoColumns.length; + var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, { + "nTh": nTh ? nTh : document.createElement('th'), + "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '', + "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol], + "mData": oDefaults.mData ? oDefaults.mData : iCol, + idx: iCol + } ); + oSettings.aoColumns.push( oCol ); + + // Add search object for column specific search. Note that the `searchCols[ iCol ]` + // passed into extend can be undefined. This allows the user to give a default + // with only some of the parameters defined, and also not give a default + var searchCols = oSettings.aoPreSearchCols; + searchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch, searchCols[ iCol ] ); + + // Use the default column options function to initialise classes etc + _fnColumnOptions( oSettings, iCol, $(nTh).data() ); + } + + + /** + * Apply options for a column + * @param {object} oSettings dataTables settings object + * @param {int} iCol column index to consider + * @param {object} oOptions object with sType, bVisible and bSearchable etc + * @memberof DataTable#oApi + */ + function _fnColumnOptions( oSettings, iCol, oOptions ) + { + var oCol = oSettings.aoColumns[ iCol ]; + var oClasses = oSettings.oClasses; + var th = $(oCol.nTh); + + // Try to get width information from the DOM. We can't get it from CSS + // as we'd need to parse the CSS stylesheet. `width` option can override + if ( ! oCol.sWidthOrig ) { + // Width attribute + oCol.sWidthOrig = th.attr('width') || null; + + // Style attribute + var t = (th.attr('style') || '').match(/width:\s*(\d+[pxem%]+)/); + if ( t ) { + oCol.sWidthOrig = t[1]; + } + } + + /* User specified column options */ + if ( oOptions !== undefined && oOptions !== null ) + { + // Backwards compatibility + _fnCompatCols( oOptions ); + + // Map camel case parameters to their Hungarian counterparts + _fnCamelToHungarian( DataTable.defaults.column, oOptions ); + + /* Backwards compatibility for mDataProp */ + if ( oOptions.mDataProp !== undefined && !oOptions.mData ) + { + oOptions.mData = oOptions.mDataProp; + } + + if ( oOptions.sType ) + { + oCol._sManualType = oOptions.sType; + } + + // `class` is a reserved word in Javascript, so we need to provide + // the ability to use a valid name for the camel case input + if ( oOptions.className && ! oOptions.sClass ) + { + oOptions.sClass = oOptions.className; + } + + $.extend( oCol, oOptions ); + _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" ); + + /* iDataSort to be applied (backwards compatibility), but aDataSort will take + * priority if defined + */ + if ( oOptions.iDataSort !== undefined ) + { + oCol.aDataSort = [ oOptions.iDataSort ]; + } + _fnMap( oCol, oOptions, "aDataSort" ); + } + + /* Cache the data get and set functions for speed */ + var mDataSrc = oCol.mData; + var mData = _fnGetObjectDataFn( mDataSrc ); + var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; + + var attrTest = function( src ) { + return typeof src === 'string' && src.indexOf('@') !== -1; + }; + oCol._bAttrSrc = $.isPlainObject( mDataSrc ) && ( + attrTest(mDataSrc.sort) || attrTest(mDataSrc.type) || attrTest(mDataSrc.filter) + ); + oCol._setter = null; + + oCol.fnGetData = function (rowData, type, meta) { + var innerData = mData( rowData, type, undefined, meta ); + + return mRender && type ? + mRender( innerData, type, rowData, meta ) : + innerData; + }; + oCol.fnSetData = function ( rowData, val, meta ) { + return _fnSetObjectDataFn( mDataSrc )( rowData, val, meta ); + }; + + // Indicate if DataTables should read DOM data as an object or array + // Used in _fnGetRowElements + if ( typeof mDataSrc !== 'number' ) { + oSettings._rowReadObject = true; + } + + /* Feature sorting overrides column specific when off */ + if ( !oSettings.oFeatures.bSort ) + { + oCol.bSortable = false; + th.addClass( oClasses.sSortableNone ); // Have to add class here as order event isn't called + } + + /* Check that the class assignment is correct for sorting */ + var bAsc = $.inArray('asc', oCol.asSorting) !== -1; + var bDesc = $.inArray('desc', oCol.asSorting) !== -1; + if ( !oCol.bSortable || (!bAsc && !bDesc) ) + { + oCol.sSortingClass = oClasses.sSortableNone; + oCol.sSortingClassJUI = ""; + } + else if ( bAsc && !bDesc ) + { + oCol.sSortingClass = oClasses.sSortableAsc; + oCol.sSortingClassJUI = oClasses.sSortJUIAscAllowed; + } + else if ( !bAsc && bDesc ) + { + oCol.sSortingClass = oClasses.sSortableDesc; + oCol.sSortingClassJUI = oClasses.sSortJUIDescAllowed; + } + else + { + oCol.sSortingClass = oClasses.sSortable; + oCol.sSortingClassJUI = oClasses.sSortJUI; + } + } + + + /** + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! + * @param {object} settings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnAdjustColumnSizing ( settings ) + { + /* Not interested in doing column width calculation if auto-width is disabled */ + if ( settings.oFeatures.bAutoWidth !== false ) + { + var columns = settings.aoColumns; + + _fnCalculateColumnWidths( settings ); + for ( var i=0 , iLen=columns.length ; i