diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index a2d5e5ce4..02e4be14d 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -171,6 +171,12 @@ ..\packages\Prowlin 0.9.4163.39219\Prowlin.dll + + ..\packages\SignalR.Server.0.4.0.0\lib\net40\SignalR.dll + + + ..\packages\SignalR.Hosting.AspNet.0.4.0.0\lib\net40\SignalR.Hosting.AspNet.dll + @@ -299,6 +305,7 @@ + diff --git a/NzbDrone.Core/Providers/DownloadProvider.cs b/NzbDrone.Core/Providers/DownloadProvider.cs index 4912b4640..426288867 100644 --- a/NzbDrone.Core/Providers/DownloadProvider.cs +++ b/NzbDrone.Core/Providers/DownloadProvider.cs @@ -17,13 +17,15 @@ namespace NzbDrone.Core.Providers private readonly ExternalNotificationProvider _externalNotificationProvider; private readonly ConfigProvider _configProvider; private readonly BlackholeProvider _blackholeProvider; + private readonly SignalRProvider _signalRProvider; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); [Inject] public DownloadProvider(SabProvider sabProvider, HistoryProvider historyProvider, EpisodeProvider episodeProvider, ExternalNotificationProvider externalNotificationProvider, - ConfigProvider configProvider, BlackholeProvider blackholeProvider) + ConfigProvider configProvider, BlackholeProvider blackholeProvider, + SignalRProvider signalRProvider) { _sabProvider = sabProvider; _historyProvider = historyProvider; @@ -31,6 +33,7 @@ namespace NzbDrone.Core.Providers _externalNotificationProvider = externalNotificationProvider; _configProvider = configProvider; _blackholeProvider = blackholeProvider; + _signalRProvider = signalRProvider; } public DownloadProvider() @@ -63,6 +66,8 @@ namespace NzbDrone.Core.Providers _historyProvider.Add(history); _episodeProvider.MarkEpisodeAsFetched(episode.EpisodeId); + + _signalRProvider.UpdateEpisodeStatus(episode.EpisodeId, EpisodeStatusType.Downloading); } _externalNotificationProvider.OnGrab(downloadTitle); diff --git a/NzbDrone.Core/Providers/SignalRProvider.cs b/NzbDrone.Core/Providers/SignalRProvider.cs new file mode 100644 index 000000000..ee96d33c6 --- /dev/null +++ b/NzbDrone.Core/Providers/SignalRProvider.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Core.Model; +using SignalR; +using SignalR.Hosting.AspNet; +using SignalR.Hubs; +using SignalR.Infrastructure; + +namespace NzbDrone.Core.Providers +{ + public class SignalRProvider : Hub + { + private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); + + public virtual void UpdateEpisodeStatus(int episodeId, EpisodeStatusType episodeStatus) + { + Logger.Trace("Sending Status update to client. EpisodeId: {0}, Status: {1}", episodeId, episodeStatus); + + GetClients().updatedStatus(episodeId, episodeStatus.ToString()); + } + + private static dynamic GetClients() + { + IConnectionManager connectionManager = AspNetHost.DependencyResolver.Resolve(); + return connectionManager.GetClients(); + } + } +} diff --git a/NzbDrone.Core/packages.config b/NzbDrone.Core/packages.config index 5df46aac9..2039b67b7 100644 --- a/NzbDrone.Core/packages.config +++ b/NzbDrone.Core/packages.config @@ -9,6 +9,8 @@ + + diff --git a/NzbDrone.Web/NzbDrone.Web.csproj b/NzbDrone.Web/NzbDrone.Web.csproj index e1ac8ee66..c87b9fcaf 100644 --- a/NzbDrone.Web/NzbDrone.Web.csproj +++ b/NzbDrone.Web/NzbDrone.Web.csproj @@ -59,6 +59,9 @@ ..\packages\MiniProfiler.1.9\lib\net40\MvcMiniProfiler.dll + + ..\packages\Newtonsoft.Json.4.0.8\lib\net40\Newtonsoft.Json.dll + ..\packages\Ninject.2.2.1.4\lib\net40-Full\Ninject.dll @@ -68,6 +71,12 @@ ..\packages\NLog.2.0.0.2000\lib\net40\NLog.dll + + ..\packages\SignalR.Server.0.4.0.0\lib\net40\SignalR.dll + + + ..\packages\SignalR.Hosting.AspNet.0.4.0.0\lib\net40\SignalR.Hosting.AspNet.dll + @@ -325,13 +334,14 @@ - + + diff --git a/NzbDrone.Web/Scripts/DataTables-1.9.0/media/js/jquery.validate.js b/NzbDrone.Web/Scripts/DataTables-1.9.0/media/js/jquery.validate.js deleted file mode 100644 index b2d48c060..000000000 --- a/NzbDrone.Web/Scripts/DataTables-1.9.0/media/js/jquery.validate.js +++ /dev/null @@ -1,1150 +0,0 @@ -/* -* Note: While Microsoft is not the author of this file, Microsoft is -* offering you a license subject to the terms of the Microsoft Software -* License Terms for Microsoft ASP.NET Model View Controller 3. -* Microsoft reserves all other rights. The notices below are provided -* for informational purposes only and are not the license terms under -* which Microsoft distributed this file. -* -* jQuery validation plug-in 1.7 -* -* http://bassistance.de/jquery-plugins/jquery-plugin-validation/ -* http://docs.jquery.com/Plugins/Validation -* -* Copyright (c) 2006 - 2008 Jörn Zaefferer -* -* $Id: jquery.validate.js 6403 2009-06-17 14:27:16Z joern.zaefferer $ -* -*/ - -(function($) { - -$.extend($.fn, { - // http://docs.jquery.com/Plugins/Validation/validate - validate: function( options ) { - - // if nothing is selected, return nothing; can't chain anyway - if (!this.length) { - options && options.debug && window.console && console.warn( "nothing selected, can't validate, returning nothing" ); - return; - } - - // check if a validator for this form was already created - var validator = $.data(this[0], 'validator'); - if ( validator ) { - return validator; - } - - validator = new $.validator( options, this[0] ); - $.data(this[0], 'validator', validator); - - if ( validator.settings.onsubmit ) { - - // allow suppresing validation by adding a cancel class to the submit button - this.find("input, button").filter(".cancel").click(function() { - validator.cancelSubmit = true; - }); - - // when a submitHandler is used, capture the submitting button - if (validator.settings.submitHandler) { - this.find("input, button").filter(":submit").click(function() { - validator.submitButton = this; - }); - } - - // validate the form on submit - this.submit( function( event ) { - if ( validator.settings.debug ) - // prevent form submit to be able to see console output - event.preventDefault(); - - function handle() { - if ( validator.settings.submitHandler ) { - if (validator.submitButton) { - // insert a hidden input as a replacement for the missing submit button - var hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); - } - validator.settings.submitHandler.call( validator, validator.currentForm ); - if (validator.submitButton) { - // and clean up afterwards; thanks to no-block-scope, hidden can be referenced - hidden.remove(); - } - return false; - } - return true; - } - - // prevent submit for invalid forms or custom submit handlers - if ( validator.cancelSubmit ) { - validator.cancelSubmit = false; - return handle(); - } - if ( validator.form() ) { - if ( validator.pendingRequest ) { - validator.formSubmitted = true; - return false; - } - return handle(); - } else { - validator.focusInvalid(); - return false; - } - }); - } - - return validator; - }, - // http://docs.jquery.com/Plugins/Validation/valid - valid: function() { - if ( $(this[0]).is('form')) { - return this.validate().form(); - } else { - var valid = true; - var validator = $(this[0].form).validate(); - this.each(function() { - valid &= validator.element(this); - }); - return valid; - } - }, - // attributes: space seperated list of attributes to retrieve and remove - removeAttrs: function(attributes) { - var result = {}, - $element = this; - $.each(attributes.split(/\s/), function(index, value) { - result[value] = $element.attr(value); - $element.removeAttr(value); - }); - return result; - }, - // http://docs.jquery.com/Plugins/Validation/rules - rules: function(command, argument) { - var element = this[0]; - - if (command) { - var settings = $.data(element.form, 'validator').settings; - var staticRules = settings.rules; - var existingRules = $.validator.staticRules(element); - switch(command) { - case "add": - $.extend(existingRules, $.validator.normalizeRule(argument)); - staticRules[element.name] = existingRules; - if (argument.messages) - settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); - break; - case "remove": - if (!argument) { - delete staticRules[element.name]; - return existingRules; - } - var filtered = {}; - $.each(argument.split(/\s/), function(index, method) { - filtered[method] = existingRules[method]; - delete existingRules[method]; - }); - return filtered; - } - } - - var data = $.validator.normalizeRules( - $.extend( - {}, - $.validator.metadataRules(element), - $.validator.classRules(element), - $.validator.attributeRules(element), - $.validator.staticRules(element) - ), element); - - // make sure required is at front - if (data.required) { - var param = data.required; - delete data.required; - data = $.extend({required: param}, data); - } - - return data; - } -}); - -// Custom selectors -$.extend($.expr[":"], { - // http://docs.jquery.com/Plugins/Validation/blank - blank: function(a) {return !$.trim("" + a.value);}, - // http://docs.jquery.com/Plugins/Validation/filled - filled: function(a) {return !!$.trim("" + a.value);}, - // http://docs.jquery.com/Plugins/Validation/unchecked - unchecked: function(a) {return !a.checked;} -}); - -// constructor for validator -$.validator = function( options, form ) { - this.settings = $.extend( true, {}, $.validator.defaults, options ); - this.currentForm = form; - this.init(); -}; - -$.validator.format = function(source, params) { - if ( arguments.length == 1 ) - return function() { - var args = $.makeArray(arguments); - args.unshift(source); - return $.validator.format.apply( this, args ); - }; - if ( arguments.length > 2 && params.constructor != Array ) { - params = $.makeArray(arguments).slice(1); - } - if ( params.constructor != Array ) { - params = [ params ]; - } - $.each(params, function(i, n) { - source = source.replace(new RegExp("\\{" + i + "\\}", "g"), n); - }); - return source; -}; - -$.extend($.validator, { - - defaults: { - messages: {}, - groups: {}, - rules: {}, - errorClass: "error", - validClass: "valid", - errorElement: "label", - focusInvalid: true, - errorContainer: $( [] ), - errorLabelContainer: $( [] ), - onsubmit: true, - ignore: [], - ignoreTitle: false, - onfocusin: function(element) { - this.lastActive = element; - - // hide error label and remove error class on focus if enabled - if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { - this.settings.unhighlight && this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); - this.errorsFor(element).hide(); - } - }, - onfocusout: function(element) { - if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { - this.element(element); - } - }, - onkeyup: function(element) { - if ( element.name in this.submitted || element == this.lastElement ) { - this.element(element); - } - }, - onclick: function(element) { - // click on selects, radiobuttons and checkboxes - if ( element.name in this.submitted ) - this.element(element); - // or option elements, check parent select in that case - else if (element.parentNode.name in this.submitted) - this.element(element.parentNode); - }, - highlight: function( element, errorClass, validClass ) { - $(element).addClass(errorClass).removeClass(validClass); - }, - unhighlight: function( element, errorClass, validClass ) { - $(element).removeClass(errorClass).addClass(validClass); - } - }, - - // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults - setDefaults: function(settings) { - $.extend( $.validator.defaults, settings ); - }, - - messages: { - required: "This field is required.", - remote: "Please fix this field.", - email: "Please enter a valid email address.", - url: "Please enter a valid URL.", - date: "Please enter a valid date.", - dateISO: "Please enter a valid date (ISO).", - number: "Please enter a valid number.", - digits: "Please enter only digits.", - creditcard: "Please enter a valid credit card number.", - equalTo: "Please enter the same value again.", - accept: "Please enter a value with a valid extension.", - maxlength: $.validator.format("Please enter no more than {0} characters."), - minlength: $.validator.format("Please enter at least {0} characters."), - rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), - range: $.validator.format("Please enter a value between {0} and {1}."), - max: $.validator.format("Please enter a value less than or equal to {0}."), - min: $.validator.format("Please enter a value greater than or equal to {0}.") - }, - - autoCreateRanges: false, - - prototype: { - - init: function() { - this.labelContainer = $(this.settings.errorLabelContainer); - this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); - this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); - this.submitted = {}; - this.valueCache = {}; - this.pendingRequest = 0; - this.pending = {}; - this.invalid = {}; - this.reset(); - - var groups = (this.groups = {}); - $.each(this.settings.groups, function(key, value) { - $.each(value.split(/\s/), function(index, name) { - groups[name] = key; - }); - }); - var rules = this.settings.rules; - $.each(rules, function(key, value) { - rules[key] = $.validator.normalizeRule(value); - }); - - function delegate(event) { - var validator = $.data(this[0].form, "validator"), - eventType = "on" + event.type.replace(/^validate/, ""); - validator.settings[eventType] && validator.settings[eventType].call(validator, this[0] ); - } - $(this.currentForm) - .validateDelegate(":text, :password, :file, select, textarea", "focusin focusout keyup", delegate) - .validateDelegate(":radio, :checkbox, select, option", "click", delegate); - - if (this.settings.invalidHandler) - $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); - }, - - // http://docs.jquery.com/Plugins/Validation/Validator/form - form: function() { - this.checkForm(); - $.extend(this.submitted, this.errorMap); - this.invalid = $.extend({}, this.errorMap); - if (!this.valid()) - $(this.currentForm).triggerHandler("invalid-form", [this]); - this.showErrors(); - return this.valid(); - }, - - checkForm: function() { - this.prepareForm(); - for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { - this.check( elements[i] ); - } - return this.valid(); - }, - - // http://docs.jquery.com/Plugins/Validation/Validator/element - element: function( element ) { - element = this.clean( element ); - this.lastElement = element; - this.prepareElement( element ); - this.currentElements = $(element); - var result = this.check( element ); - if ( result ) { - delete this.invalid[element.name]; - } else { - this.invalid[element.name] = true; - } - if ( !this.numberOfInvalids() ) { - // Hide error containers on last error - this.toHide = this.toHide.add( this.containers ); - } - this.showErrors(); - return result; - }, - - // http://docs.jquery.com/Plugins/Validation/Validator/showErrors - showErrors: function(errors) { - if(errors) { - // add items to error list and map - $.extend( this.errorMap, errors ); - this.errorList = []; - for ( var name in errors ) { - this.errorList.push({ - message: errors[name], - element: this.findByName(name)[0] - }); - } - // remove items from success list - this.successList = $.grep( this.successList, function(element) { - return !(element.name in errors); - }); - } - this.settings.showErrors - ? this.settings.showErrors.call( this, this.errorMap, this.errorList ) - : this.defaultShowErrors(); - }, - - // http://docs.jquery.com/Plugins/Validation/Validator/resetForm - resetForm: function() { - if ( $.fn.resetForm ) - $( this.currentForm ).resetForm(); - this.submitted = {}; - this.prepareForm(); - this.hideErrors(); - this.elements().removeClass( this.settings.errorClass ); - }, - - numberOfInvalids: function() { - return this.objectLength(this.invalid); - }, - - objectLength: function( obj ) { - var count = 0; - for ( var i in obj ) - count++; - return count; - }, - - hideErrors: function() { - this.addWrapper( this.toHide ).hide(); - }, - - valid: function() { - return this.size() == 0; - }, - - size: function() { - return this.errorList.length; - }, - - focusInvalid: function() { - if( this.settings.focusInvalid ) { - try { - $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) - .filter(":visible") - .focus() - // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find - .trigger("focusin"); - } catch(e) { - // ignore IE throwing errors when focusing hidden elements - } - } - }, - - findLastActive: function() { - var lastActive = this.lastActive; - return lastActive && $.grep(this.errorList, function(n) { - return n.element.name == lastActive.name; - }).length == 1 && lastActive; - }, - - elements: function() { - var validator = this, - rulesCache = {}; - - // select all valid inputs inside the form (no submit or reset buttons) - // workaround $Query([]).add until http://dev.jquery.com/ticket/2114 is solved - return $([]).add(this.currentForm.elements) - .filter(":input") - .not(":submit, :reset, :image, [disabled]") - .not( this.settings.ignore ) - .filter(function() { - !this.name && validator.settings.debug && window.console && console.error( "%o has no name assigned", this); - - // select only the first element for each name, and only those with rules specified - if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) - return false; - - rulesCache[this.name] = true; - return true; - }); - }, - - clean: function( selector ) { - return $( selector )[0]; - }, - - errors: function() { - return $( this.settings.errorElement + "." + this.settings.errorClass, this.errorContext ); - }, - - reset: function() { - this.successList = []; - this.errorList = []; - this.errorMap = {}; - this.toShow = $([]); - this.toHide = $([]); - this.currentElements = $([]); - }, - - prepareForm: function() { - this.reset(); - this.toHide = this.errors().add( this.containers ); - }, - - prepareElement: function( element ) { - this.reset(); - this.toHide = this.errorsFor(element); - }, - - check: function( element ) { - element = this.clean( element ); - - // if radio/checkbox, validate first element in group instead - if (this.checkable(element)) { - element = this.findByName( element.name )[0]; - } - - var rules = $(element).rules(); - var dependencyMismatch = false; - for( method in rules ) { - var rule = { method: method, parameters: rules[method] }; - try { - var result = $.validator.methods[method].call( this, element.value.replace(/\r/g, ""), element, rule.parameters ); - - // if a method indicates that the field is optional and therefore valid, - // don't mark it as valid when there are no other rules - if ( result == "dependency-mismatch" ) { - dependencyMismatch = true; - continue; - } - dependencyMismatch = false; - - if ( result == "pending" ) { - this.toHide = this.toHide.not( this.errorsFor(element) ); - return; - } - - if( !result ) { - this.formatAndAdd( element, rule ); - return false; - } - } catch(e) { - this.settings.debug && window.console && console.log("exception occured when checking element " + element.id - + ", check the '" + rule.method + "' method", e); - throw e; - } - } - if (dependencyMismatch) - return; - if ( this.objectLength(rules) ) - this.successList.push(element); - return true; - }, - - // return the custom message for the given element and validation method - // specified in the element's "messages" metadata - customMetaMessage: function(element, method) { - if (!$.metadata) - return; - - var meta = this.settings.meta - ? $(element).metadata()[this.settings.meta] - : $(element).metadata(); - - return meta && meta.messages && meta.messages[method]; - }, - - // return the custom message for the given element name and validation method - customMessage: function( name, method ) { - var m = this.settings.messages[name]; - return m && (m.constructor == String - ? m - : m[method]); - }, - - // return the first defined argument, allowing empty strings - findDefined: function() { - for(var i = 0; i < arguments.length; i++) { - if (arguments[i] !== undefined) - return arguments[i]; - } - return undefined; - }, - - defaultMessage: function( element, method) { - return this.findDefined( - this.customMessage( element.name, method ), - this.customMetaMessage( element, method ), - // title is never undefined, so handle empty string as undefined - !this.settings.ignoreTitle && element.title || undefined, - $.validator.messages[method], - "Warning: No message defined for " + element.name + "" - ); - }, - - formatAndAdd: function( element, rule ) { - var message = this.defaultMessage( element, rule.method ), - theregex = /\$?\{(\d+)\}/g; - if ( typeof message == "function" ) { - message = message.call(this, rule.parameters, element); - } else if (theregex.test(message)) { - message = jQuery.format(message.replace(theregex, '{$1}'), rule.parameters); - } - this.errorList.push({ - message: message, - element: element - }); - - this.errorMap[element.name] = message; - this.submitted[element.name] = message; - }, - - addWrapper: function(toToggle) { - if ( this.settings.wrapper ) - toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); - return toToggle; - }, - - defaultShowErrors: function() { - for ( var i = 0; this.errorList[i]; i++ ) { - var error = this.errorList[i]; - this.settings.highlight && this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); - this.showLabel( error.element, error.message ); - } - if( this.errorList.length ) { - this.toShow = this.toShow.add( this.containers ); - } - if (this.settings.success) { - for ( var i = 0; this.successList[i]; i++ ) { - this.showLabel( this.successList[i] ); - } - } - if (this.settings.unhighlight) { - for ( var i = 0, elements = this.validElements(); elements[i]; i++ ) { - this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); - } - } - this.toHide = this.toHide.not( this.toShow ); - this.hideErrors(); - this.addWrapper( this.toShow ).show(); - }, - - validElements: function() { - return this.currentElements.not(this.invalidElements()); - }, - - invalidElements: function() { - return $(this.errorList).map(function() { - return this.element; - }); - }, - - showLabel: function(element, message) { - var label = this.errorsFor( element ); - if ( label.length ) { - // refresh error/success class - label.removeClass().addClass( this.settings.errorClass ); - - // check if we have a generated label, replace the message then - label.attr("generated") && label.html(message); - } else { - // create label - label = $("<" + this.settings.errorElement + "/>") - .attr({"for": this.idOrName(element), generated: true}) - .addClass(this.settings.errorClass) - .html(message || ""); - if ( this.settings.wrapper ) { - // make sure the element is visible, even in IE - // actually showing the wrapped element is handled elsewhere - label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); - } - if ( !this.labelContainer.append(label).length ) - this.settings.errorPlacement - ? this.settings.errorPlacement(label, $(element) ) - : label.insertAfter(element); - } - if ( !message && this.settings.success ) { - label.text(""); - typeof this.settings.success == "string" - ? label.addClass( this.settings.success ) - : this.settings.success( label ); - } - this.toShow = this.toShow.add(label); - }, - - errorsFor: function(element) { - var name = this.idOrName(element); - return this.errors().filter(function() { - return $(this).attr('for') == name; - }); - }, - - idOrName: function(element) { - return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); - }, - - checkable: function( element ) { - return /radio|checkbox/i.test(element.type); - }, - - findByName: function( name ) { - // select by name and filter by form for performance over form.find("[name=...]") - var form = this.currentForm; - return $(document.getElementsByName(name)).map(function(index, element) { - return element.form == form && element.name == name && element || null; - }); - }, - - getLength: function(value, element) { - switch( element.nodeName.toLowerCase() ) { - case 'select': - return $("option:selected", element).length; - case 'input': - if( this.checkable( element) ) - return this.findByName(element.name).filter(':checked').length; - } - return value.length; - }, - - depend: function(param, element) { - return this.dependTypes[typeof param] - ? this.dependTypes[typeof param](param, element) - : true; - }, - - dependTypes: { - "boolean": function(param, element) { - return param; - }, - "string": function(param, element) { - return !!$(param, element.form).length; - }, - "function": function(param, element) { - return param(element); - } - }, - - optional: function(element) { - return !$.validator.methods.required.call(this, $.trim(element.value), element) && "dependency-mismatch"; - }, - - startRequest: function(element) { - if (!this.pending[element.name]) { - this.pendingRequest++; - this.pending[element.name] = true; - } - }, - - stopRequest: function(element, valid) { - this.pendingRequest--; - // sometimes synchronization fails, make sure pendingRequest is never < 0 - if (this.pendingRequest < 0) - this.pendingRequest = 0; - delete this.pending[element.name]; - if ( valid && this.pendingRequest == 0 && this.formSubmitted && this.form() ) { - $(this.currentForm).submit(); - this.formSubmitted = false; - } else if (!valid && this.pendingRequest == 0 && this.formSubmitted) { - $(this.currentForm).triggerHandler("invalid-form", [this]); - this.formSubmitted = false; - } - }, - - previousValue: function(element) { - return $.data(element, "previousValue") || $.data(element, "previousValue", { - old: null, - valid: true, - message: this.defaultMessage( element, "remote" ) - }); - } - - }, - - classRuleSettings: { - required: {required: true}, - email: {email: true}, - url: {url: true}, - date: {date: true}, - dateISO: {dateISO: true}, - dateDE: {dateDE: true}, - number: {number: true}, - numberDE: {numberDE: true}, - digits: {digits: true}, - creditcard: {creditcard: true} - }, - - addClassRules: function(className, rules) { - className.constructor == String ? - this.classRuleSettings[className] = rules : - $.extend(this.classRuleSettings, className); - }, - - classRules: function(element) { - var rules = {}; - var classes = $(element).attr('class'); - classes && $.each(classes.split(' '), function() { - if (this in $.validator.classRuleSettings) { - $.extend(rules, $.validator.classRuleSettings[this]); - } - }); - return rules; - }, - - attributeRules: function(element) { - var rules = {}; - var $element = $(element); - - for (method in $.validator.methods) { - var value = $element.attr(method); - if (value) { - rules[method] = value; - } - } - - // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs - if (rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength)) { - delete rules.maxlength; - } - - return rules; - }, - - metadataRules: function(element) { - if (!$.metadata) return {}; - - var meta = $.data(element.form, 'validator').settings.meta; - return meta ? - $(element).metadata()[meta] : - $(element).metadata(); - }, - - staticRules: function(element) { - var rules = {}; - var validator = $.data(element.form, 'validator'); - if (validator.settings.rules) { - rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; - } - return rules; - }, - - normalizeRules: function(rules, element) { - // handle dependency check - $.each(rules, function(prop, val) { - // ignore rule when param is explicitly false, eg. required:false - if (val === false) { - delete rules[prop]; - return; - } - if (val.param || val.depends) { - var keepRule = true; - switch (typeof val.depends) { - case "string": - keepRule = !!$(val.depends, element.form).length; - break; - case "function": - keepRule = val.depends.call(element, element); - break; - } - if (keepRule) { - rules[prop] = val.param !== undefined ? val.param : true; - } else { - delete rules[prop]; - } - } - }); - - // evaluate parameters - $.each(rules, function(rule, parameter) { - rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; - }); - - // clean number parameters - $.each(['minlength', 'maxlength', 'min', 'max'], function() { - if (rules[this]) { - rules[this] = Number(rules[this]); - } - }); - $.each(['rangelength', 'range'], function() { - if (rules[this]) { - rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; - } - }); - - if ($.validator.autoCreateRanges) { - // auto-create ranges - if (rules.min && rules.max) { - rules.range = [rules.min, rules.max]; - delete rules.min; - delete rules.max; - } - if (rules.minlength && rules.maxlength) { - rules.rangelength = [rules.minlength, rules.maxlength]; - delete rules.minlength; - delete rules.maxlength; - } - } - - // To support custom messages in metadata ignore rule methods titled "messages" - if (rules.messages) { - delete rules.messages; - } - - return rules; - }, - - // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} - normalizeRule: function(data) { - if( typeof data == "string" ) { - var transformed = {}; - $.each(data.split(/\s/), function() { - transformed[this] = true; - }); - data = transformed; - } - return data; - }, - - // http://docs.jquery.com/Plugins/Validation/Validator/addMethod - addMethod: function(name, method, message) { - $.validator.methods[name] = method; - $.validator.messages[name] = message != undefined ? message : $.validator.messages[name]; - if (method.length < 3) { - $.validator.addClassRules(name, $.validator.normalizeRule(name)); - } - }, - - methods: { - - // http://docs.jquery.com/Plugins/Validation/Methods/required - required: function(value, element, param) { - // check if dependency is met - if ( !this.depend(param, element) ) - return "dependency-mismatch"; - switch( element.nodeName.toLowerCase() ) { - case 'select': - // could be an array for select-multiple or a string, both are fine this way - var val = $(element).val(); - return val && val.length > 0; - case 'input': - if ( this.checkable(element) ) - return this.getLength(value, element) > 0; - default: - return $.trim(value).length > 0; - } - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/remote - remote: function(value, element, param) { - if ( this.optional(element) ) - return "dependency-mismatch"; - - var previous = this.previousValue(element); - if (!this.settings.messages[element.name] ) - this.settings.messages[element.name] = {}; - previous.originalMessage = this.settings.messages[element.name].remote; - this.settings.messages[element.name].remote = previous.message; - - param = typeof param == "string" && {url:param} || param; - - if ( previous.old !== value ) { - previous.old = value; - var validator = this; - this.startRequest(element); - var data = {}; - data[element.name] = value; - $.ajax($.extend(true, { - url: param, - mode: "abort", - port: "validate" + element.name, - dataType: "json", - data: data, - success: function(response) { - validator.settings.messages[element.name].remote = previous.originalMessage; - var valid = response === true; - if ( valid ) { - var submitted = validator.formSubmitted; - validator.prepareElement(element); - validator.formSubmitted = submitted; - validator.successList.push(element); - validator.showErrors(); - } else { - var errors = {}; - var message = (previous.message = response || validator.defaultMessage( element, "remote" )); - errors[element.name] = $.isFunction(message) ? message(value) : message; - validator.showErrors(errors); - } - previous.valid = valid; - validator.stopRequest(element, valid); - } - }, param)); - return "pending"; - } else if( this.pending[element.name] ) { - return "pending"; - } - return previous.valid; - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/minlength - minlength: function(value, element, param) { - return this.optional(element) || this.getLength($.trim(value), element) >= param; - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/maxlength - maxlength: function(value, element, param) { - return this.optional(element) || this.getLength($.trim(value), element) <= param; - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/rangelength - rangelength: function(value, element, param) { - var length = this.getLength($.trim(value), element); - return this.optional(element) || ( length >= param[0] && length <= param[1] ); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/min - min: function( value, element, param ) { - return this.optional(element) || value >= param; - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/max - max: function( value, element, param ) { - return this.optional(element) || value <= param; - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/range - range: function( value, element, param ) { - return this.optional(element) || ( value >= param[0] && value <= param[1] ); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/email - email: function(value, element) { - // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ - return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?$/i.test(value); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/url - url: function(value, element) { - // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ - return this.optional(element) || /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/date - date: function(value, element) { - return this.optional(element) || !/Invalid|NaN/.test(new Date(value)); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/dateISO - dateISO: function(value, element) { - return this.optional(element) || /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(value); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/number - number: function(value, element) { - return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/.test(value); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/digits - digits: function(value, element) { - return this.optional(element) || /^\d+$/.test(value); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/creditcard - // based on http://en.wikipedia.org/wiki/Luhn - creditcard: function(value, element) { - if ( this.optional(element) ) - return "dependency-mismatch"; - // accept only digits and dashes - if (/[^0-9-]+/.test(value)) - return false; - var nCheck = 0, - nDigit = 0, - bEven = false; - - value = value.replace(/\D/g, ""); - - for (var n = value.length - 1; n >= 0; n--) { - var cDigit = value.charAt(n); - var nDigit = parseInt(cDigit, 10); - if (bEven) { - if ((nDigit *= 2) > 9) - nDigit -= 9; - } - nCheck += nDigit; - bEven = !bEven; - } - - return (nCheck % 10) == 0; - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/accept - accept: function(value, element, param) { - param = typeof param == "string" ? param.replace(/,/g, '|') : "png|jpe?g|gif"; - return this.optional(element) || value.match(new RegExp(".(" + param + ")$", "i")); - }, - - // http://docs.jquery.com/Plugins/Validation/Methods/equalTo - equalTo: function(value, element, param) { - // bind to the blur event of the target in order to revalidate whenever the target field is updated - // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead - var target = $(param).unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { - $(element).valid(); - }); - return value == target.val(); - } - - } - -}); - -// deprecated, use $.validator.format instead -$.format = $.validator.format; - -})(jQuery); - -// ajax mode: abort -// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); -// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() -;(function($) { - var ajax = $.ajax; - var pendingRequests = {}; - $.ajax = function(settings) { - // create settings for compatibility with ajaxSetup - settings = $.extend(settings, $.extend({}, $.ajaxSettings, settings)); - var port = settings.port; - if (settings.mode == "abort") { - if ( pendingRequests[port] ) { - pendingRequests[port].abort(); - } - return (pendingRequests[port] = ajax.apply(this, arguments)); - } - return ajax.apply(this, arguments); - }; -})(jQuery); - -// provides cross-browser focusin and focusout events -// IE has native support, in other browsers, use event caputuring (neither bubbles) - -// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation -// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target -;(function($) { - // only implement if not provided by jQuery core (since 1.4) - // TODO verify if jQuery 1.4's implementation is compatible with older jQuery special-event APIs - if (!jQuery.event.special.focusin && !jQuery.event.special.focusout && document.addEventListener) { - $.each({ - focus: 'focusin', - blur: 'focusout' - }, function( original, fix ){ - $.event.special[fix] = { - setup:function() { - this.addEventListener( original, handler, true ); - }, - teardown:function() { - this.removeEventListener( original, handler, true ); - }, - handler: function(e) { - arguments[0] = $.event.fix(e); - arguments[0].type = fix; - return $.event.handle.apply(this, arguments); - } - }; - function handler(e) { - e = $.event.fix(e); - e.type = fix; - return $.event.handle.call(this, e); - } - }); - }; - $.extend($.fn, { - validateDelegate: function(delegate, type, handler) { - return this.bind(type, function(event) { - var target = $(event.target); - if (target.is(delegate)) { - return handler.apply(target, arguments); - } - }); - } - }); -})(jQuery); diff --git a/NzbDrone.Web/Scripts/NzbDrone/grid.js b/NzbDrone.Web/Scripts/NzbDrone/grid.js index 94bcaceda..481c5482a 100644 --- a/NzbDrone.Web/Scripts/NzbDrone/grid.js +++ b/NzbDrone.Web/Scripts/NzbDrone/grid.js @@ -62,4 +62,36 @@ function redrawGrid() { //Force reload using Ajax Binding (bServerSide == false) function reloadGrid() { oTable.fnReloadAjax(); -} \ No newline at end of file +} + + +//SignalR +$(function () { + // Proxy created on the fly + var signalRProvider = $.connection.signalRProvider; + + // Declare a function on the chat hub so the server can invoke it + signalRProvider.updatedStatus = function (episodeId, episodeStatus) { + var imageSrc = '../../Content/Images/' + episodeStatus + '.png'; + var row = $('tr.episodeId_' + episodeId); + + if (row.length == 0) + return; + + var statusImage = $(row).find('img.statusImage'); + + if (statusImage.length == 0) + return; + + statusImage.attr('alt', episodeStatus); + statusImage.attr('title', episodeStatus); + statusImage.attr('src', imageSrc); + + if (episodeStatus != "Missing") { + statusImage.parent('td').removeClass('episodeMissing'); + } + }; + + // Start the connection + $.connection.hub.start(); +}); \ No newline at end of file diff --git a/NzbDrone.Web/Scripts/jquery.signalR.js b/NzbDrone.Web/Scripts/jquery.signalR.js new file mode 100644 index 000000000..c877e0577 --- /dev/null +++ b/NzbDrone.Web/Scripts/jquery.signalR.js @@ -0,0 +1,885 @@ +/// +(function ($, window) { + /// + "use strict"; + + if (typeof ($) !== "function") { + // no jQuery! + throw "SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file."; + } + + if (!window.JSON) { + // no JSON! + throw "SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8."; + } + + var signalR, + _connection, + events = { + onStart: "onStart", + onStarting: "onStarting", + onSending: "onSending", + onReceived: "onReceived", + onError: "onError", + onReconnect: "onReconnect", + onDisconnect: "onDisconnect" + }, + log = function (msg, logging) { + if (logging === false) { + return; + } + var m; + if (typeof (window.console) === "undefined") { + return; + } + m = "[" + new Date().toTimeString() + "] SignalR: " + msg; + if (window.console.debug) { + window.console.debug(m); + } else if (window.console.log) { + window.console.log(m); + } + }; + + signalR = function (url, qs, logging) { + /// Creates a new SignalR connection for the given url + /// The URL of the long polling endpoint + /// + /// [Optional] Custom querystring parameters to add to the connection URL. + /// If an object, every non-function member will be added to the querystring. + /// If a string, it's added to the QS as specified. + /// + /// + /// [Optional] A flag indicating whether connection logging is enabled to the browser + /// console/log. Defaults to false. + /// + /// + + return new signalR.fn.init(url, qs, logging); + }; + + signalR.fn = signalR.prototype = { + init: function (url, qs, logging) { + this.url = url; + this.qs = qs; + if (typeof (logging) === "boolean") { + this.logging = logging; + } + }, + + logging: false, + + reconnectDelay: 2000, + + start: function (options, callback) { + /// Starts the connection + /// Options map + /// A callback function to execute when the connection has started + var connection = this, + config = { + transport: "auto" + }, + initialize, + promise = $.Deferred(); + + if (connection.transport) { + // Already started, just return + promise.resolve(connection); + return promise; + } + + if ($.type(options) === "function") { + // Support calling with single callback parameter + callback = options; + } else if ($.type(options) === "object") { + $.extend(config, options); + if ($.type(config.callback) === "function") { + callback = config.callback; + } + } + + $(connection).bind(events.onStart, function (e, data) { + if ($.type(callback) === "function") { + callback.call(connection); + } + promise.resolve(connection); + }); + + initialize = function (transports, index) { + index = index || 0; + if (index >= transports.length) { + if (!connection.transport) { + // No transport initialized successfully + promise.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."); + } + return; + } + + var transportName = transports[index], + transport = $.type(transportName) === "object" ? transportName : signalR.transports[transportName]; + + transport.start(connection, function () { + connection.transport = transport; + $(connection).trigger(events.onStart); + }, function () { + initialize(transports, index + 1); + }); + }; + + window.setTimeout(function () { + $.ajax(connection.url + "/negotiate", { + global: false, + type: "POST", + data: {}, + error: function (error) { + $(connection).trigger(events.onError, [error]); + promise.reject("SignalR: Error during negotiation request: " + error); + }, + success: function (res) { + connection.appRelativeUrl = res.Url; + connection.id = res.ConnectionId; + connection.webSocketServerUrl = res.WebSocketServerUrl; + + if (!res.ProtocolVersion || res.ProtocolVersion !== "1.0") { + $(connection).trigger(events.onError, "SignalR: Incompatible protocol version."); + promise.reject("SignalR: Incompatible protocol version."); + return; + } + + $(connection).trigger(events.onStarting); + + var transports = [], + supportedTransports = []; + + $.each(signalR.transports, function (key) { + if (key === "webSockets" && !res.TryWebSockets) { + // Server said don't even try WebSockets, but keep processing the loop + return true; + } + supportedTransports.push(key); + }); + + if ($.isArray(config.transport)) { + // ordered list provided + $.each(config.transport, function () { + var transport = this; + if ($.type(transport) === "object" || ($.type(transport) === "string" && $.inArray("" + transport, supportedTransports) >= 0)) { + transports.push($.type(transport) === "string" ? "" + transport : transport); + } + }); + } else if ($.type(config.transport) === "object" || + $.inArray(config.transport, supportedTransports) >= 0) { + // specific transport provided, as object or a named transport, e.g. "longPolling" + transports.push(config.transport); + } else { // default "auto" + transports = supportedTransports; + } + initialize(transports); + } + }); + }, 0); + + return promise; + }, + + starting: function (callback) { + /// Adds a callback that will be invoked before the connection is started + /// A callback function to execute when the connection is starting + /// + var connection = this, + $connection = $(connection); + + $connection.bind(events.onStarting, function (e, data) { + callback.call(connection); + // Unbind immediately, we don't want to call this callback again + $connection.unbind(events.onStarting); + }); + + return connection; + }, + + send: function (data) { + /// Sends data over the connection + /// The data to send over the connection + /// + var connection = this; + + if (!connection.transport) { + // Connection hasn't been started yet + throw "SignalR: Connection must be started before data can be sent. Call .start() before .send()"; + } + + connection.transport.send(connection, data); + + return connection; + }, + + sending: function (callback) { + /// Adds a callback that will be invoked before anything is sent over the connection + /// A callback function to execute before each time data is sent on the connection + /// + var connection = this; + $(connection).bind(events.onSending, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + received: function (callback) { + /// Adds a callback that will be invoked after anything is received over the connection + /// A callback function to execute when any data is received on the connection + /// + var connection = this; + $(connection).bind(events.onReceived, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + error: function (callback) { + /// Adds a callback that will be invoked after an error occurs with the connection + /// A callback function to execute when an error occurs on the connection + /// + var connection = this; + $(connection).bind(events.onError, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + disconnected: function (callback) { + /// Adds a callback that will be invoked when the client disconnects + /// A callback function to execute when the connection is broken + /// + var connection = this; + $(connection).bind(events.onDisconnect, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + reconnected: function (callback) { + /// Adds a callback that will be invoked when the underlying transport reconnects + /// A callback function to execute when the connection is restored + /// + var connection = this; + $(connection).bind(events.onReconnect, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + stop: function () { + /// Stops listening + /// + var connection = this; + + if (connection.transport) { + connection.transport.stop(connection); + connection.transport = null; + } + + delete connection.messageId; + delete connection.groups; + + // Trigger the disconnect event + $connection.trigger(events.onDisconnect); + + return connection; + }, + + log: log + }; + + signalR.fn.init.prototype = signalR.fn; + + + // Transports + var transportLogic = { + + addQs: function (url, connection) { + if (!connection.qs) { + return url; + } + + if (typeof (connection.qs) === "object") { + return url + "&" + $.param(connection.qs); + } + + if (typeof (connection.qs) === "string") { + return url + "&" + connection.qs; + } + + return url + "&" + escape(connection.qs.toString()); + }, + + getUrl: function (connection, transport, reconnecting) { + /// Gets the url for making a GET based connect request + var url = connection.url, + qs = "transport=" + transport + "&connectionId=" + window.escape(connection.id); + + if (connection.data) { + qs += "&connectionData=" + window.escape(connection.data); + } + + if (!reconnecting) { + url = url + "/connect"; + } else { + if (connection.messageId) { + qs += "&messageId=" + connection.messageId; + } + if (connection.groups) { + qs += "&groups=" + window.escape(JSON.stringify(connection.groups)); + } + } + url += "?" + qs; + url = this.addQs(url, connection); + return url; + }, + + ajaxSend: function (connection, data) { + var url = connection.url + "/send" + "?transport=" + connection.transport.name + "&connectionId=" + window.escape(connection.id); + url = this.addQs(url, connection); + $.ajax(url, { + global: false, + type: "POST", + dataType: "json", + data: { + data: data + }, + success: function (result) { + if (result) { + $(connection).trigger(events.onReceived, [result]); + } + }, + error: function (errData, textStatus) { + if (textStatus === "abort") { + return; + } + $(connection).trigger(events.onError, [errData]); + } + }); + }, + + processMessages: function (connection, data) { + var $connection = $(connection); + + if (data) { + if (data.Disconnect) { + log("Disconnect command received from server", connection.logging); + + // Disconnected by the server + connection.stop(); + + // Trigger the disconnect event + $connection.trigger(events.onDisconnect); + return; + } + + if (data.Messages) { + $.each(data.Messages, function () { + try { + $connection.trigger(events.onReceived, [this]); + } + catch (e) { + log("Error raising received " + e, connection.logging); + $(connection).trigger(events.onError, [e]); + } + }); + } + connection.messageId = data.MessageId; + connection.groups = data.TransportData.Groups; + } + }, + + foreverFrame: { + count: 0, + connections: {} + } + }; + + signalR.transports = { + + webSockets: { + name: "webSockets", + + send: function (connection, data) { + connection.socket.send(data); + }, + + start: function (connection, onSuccess, onFailed) { + var url, + opened = false, + protocol; + + if (window.MozWebSocket) { + window.WebSocket = window.MozWebSocket; + } + + if (!window.WebSocket) { + onFailed(); + return; + } + + if (!connection.socket) { + if (connection.webSocketServerUrl) { + url = connection.webSocketServerUrl; + } + else { + // Determine the protocol + protocol = document.location.protocol === "https:" ? "wss://" : "ws://"; + + url = protocol + document.location.host + connection.appRelativeUrl; + } + + // Build the url + $(connection).trigger(events.onSending); + if (connection.data) { + url += "?connectionData=" + connection.data + "&transport=webSockets&connectionId=" + connection.id; + } else { + url += "?transport=webSockets&connectionId=" + connection.id; + } + + connection.socket = new window.WebSocket(url); + connection.socket.onopen = function () { + opened = true; + if (onSuccess) { + onSuccess(); + } + }; + + connection.socket.onclose = function (event) { + if (!opened) { + if (onFailed) { + onFailed(); + } + } else if (typeof event.wasClean != "undefined" && event.wasClean === false) { + // Ideally this would use the websocket.onerror handler (rather than checking wasClean in onclose) but + // I found in some circumstances Chrome won't call onerror. This implementation seems to work on all browsers. + $(connection).trigger(events.onError); + // TODO: Support reconnect attempt here, need to ensure last message id, groups, and connection data go up on reconnect + } + connection.socket = null; + }; + + connection.socket.onmessage = function (event) { + var data = window.JSON.parse(event.data), + $connection; + if (data) { + $connection = $(connection); + + if (data.Messages) { + $.each(data.Messages, function () { + try { + $connection.trigger(events.onReceived, [this]); + } + catch (e) { + log("Error raising received " + e, connection.logging); + } + }); + } else { + $connection.trigger(events.onReceived, [data]); + } + } + }; + } + }, + + stop: function (connection) { + if (connection.socket !== null) { + connection.socket.close(); + connection.socket = null; + } + } + }, + + serverSentEvents: { + name: "serverSentEvents", + + timeOut: 3000, + + start: function (connection, onSuccess, onFailed) { + var that = this, + opened = false, + $connection = $(connection), + reconnecting = !onSuccess, + url, + connectTimeOut; + + if (connection.eventSource) { + connection.stop(); + } + + if (!window.EventSource) { + if (onFailed) { + onFailed(); + } + return; + } + + $connection.trigger(events.onSending); + + url = transportLogic.getUrl(connection, this.name, reconnecting); + + try { + connection.eventSource = new window.EventSource(url); + } + catch (e) { + log("EventSource failed trying to connect with error " + e.Message, connection.logging); + if (onFailed) { + // The connection failed, call the failed callback + onFailed(); + } + else { + $connection.trigger(events.onError, [e]); + if (reconnecting) { + // If we were reconnecting, rather than doing initial connect, then try reconnect again + log("EventSource reconnecting", connection.logging); + that.reconnect(connection); + } + } + return; + } + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + connectTimeOut = window.setTimeout(function () { + if (opened === false) { + log("EventSource timed out trying to connect", connection.logging); + + if (onFailed) { + onFailed(); + } + + if (reconnecting) { + // If we were reconnecting, rather than doing initial connect, then try reconnect again + log("EventSource reconnecting", connection.logging); + that.reconnect(connection); + } else { + that.stop(connection); + } + } + }, + that.timeOut); + + connection.eventSource.addEventListener("open", function (e) { + log("EventSource connected", connection.logging); + + if (connectTimeOut) { + window.clearTimeout(connectTimeOut); + } + + if (opened === false) { + opened = true; + + if (onSuccess) { + onSuccess(); + } + + if (reconnecting) { + $connection.trigger(events.onReconnect); + } + } + }, false); + + connection.eventSource.addEventListener("message", function (e) { + // process messages + if (e.data === "initialized") { + return; + } + transportLogic.processMessages(connection, window.JSON.parse(e.data)); + }, false); + + connection.eventSource.addEventListener("error", function (e) { + if (!opened) { + if (onFailed) { + onFailed(); + } + return; + } + + log("EventSource readyState: " + connection.eventSource.readyState, connection.logging); + + if (e.eventPhase === window.EventSource.CLOSED) { + // connection closed + if (connection.eventSource.readyState === window.EventSource.CONNECTING) { + // We don't use the EventSource's native reconnect function as it + // doesn't allow us to change the URL when reconnecting. We need + // to change the URL to not include the /connect suffix, and pass + // the last message id we received. + log("EventSource reconnecting due to the server connection ending", connection.logging); + that.reconnect(connection); + } + else { + // The EventSource has closed, either because its close() method was called, + // or the server sent down a "don't reconnect" frame. + log("EventSource closed", connection.logging); + that.stop(connection); + } + } else { + // connection error + log("EventSource error", connection.logging); + $connection.trigger(events.onError); + } + }, false); + }, + + reconnect: function (connection) { + var that = this; + window.setTimeout(function () { + that.stop(connection); + that.start(connection); + }, connection.reconnectDelay); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function (connection) { + if (connection && connection.eventSource) { + connection.eventSource.close(); + connection.eventSource = null; + delete connection.eventSource; + } + } + }, + + foreverFrame: { + name: "foreverFrame", + + timeOut: 3000, + + start: function (connection, onSuccess, onFailed) { + var that = this, + frameId = (transportLogic.foreverFrame.count += 1), + url, + connectTimeOut, + frame = $(""); + + if (window.EventSource) { + // If the browser supports SSE, don't use Forever Frame + if (onFailed) { + onFailed(); + } + return; + } + + $(connection).trigger(events.onSending); + + // Build the url + url = transportLogic.getUrl(connection, this.name); + url += "&frameId=" + frameId; + + frame.prop("src", url); + transportLogic.foreverFrame.connections[frameId] = connection; + + frame.bind("readystatechange", function () { + if ($.inArray(this.readyState, ["loaded", "complete"]) >= 0) { + log("Forever frame iframe readyState changed to " + this.readyState + ", reconnecting", connection.logging); + that.reconnect(connection); + } + }); + + connection.frame = frame[0]; + connection.frameId = frameId; + + if (onSuccess) { + connection.onSuccess = onSuccess; + } + + $("body").append(frame); + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + connectTimeOut = window.setTimeout(function () { + if (connection.onSuccess) { + that.stop(connection); + + if (onFailed) { + onFailed(); + } + } + }, that.timeOut); + }, + + reconnect: function (connection) { + var that = this; + window.setTimeout(function () { + var frame = connection.frame, + src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId; + frame.src = src; + }, connection.reconnectDelay); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + receive: transportLogic.processMessages, + + stop: function (connection) { + if (connection.frame) { + if (connection.frame.stop) { + connection.frame.stop(); + } else if (connection.frame.document && connection.frame.document.execCommand) { + connection.frame.document.execCommand("Stop"); + } + $(connection.frame).remove(); + delete transportLogic.foreverFrame.connections[connection.frameId]; + connection.frame = null; + connection.frameId = null; + delete connection.frame; + delete connection.frameId; + } + }, + + getConnection: function (id) { + return transportLogic.foreverFrame.connections[id]; + }, + + started: function (connection) { + if (connection.onSuccess) { + connection.onSuccess(); + connection.onSuccess = null; + delete connection.onSuccess; + } + else { + // If there's no onSuccess handler we assume this is a reconnect + $(connection).trigger(events.onReconnect); + } + } + }, + + longPolling: { + name: "longPolling", + + reconnectDelay: 3000, + + start: function (connection, onSuccess, onFailed) { + /// Starts the long polling connection + /// The SignalR connection to start + var that = this; + if (connection.pollXhr) { + connection.stop(); + } + + connection.messageId = null; + + window.setTimeout(function () { + (function poll(instance, raiseReconnect) { + $(instance).trigger(events.onSending); + + var messageId = instance.messageId, + connect = (messageId === null), + url = transportLogic.getUrl(instance, that.name, !connect), + reconnectTimeOut = null, + reconnectFired = false; + + instance.pollXhr = $.ajax(url, { + global: false, + + type: "GET", + + dataType: "json", + + success: function (data) { + var delay = 0, + timedOutReceived = false; + + if (raiseReconnect === true) { + // Fire the reconnect event if it hasn't been fired as yet + if (reconnectFired === false) { + $(instance).trigger(events.onReconnect); + reconnectFired = true; + } + } + + transportLogic.processMessages(instance, data); + if (data && $.type(data.TransportData.LongPollDelay) === "number") { + delay = data.TransportData.LongPollDelay; + } + + if (data && data.TimedOut) { + timedOutReceived = data.TimedOut; + } + + if (delay > 0) { + window.setTimeout(function () { + poll(instance, timedOutReceived); + }, delay); + } else { + poll(instance, timedOutReceived); + } + }, + + error: function (data, textStatus) { + if (textStatus === "abort") { + return; + } + + if (reconnectTimeOut) { + // If the request failed then we clear the timeout so that the + // reconnect event doesn't get fired + clearTimeout(reconnectTimeOut); + } + + $(instance).trigger(events.onError, [data]); + + window.setTimeout(function () { + poll(instance, true); + }, connection.reconnectDelay); + } + }); + + if (raiseReconnect === true) { + reconnectTimeOut = window.setTimeout(function () { + if (reconnectFired === false) { + $(instance).trigger(events.onReconnect); + reconnectFired = true; + } + }, + that.reconnectDelay); + } + + } (connection)); + + // Now connected + // There's no good way know when the long poll has actually started so + // we assume it only takes around 150ms (max) to start the connection + window.setTimeout(onSuccess, 150); + + }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function (connection) { + /// Stops the long polling connection + /// The SignalR connection to stop + if (connection.pollXhr) { + connection.pollXhr.abort(); + connection.pollXhr = null; + delete connection.pollXhr; + } + } + } + }; + + signalR.noConflict = function () { + /// Reinstates the original value of $.connection and returns the signalR object for manual assignment + /// + if ($.connection === signalR) { + $.connection = _connection; + } + return signalR; + }; + + if ($.connection) { + _connection = $.connection; + } + + $.connection = $.signalR = signalR; + +} (window.jQuery, window)); \ No newline at end of file diff --git a/NzbDrone.Web/Scripts/jquery.signalR.min.js b/NzbDrone.Web/Scripts/jquery.signalR.min.js new file mode 100644 index 000000000..9e99f53a3 --- /dev/null +++ b/NzbDrone.Web/Scripts/jquery.signalR.min.js @@ -0,0 +1 @@ +(function(n,t){"use strict";var f,e,i,r,u;if(typeof n!="function")throw"SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file.";if(!t.JSON)throw"SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8.";i={onStart:"onStart",onStarting:"onStarting",onSending:"onSending",onReceived:"onReceived",onError:"onError",onReconnect:"onReconnect",onDisconnect:"onDisconnect"},r=function(n,i){if(i===!1)return;var r;if(typeof t.console=="undefined")return;r="["+(new Date).toTimeString()+"] SignalR: "+n,t.console.debug?t.console.debug(r):t.console.log&&t.console.log(r)},f=function(n,t,i){return new f.fn.init(n,t,i)},f.fn=f.prototype={init:function(n,t,i){this.url=n,this.qs=t,typeof i=="boolean"&&(this.logging=i)},logging:!1,reconnectDelay:2e3,start:function(r,u){var e=this,o={transport:"auto"},h,s=n.Deferred();return e.transport?(s.resolve(e),s):(n.type(r)==="function"?u=r:n.type(r)==="object"&&(n.extend(o,r),n.type(o.callback)==="function"&&(u=o.callback)),n(e).bind(i.onStart,function(){n.type(u)==="function"&&u.call(e),s.resolve(e)}),h=function(t,r){r=r||0;if(r>=t.length){e.transport||s.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization.");return}var u=t[r],o=n.type(u)==="object"?u:f.transports[u];o.start(e,function(){e.transport=o,n(e).trigger(i.onStart)},function(){h(t,r+1)})},t.setTimeout(function(){n.ajax(e.url+"/negotiate",{global:!1,type:"POST",data:{},error:function(t){n(e).trigger(i.onError,[t]),s.reject("SignalR: Error during negotiation request: "+t)},success:function(t){e.appRelativeUrl=t.Url,e.id=t.ConnectionId,e.webSocketServerUrl=t.WebSocketServerUrl;if(!t.ProtocolVersion||t.ProtocolVersion!=="1.0"){n(e).trigger(i.onError,"SignalR: Incompatible protocol version."),s.reject("SignalR: Incompatible protocol version.");return}n(e).trigger(i.onStarting);var u=[],r=[];n.each(f.transports,function(n){if(n==="webSockets"&&!t.TryWebSockets)return!0;r.push(n)}),n.isArray(o.transport)?n.each(o.transport,function(){var t=this;n.type(t)!=="object"&&(n.type(t)!=="string"||n.inArray(""+t,r)<0)||u.push(n.type(t)==="string"?""+t:t)}):n.type(o.transport)!=="object"&&n.inArray(o.transport,r)<0?u=r:u.push(o.transport),h(u)}})},0),s)},starting:function(t){var r=this,u=n(r);return u.bind(i.onStarting,function(){t.call(r),u.unbind(i.onStarting)}),r},send:function(n){var t=this;if(!t.transport)throw"SignalR: Connection must be started before data can be sent. Call .start() before .send()";return t.transport.send(t,n),t},sending:function(t){var r=this;return n(r).bind(i.onSending,function(){t.call(r)}),r},received:function(t){var r=this;return n(r).bind(i.onReceived,function(n,i){t.call(r,i)}),r},error:function(t){var r=this;return n(r).bind(i.onError,function(n,i){t.call(r,i)}),r},disconnected:function(t){var r=this;return n(r).bind(i.onDisconnect,function(){t.call(r)}),r},reconnected:function(t){var r=this;return n(r).bind(i.onReconnect,function(){t.call(r)}),r},stop:function(){var n=this;return n.transport&&(n.transport.stop(n),n.transport=null),delete n.messageId,delete n.groups,$connection.trigger(i.onDisconnect),n},log:r},f.fn.init.prototype=f.fn,u={addQs:function(t,i){return i.qs?typeof i.qs=="object"?t+"&"+n.param(i.qs):typeof i.qs=="string"?t+"&"+i.qs:t+"&"+escape(i.qs.toString()):t},getUrl:function(n,i,r){var u=n.url,f="transport="+i+"&connectionId="+t.escape(n.id);return n.data&&(f+="&connectionData="+t.escape(n.data)),r?(n.messageId&&(f+="&messageId="+n.messageId),n.groups&&(f+="&groups="+t.escape(JSON.stringify(n.groups)))):u=u+"/connect",u+="?"+f,u=this.addQs(u,n)},ajaxSend:function(r,u){var f=r.url+"/send?transport="+r.transport.name+"&connectionId="+t.escape(r.id);f=this.addQs(f,r),n.ajax(f,{global:!1,type:"POST",dataType:"json",data:{data:u},success:function(t){t&&n(r).trigger(i.onReceived,[t])},error:function(t,u){if(u==="abort")return;n(r).trigger(i.onError,[t])}})},processMessages:function(t,u){var f=n(t);if(u){if(u.Disconnect){r("Disconnect command received from server",t.logging),t.stop(),f.trigger(i.onDisconnect);return}u.Messages&&n.each(u.Messages,function(){try{f.trigger(i.onReceived,[this])}catch(u){r("Error raising received "+u,t.logging),n(t).trigger(i.onError,[u])}}),t.messageId=u.MessageId,t.groups=u.TransportData.Groups}},foreverFrame:{count:0,connections:{}}},f.transports={webSockets:{name:"webSockets",send:function(n,t){n.socket.send(t)},start:function(u,f,e){var o,h=!1,s;t.MozWebSocket&&(t.WebSocket=t.MozWebSocket);if(!t.WebSocket){e();return}u.socket||(u.webSocketServerUrl?o=u.webSocketServerUrl:(s=document.location.protocol==="https:"?"wss://":"ws://",o=s+document.location.host+u.appRelativeUrl),n(u).trigger(i.onSending),o+=u.data?"?connectionData="+u.data+"&transport=webSockets&connectionId="+u.id:"?transport=webSockets&connectionId="+u.id,u.socket=new t.WebSocket(o),u.socket.onopen=function(){h=!0,f&&f()},u.socket.onclose=function(t){h?typeof t.wasClean!="undefined"&&t.wasClean===!1&&n(u).trigger(i.onError):e&&e(),u.socket=null},u.socket.onmessage=function(f){var e=t.JSON.parse(f.data),o;e&&(o=n(u),e.Messages?n.each(e.Messages,function(){try{o.trigger(i.onReceived,[this])}catch(n){r("Error raising received "+n,u.logging)}}):o.trigger(i.onReceived,[e]))})},stop:function(n){n.socket!==null&&(n.socket.close(),n.socket=null)}},serverSentEvents:{name:"serverSentEvents",timeOut:3e3,start:function(f,e,o){var s=this,l=!1,c=n(f),h=!e,v,a;f.eventSource&&f.stop();if(!t.EventSource){o&&o();return}c.trigger(i.onSending),v=u.getUrl(f,this.name,h);try{f.eventSource=new t.EventSource(v)}catch(y){r("EventSource failed trying to connect with error "+y.Message,f.logging),o?o():(c.trigger(i.onError,[y]),h&&(r("EventSource reconnecting",f.logging),s.reconnect(f)));return}a=t.setTimeout(function(){l===!1&&(r("EventSource timed out trying to connect",f.logging),o&&o(),h?(r("EventSource reconnecting",f.logging),s.reconnect(f)):s.stop(f))},s.timeOut),f.eventSource.addEventListener("open",function(){r("EventSource connected",f.logging),a&&t.clearTimeout(a),l===!1&&(l=!0,e&&e(),h&&c.trigger(i.onReconnect))},!1),f.eventSource.addEventListener("message",function(n){if(n.data==="initialized")return;u.processMessages(f,t.JSON.parse(n.data))},!1),f.eventSource.addEventListener("error",function(n){if(!l){o&&o();return}r("EventSource readyState: "+f.eventSource.readyState,f.logging),n.eventPhase===t.EventSource.CLOSED?f.eventSource.readyState===t.EventSource.CONNECTING?(r("EventSource reconnecting due to the server connection ending",f.logging),s.reconnect(f)):(r("EventSource closed",f.logging),s.stop(f)):(r("EventSource error",f.logging),c.trigger(i.onError))},!1)},reconnect:function(n){var i=this;t.setTimeout(function(){i.stop(n),i.start(n)},n.reconnectDelay)},send:function(n,t){u.ajaxSend(n,t)},stop:function(n){n&&n.eventSource&&(n.eventSource.close(),n.eventSource=null,delete n.eventSource)}},foreverFrame:{name:"foreverFrame",timeOut:3e3,start:function(f,e,o){var h=this,l=u.foreverFrame.count+=1,c,a,s=n("");if(t.EventSource){o&&o();return}n(f).trigger(i.onSending),c=u.getUrl(f,this.name),c+="&frameId="+l,s.prop("src",c),u.foreverFrame.connections[l]=f,s.bind("readystatechange",function(){n.inArray(this.readyState,["loaded","complete"])<0||(r("Forever frame iframe readyState changed to "+this.readyState+", reconnecting",f.logging),h.reconnect(f))}),f.frame=s[0],f.frameId=l,e&&(f.onSuccess=e),n("body").append(s),a=t.setTimeout(function(){f.onSuccess&&(h.stop(f),o&&o())},h.timeOut)},reconnect:function(n){var i=this;t.setTimeout(function(){var r=n.frame,t=u.getUrl(n,i.name,!0)+"&frameId="+n.frameId;r.src=t},n.reconnectDelay)},send:function(n,t){u.ajaxSend(n,t)},receive:u.processMessages,stop:function(t){t.frame&&(t.frame.stop?t.frame.stop():t.frame.document&&t.frame.document.execCommand&&t.frame.document.execCommand("Stop"),n(t.frame).remove(),delete u.foreverFrame.connections[t.frameId],t.frame=null,t.frameId=null,delete t.frame,delete t.frameId)},getConnection:function(n){return u.foreverFrame.connections[n]},started:function(t){t.onSuccess?(t.onSuccess(),t.onSuccess=null,delete t.onSuccess):n(t).trigger(i.onReconnect)}},longPolling:{name:"longPolling",reconnectDelay:3e3,start:function(r,f){var o=this;r.pollXhr&&r.stop(),r.messageId=null,t.setTimeout(function(){(function e(f,s){n(f).trigger(i.onSending);var a=f.messageId,l=a===null,v=u.getUrl(f,o.name,!l),c=null,h=!1;f.pollXhr=n.ajax(v,{global:!1,type:"GET",dataType:"json",success:function(r){var c=0,o=!1;s===!0&&h===!1&&(n(f).trigger(i.onReconnect),h=!0),u.processMessages(f,r),r&&n.type(r.TransportData.LongPollDelay)==="number"&&(c=r.TransportData.LongPollDelay),r&&r.TimedOut&&(o=r.TimedOut),c>0?t.setTimeout(function(){e(f,o)},c):e(f,o)},error:function(u,o){if(o==="abort")return;c&&clearTimeout(c),n(f).trigger(i.onError,[u]),t.setTimeout(function(){e(f,!0)},r.reconnectDelay)}}),s===!0&&(c=t.setTimeout(function(){h===!1&&(n(f).trigger(i.onReconnect),h=!0)},o.reconnectDelay))})(r),t.setTimeout(f,150)},250)},send:function(n,t){u.ajaxSend(n,t)},stop:function(n){n.pollXhr&&(n.pollXhr.abort(),n.pollXhr=null,delete n.pollXhr)}}},f.noConflict=function(){return n.connection===f&&(n.connection=e),f},n.connection&&(e=n.connection),n.connection=n.signalR=f})(window.jQuery,window) \ No newline at end of file diff --git a/NzbDrone.Web/Views/Series/Episode.cshtml b/NzbDrone.Web/Views/Series/Episode.cshtml index 146009c7a..f26d0b906 100644 --- a/NzbDrone.Web/Views/Series/Episode.cshtml +++ b/NzbDrone.Web/Views/Series/Episode.cshtml @@ -2,7 +2,7 @@ @using NzbDrone.Web.Helpers @model NzbDrone.Web.Models.EpisodeModel - + @Model.EpisodeNumber @Model.Title @Model.AirDate @@ -25,7 +25,7 @@ @*Commands Column*@ - @Model.Status + @Model.Status @Ajax.ImageActionLink("../../Content/Images/Search.png", new { Alt = "Search", Title = "Search for episode", @class = "gridImage" }, "Search", "Episode", new { episodeId = Model.EpisodeId }, null, null) @Ajax.ImageActionLink("../../Content/Images/Rename.png", new { Alt = "Rename", Title = "Rename episode", @class = "gridImage" }, "Rename", "Episode", new { episodeFileId = Model.EpisodeFileId }, null, null) diff --git a/NzbDrone.Web/Views/Shared/_ReferenceLayout.cshtml b/NzbDrone.Web/Views/Shared/_ReferenceLayout.cshtml index 1332bda8d..1c58cb17e 100644 --- a/NzbDrone.Web/Views/Shared/_ReferenceLayout.cshtml +++ b/NzbDrone.Web/Views/Shared/_ReferenceLayout.cshtml @@ -28,7 +28,10 @@ @Html.IncludeScript("jquery-tgc-countdown-1.0.js") @Html.IncludeScript("jquery.watermark.min.js") @Html.IncludeScript("jquery.hotkeys.js") + @Html.IncludeScript("jquery.signalR.min.js") + @Html.IncludeScript("jquery.validate.min.js") @Html.IncludeScript("doTimeout.js") + @Html.IncludeScript("NzbDrone/localSearch.js") @Html.IncludeScript("NzbDrone/AutoComplete.js") @Html.IncludeScript("NzbDrone/Notification.js") @@ -38,7 +41,6 @@ @Html.IncludeScript("DataTables-1.9.0/media/js/jquery.dataTables.reloadAjax.js") @Html.IncludeScript("DataTables-1.9.0/media/js/jquery.dataTables.editable.js") @Html.IncludeScript("DataTables-1.9.0/media/js/jquery.jeditable.js") - @Html.IncludeScript("DataTables-1.9.0/media/js/jquery.validate.js") @Html.IncludeScript("jquery.dataTables.4button.pagination.js") @RenderSection("Scripts", required: false) diff --git a/NzbDrone.Web/packages.config b/NzbDrone.Web/packages.config index 9e0050eae..1c21698a3 100644 --- a/NzbDrone.Web/packages.config +++ b/NzbDrone.Web/packages.config @@ -14,10 +14,14 @@ + + + + \ No newline at end of file diff --git a/packages/SignalR.Hosting.AspNet.0.4.0.0/SignalR.Hosting.AspNet.0.4.0.0.nupkg b/packages/SignalR.Hosting.AspNet.0.4.0.0/SignalR.Hosting.AspNet.0.4.0.0.nupkg new file mode 100644 index 000000000..4009bc929 Binary files /dev/null and b/packages/SignalR.Hosting.AspNet.0.4.0.0/SignalR.Hosting.AspNet.0.4.0.0.nupkg differ diff --git a/packages/SignalR.Hosting.AspNet.0.4.0.0/lib/net40/SignalR.Hosting.AspNet.dll b/packages/SignalR.Hosting.AspNet.0.4.0.0/lib/net40/SignalR.Hosting.AspNet.dll new file mode 100644 index 000000000..b3da4f88c Binary files /dev/null and b/packages/SignalR.Hosting.AspNet.0.4.0.0/lib/net40/SignalR.Hosting.AspNet.dll differ diff --git a/packages/SignalR.Js.0.4.0/SignalR.Js.0.4.0.nupkg b/packages/SignalR.Js.0.4.0/SignalR.Js.0.4.0.nupkg new file mode 100644 index 000000000..853d396a7 Binary files /dev/null and b/packages/SignalR.Js.0.4.0/SignalR.Js.0.4.0.nupkg differ diff --git a/packages/SignalR.Js.0.4.0/content/Scripts/jquery.signalR.js b/packages/SignalR.Js.0.4.0/content/Scripts/jquery.signalR.js new file mode 100644 index 000000000..c877e0577 --- /dev/null +++ b/packages/SignalR.Js.0.4.0/content/Scripts/jquery.signalR.js @@ -0,0 +1,885 @@ +/// +(function ($, window) { + /// + "use strict"; + + if (typeof ($) !== "function") { + // no jQuery! + throw "SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file."; + } + + if (!window.JSON) { + // no JSON! + throw "SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8."; + } + + var signalR, + _connection, + events = { + onStart: "onStart", + onStarting: "onStarting", + onSending: "onSending", + onReceived: "onReceived", + onError: "onError", + onReconnect: "onReconnect", + onDisconnect: "onDisconnect" + }, + log = function (msg, logging) { + if (logging === false) { + return; + } + var m; + if (typeof (window.console) === "undefined") { + return; + } + m = "[" + new Date().toTimeString() + "] SignalR: " + msg; + if (window.console.debug) { + window.console.debug(m); + } else if (window.console.log) { + window.console.log(m); + } + }; + + signalR = function (url, qs, logging) { + /// Creates a new SignalR connection for the given url + /// The URL of the long polling endpoint + /// + /// [Optional] Custom querystring parameters to add to the connection URL. + /// If an object, every non-function member will be added to the querystring. + /// If a string, it's added to the QS as specified. + /// + /// + /// [Optional] A flag indicating whether connection logging is enabled to the browser + /// console/log. Defaults to false. + /// + /// + + return new signalR.fn.init(url, qs, logging); + }; + + signalR.fn = signalR.prototype = { + init: function (url, qs, logging) { + this.url = url; + this.qs = qs; + if (typeof (logging) === "boolean") { + this.logging = logging; + } + }, + + logging: false, + + reconnectDelay: 2000, + + start: function (options, callback) { + /// Starts the connection + /// Options map + /// A callback function to execute when the connection has started + var connection = this, + config = { + transport: "auto" + }, + initialize, + promise = $.Deferred(); + + if (connection.transport) { + // Already started, just return + promise.resolve(connection); + return promise; + } + + if ($.type(options) === "function") { + // Support calling with single callback parameter + callback = options; + } else if ($.type(options) === "object") { + $.extend(config, options); + if ($.type(config.callback) === "function") { + callback = config.callback; + } + } + + $(connection).bind(events.onStart, function (e, data) { + if ($.type(callback) === "function") { + callback.call(connection); + } + promise.resolve(connection); + }); + + initialize = function (transports, index) { + index = index || 0; + if (index >= transports.length) { + if (!connection.transport) { + // No transport initialized successfully + promise.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."); + } + return; + } + + var transportName = transports[index], + transport = $.type(transportName) === "object" ? transportName : signalR.transports[transportName]; + + transport.start(connection, function () { + connection.transport = transport; + $(connection).trigger(events.onStart); + }, function () { + initialize(transports, index + 1); + }); + }; + + window.setTimeout(function () { + $.ajax(connection.url + "/negotiate", { + global: false, + type: "POST", + data: {}, + error: function (error) { + $(connection).trigger(events.onError, [error]); + promise.reject("SignalR: Error during negotiation request: " + error); + }, + success: function (res) { + connection.appRelativeUrl = res.Url; + connection.id = res.ConnectionId; + connection.webSocketServerUrl = res.WebSocketServerUrl; + + if (!res.ProtocolVersion || res.ProtocolVersion !== "1.0") { + $(connection).trigger(events.onError, "SignalR: Incompatible protocol version."); + promise.reject("SignalR: Incompatible protocol version."); + return; + } + + $(connection).trigger(events.onStarting); + + var transports = [], + supportedTransports = []; + + $.each(signalR.transports, function (key) { + if (key === "webSockets" && !res.TryWebSockets) { + // Server said don't even try WebSockets, but keep processing the loop + return true; + } + supportedTransports.push(key); + }); + + if ($.isArray(config.transport)) { + // ordered list provided + $.each(config.transport, function () { + var transport = this; + if ($.type(transport) === "object" || ($.type(transport) === "string" && $.inArray("" + transport, supportedTransports) >= 0)) { + transports.push($.type(transport) === "string" ? "" + transport : transport); + } + }); + } else if ($.type(config.transport) === "object" || + $.inArray(config.transport, supportedTransports) >= 0) { + // specific transport provided, as object or a named transport, e.g. "longPolling" + transports.push(config.transport); + } else { // default "auto" + transports = supportedTransports; + } + initialize(transports); + } + }); + }, 0); + + return promise; + }, + + starting: function (callback) { + /// Adds a callback that will be invoked before the connection is started + /// A callback function to execute when the connection is starting + /// + var connection = this, + $connection = $(connection); + + $connection.bind(events.onStarting, function (e, data) { + callback.call(connection); + // Unbind immediately, we don't want to call this callback again + $connection.unbind(events.onStarting); + }); + + return connection; + }, + + send: function (data) { + /// Sends data over the connection + /// The data to send over the connection + /// + var connection = this; + + if (!connection.transport) { + // Connection hasn't been started yet + throw "SignalR: Connection must be started before data can be sent. Call .start() before .send()"; + } + + connection.transport.send(connection, data); + + return connection; + }, + + sending: function (callback) { + /// Adds a callback that will be invoked before anything is sent over the connection + /// A callback function to execute before each time data is sent on the connection + /// + var connection = this; + $(connection).bind(events.onSending, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + received: function (callback) { + /// Adds a callback that will be invoked after anything is received over the connection + /// A callback function to execute when any data is received on the connection + /// + var connection = this; + $(connection).bind(events.onReceived, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + error: function (callback) { + /// Adds a callback that will be invoked after an error occurs with the connection + /// A callback function to execute when an error occurs on the connection + /// + var connection = this; + $(connection).bind(events.onError, function (e, data) { + callback.call(connection, data); + }); + return connection; + }, + + disconnected: function (callback) { + /// Adds a callback that will be invoked when the client disconnects + /// A callback function to execute when the connection is broken + /// + var connection = this; + $(connection).bind(events.onDisconnect, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + reconnected: function (callback) { + /// Adds a callback that will be invoked when the underlying transport reconnects + /// A callback function to execute when the connection is restored + /// + var connection = this; + $(connection).bind(events.onReconnect, function (e, data) { + callback.call(connection); + }); + return connection; + }, + + stop: function () { + /// Stops listening + /// + var connection = this; + + if (connection.transport) { + connection.transport.stop(connection); + connection.transport = null; + } + + delete connection.messageId; + delete connection.groups; + + // Trigger the disconnect event + $connection.trigger(events.onDisconnect); + + return connection; + }, + + log: log + }; + + signalR.fn.init.prototype = signalR.fn; + + + // Transports + var transportLogic = { + + addQs: function (url, connection) { + if (!connection.qs) { + return url; + } + + if (typeof (connection.qs) === "object") { + return url + "&" + $.param(connection.qs); + } + + if (typeof (connection.qs) === "string") { + return url + "&" + connection.qs; + } + + return url + "&" + escape(connection.qs.toString()); + }, + + getUrl: function (connection, transport, reconnecting) { + /// Gets the url for making a GET based connect request + var url = connection.url, + qs = "transport=" + transport + "&connectionId=" + window.escape(connection.id); + + if (connection.data) { + qs += "&connectionData=" + window.escape(connection.data); + } + + if (!reconnecting) { + url = url + "/connect"; + } else { + if (connection.messageId) { + qs += "&messageId=" + connection.messageId; + } + if (connection.groups) { + qs += "&groups=" + window.escape(JSON.stringify(connection.groups)); + } + } + url += "?" + qs; + url = this.addQs(url, connection); + return url; + }, + + ajaxSend: function (connection, data) { + var url = connection.url + "/send" + "?transport=" + connection.transport.name + "&connectionId=" + window.escape(connection.id); + url = this.addQs(url, connection); + $.ajax(url, { + global: false, + type: "POST", + dataType: "json", + data: { + data: data + }, + success: function (result) { + if (result) { + $(connection).trigger(events.onReceived, [result]); + } + }, + error: function (errData, textStatus) { + if (textStatus === "abort") { + return; + } + $(connection).trigger(events.onError, [errData]); + } + }); + }, + + processMessages: function (connection, data) { + var $connection = $(connection); + + if (data) { + if (data.Disconnect) { + log("Disconnect command received from server", connection.logging); + + // Disconnected by the server + connection.stop(); + + // Trigger the disconnect event + $connection.trigger(events.onDisconnect); + return; + } + + if (data.Messages) { + $.each(data.Messages, function () { + try { + $connection.trigger(events.onReceived, [this]); + } + catch (e) { + log("Error raising received " + e, connection.logging); + $(connection).trigger(events.onError, [e]); + } + }); + } + connection.messageId = data.MessageId; + connection.groups = data.TransportData.Groups; + } + }, + + foreverFrame: { + count: 0, + connections: {} + } + }; + + signalR.transports = { + + webSockets: { + name: "webSockets", + + send: function (connection, data) { + connection.socket.send(data); + }, + + start: function (connection, onSuccess, onFailed) { + var url, + opened = false, + protocol; + + if (window.MozWebSocket) { + window.WebSocket = window.MozWebSocket; + } + + if (!window.WebSocket) { + onFailed(); + return; + } + + if (!connection.socket) { + if (connection.webSocketServerUrl) { + url = connection.webSocketServerUrl; + } + else { + // Determine the protocol + protocol = document.location.protocol === "https:" ? "wss://" : "ws://"; + + url = protocol + document.location.host + connection.appRelativeUrl; + } + + // Build the url + $(connection).trigger(events.onSending); + if (connection.data) { + url += "?connectionData=" + connection.data + "&transport=webSockets&connectionId=" + connection.id; + } else { + url += "?transport=webSockets&connectionId=" + connection.id; + } + + connection.socket = new window.WebSocket(url); + connection.socket.onopen = function () { + opened = true; + if (onSuccess) { + onSuccess(); + } + }; + + connection.socket.onclose = function (event) { + if (!opened) { + if (onFailed) { + onFailed(); + } + } else if (typeof event.wasClean != "undefined" && event.wasClean === false) { + // Ideally this would use the websocket.onerror handler (rather than checking wasClean in onclose) but + // I found in some circumstances Chrome won't call onerror. This implementation seems to work on all browsers. + $(connection).trigger(events.onError); + // TODO: Support reconnect attempt here, need to ensure last message id, groups, and connection data go up on reconnect + } + connection.socket = null; + }; + + connection.socket.onmessage = function (event) { + var data = window.JSON.parse(event.data), + $connection; + if (data) { + $connection = $(connection); + + if (data.Messages) { + $.each(data.Messages, function () { + try { + $connection.trigger(events.onReceived, [this]); + } + catch (e) { + log("Error raising received " + e, connection.logging); + } + }); + } else { + $connection.trigger(events.onReceived, [data]); + } + } + }; + } + }, + + stop: function (connection) { + if (connection.socket !== null) { + connection.socket.close(); + connection.socket = null; + } + } + }, + + serverSentEvents: { + name: "serverSentEvents", + + timeOut: 3000, + + start: function (connection, onSuccess, onFailed) { + var that = this, + opened = false, + $connection = $(connection), + reconnecting = !onSuccess, + url, + connectTimeOut; + + if (connection.eventSource) { + connection.stop(); + } + + if (!window.EventSource) { + if (onFailed) { + onFailed(); + } + return; + } + + $connection.trigger(events.onSending); + + url = transportLogic.getUrl(connection, this.name, reconnecting); + + try { + connection.eventSource = new window.EventSource(url); + } + catch (e) { + log("EventSource failed trying to connect with error " + e.Message, connection.logging); + if (onFailed) { + // The connection failed, call the failed callback + onFailed(); + } + else { + $connection.trigger(events.onError, [e]); + if (reconnecting) { + // If we were reconnecting, rather than doing initial connect, then try reconnect again + log("EventSource reconnecting", connection.logging); + that.reconnect(connection); + } + } + return; + } + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + connectTimeOut = window.setTimeout(function () { + if (opened === false) { + log("EventSource timed out trying to connect", connection.logging); + + if (onFailed) { + onFailed(); + } + + if (reconnecting) { + // If we were reconnecting, rather than doing initial connect, then try reconnect again + log("EventSource reconnecting", connection.logging); + that.reconnect(connection); + } else { + that.stop(connection); + } + } + }, + that.timeOut); + + connection.eventSource.addEventListener("open", function (e) { + log("EventSource connected", connection.logging); + + if (connectTimeOut) { + window.clearTimeout(connectTimeOut); + } + + if (opened === false) { + opened = true; + + if (onSuccess) { + onSuccess(); + } + + if (reconnecting) { + $connection.trigger(events.onReconnect); + } + } + }, false); + + connection.eventSource.addEventListener("message", function (e) { + // process messages + if (e.data === "initialized") { + return; + } + transportLogic.processMessages(connection, window.JSON.parse(e.data)); + }, false); + + connection.eventSource.addEventListener("error", function (e) { + if (!opened) { + if (onFailed) { + onFailed(); + } + return; + } + + log("EventSource readyState: " + connection.eventSource.readyState, connection.logging); + + if (e.eventPhase === window.EventSource.CLOSED) { + // connection closed + if (connection.eventSource.readyState === window.EventSource.CONNECTING) { + // We don't use the EventSource's native reconnect function as it + // doesn't allow us to change the URL when reconnecting. We need + // to change the URL to not include the /connect suffix, and pass + // the last message id we received. + log("EventSource reconnecting due to the server connection ending", connection.logging); + that.reconnect(connection); + } + else { + // The EventSource has closed, either because its close() method was called, + // or the server sent down a "don't reconnect" frame. + log("EventSource closed", connection.logging); + that.stop(connection); + } + } else { + // connection error + log("EventSource error", connection.logging); + $connection.trigger(events.onError); + } + }, false); + }, + + reconnect: function (connection) { + var that = this; + window.setTimeout(function () { + that.stop(connection); + that.start(connection); + }, connection.reconnectDelay); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function (connection) { + if (connection && connection.eventSource) { + connection.eventSource.close(); + connection.eventSource = null; + delete connection.eventSource; + } + } + }, + + foreverFrame: { + name: "foreverFrame", + + timeOut: 3000, + + start: function (connection, onSuccess, onFailed) { + var that = this, + frameId = (transportLogic.foreverFrame.count += 1), + url, + connectTimeOut, + frame = $(""); + + if (window.EventSource) { + // If the browser supports SSE, don't use Forever Frame + if (onFailed) { + onFailed(); + } + return; + } + + $(connection).trigger(events.onSending); + + // Build the url + url = transportLogic.getUrl(connection, this.name); + url += "&frameId=" + frameId; + + frame.prop("src", url); + transportLogic.foreverFrame.connections[frameId] = connection; + + frame.bind("readystatechange", function () { + if ($.inArray(this.readyState, ["loaded", "complete"]) >= 0) { + log("Forever frame iframe readyState changed to " + this.readyState + ", reconnecting", connection.logging); + that.reconnect(connection); + } + }); + + connection.frame = frame[0]; + connection.frameId = frameId; + + if (onSuccess) { + connection.onSuccess = onSuccess; + } + + $("body").append(frame); + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + connectTimeOut = window.setTimeout(function () { + if (connection.onSuccess) { + that.stop(connection); + + if (onFailed) { + onFailed(); + } + } + }, that.timeOut); + }, + + reconnect: function (connection) { + var that = this; + window.setTimeout(function () { + var frame = connection.frame, + src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId; + frame.src = src; + }, connection.reconnectDelay); + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + receive: transportLogic.processMessages, + + stop: function (connection) { + if (connection.frame) { + if (connection.frame.stop) { + connection.frame.stop(); + } else if (connection.frame.document && connection.frame.document.execCommand) { + connection.frame.document.execCommand("Stop"); + } + $(connection.frame).remove(); + delete transportLogic.foreverFrame.connections[connection.frameId]; + connection.frame = null; + connection.frameId = null; + delete connection.frame; + delete connection.frameId; + } + }, + + getConnection: function (id) { + return transportLogic.foreverFrame.connections[id]; + }, + + started: function (connection) { + if (connection.onSuccess) { + connection.onSuccess(); + connection.onSuccess = null; + delete connection.onSuccess; + } + else { + // If there's no onSuccess handler we assume this is a reconnect + $(connection).trigger(events.onReconnect); + } + } + }, + + longPolling: { + name: "longPolling", + + reconnectDelay: 3000, + + start: function (connection, onSuccess, onFailed) { + /// Starts the long polling connection + /// The SignalR connection to start + var that = this; + if (connection.pollXhr) { + connection.stop(); + } + + connection.messageId = null; + + window.setTimeout(function () { + (function poll(instance, raiseReconnect) { + $(instance).trigger(events.onSending); + + var messageId = instance.messageId, + connect = (messageId === null), + url = transportLogic.getUrl(instance, that.name, !connect), + reconnectTimeOut = null, + reconnectFired = false; + + instance.pollXhr = $.ajax(url, { + global: false, + + type: "GET", + + dataType: "json", + + success: function (data) { + var delay = 0, + timedOutReceived = false; + + if (raiseReconnect === true) { + // Fire the reconnect event if it hasn't been fired as yet + if (reconnectFired === false) { + $(instance).trigger(events.onReconnect); + reconnectFired = true; + } + } + + transportLogic.processMessages(instance, data); + if (data && $.type(data.TransportData.LongPollDelay) === "number") { + delay = data.TransportData.LongPollDelay; + } + + if (data && data.TimedOut) { + timedOutReceived = data.TimedOut; + } + + if (delay > 0) { + window.setTimeout(function () { + poll(instance, timedOutReceived); + }, delay); + } else { + poll(instance, timedOutReceived); + } + }, + + error: function (data, textStatus) { + if (textStatus === "abort") { + return; + } + + if (reconnectTimeOut) { + // If the request failed then we clear the timeout so that the + // reconnect event doesn't get fired + clearTimeout(reconnectTimeOut); + } + + $(instance).trigger(events.onError, [data]); + + window.setTimeout(function () { + poll(instance, true); + }, connection.reconnectDelay); + } + }); + + if (raiseReconnect === true) { + reconnectTimeOut = window.setTimeout(function () { + if (reconnectFired === false) { + $(instance).trigger(events.onReconnect); + reconnectFired = true; + } + }, + that.reconnectDelay); + } + + } (connection)); + + // Now connected + // There's no good way know when the long poll has actually started so + // we assume it only takes around 150ms (max) to start the connection + window.setTimeout(onSuccess, 150); + + }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab + }, + + send: function (connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function (connection) { + /// Stops the long polling connection + /// The SignalR connection to stop + if (connection.pollXhr) { + connection.pollXhr.abort(); + connection.pollXhr = null; + delete connection.pollXhr; + } + } + } + }; + + signalR.noConflict = function () { + /// Reinstates the original value of $.connection and returns the signalR object for manual assignment + /// + if ($.connection === signalR) { + $.connection = _connection; + } + return signalR; + }; + + if ($.connection) { + _connection = $.connection; + } + + $.connection = $.signalR = signalR; + +} (window.jQuery, window)); \ No newline at end of file diff --git a/packages/SignalR.Js.0.4.0/content/Scripts/jquery.signalR.min.js b/packages/SignalR.Js.0.4.0/content/Scripts/jquery.signalR.min.js new file mode 100644 index 000000000..9e99f53a3 --- /dev/null +++ b/packages/SignalR.Js.0.4.0/content/Scripts/jquery.signalR.min.js @@ -0,0 +1 @@ +(function(n,t){"use strict";var f,e,i,r,u;if(typeof n!="function")throw"SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file.";if(!t.JSON)throw"SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8.";i={onStart:"onStart",onStarting:"onStarting",onSending:"onSending",onReceived:"onReceived",onError:"onError",onReconnect:"onReconnect",onDisconnect:"onDisconnect"},r=function(n,i){if(i===!1)return;var r;if(typeof t.console=="undefined")return;r="["+(new Date).toTimeString()+"] SignalR: "+n,t.console.debug?t.console.debug(r):t.console.log&&t.console.log(r)},f=function(n,t,i){return new f.fn.init(n,t,i)},f.fn=f.prototype={init:function(n,t,i){this.url=n,this.qs=t,typeof i=="boolean"&&(this.logging=i)},logging:!1,reconnectDelay:2e3,start:function(r,u){var e=this,o={transport:"auto"},h,s=n.Deferred();return e.transport?(s.resolve(e),s):(n.type(r)==="function"?u=r:n.type(r)==="object"&&(n.extend(o,r),n.type(o.callback)==="function"&&(u=o.callback)),n(e).bind(i.onStart,function(){n.type(u)==="function"&&u.call(e),s.resolve(e)}),h=function(t,r){r=r||0;if(r>=t.length){e.transport||s.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization.");return}var u=t[r],o=n.type(u)==="object"?u:f.transports[u];o.start(e,function(){e.transport=o,n(e).trigger(i.onStart)},function(){h(t,r+1)})},t.setTimeout(function(){n.ajax(e.url+"/negotiate",{global:!1,type:"POST",data:{},error:function(t){n(e).trigger(i.onError,[t]),s.reject("SignalR: Error during negotiation request: "+t)},success:function(t){e.appRelativeUrl=t.Url,e.id=t.ConnectionId,e.webSocketServerUrl=t.WebSocketServerUrl;if(!t.ProtocolVersion||t.ProtocolVersion!=="1.0"){n(e).trigger(i.onError,"SignalR: Incompatible protocol version."),s.reject("SignalR: Incompatible protocol version.");return}n(e).trigger(i.onStarting);var u=[],r=[];n.each(f.transports,function(n){if(n==="webSockets"&&!t.TryWebSockets)return!0;r.push(n)}),n.isArray(o.transport)?n.each(o.transport,function(){var t=this;n.type(t)!=="object"&&(n.type(t)!=="string"||n.inArray(""+t,r)<0)||u.push(n.type(t)==="string"?""+t:t)}):n.type(o.transport)!=="object"&&n.inArray(o.transport,r)<0?u=r:u.push(o.transport),h(u)}})},0),s)},starting:function(t){var r=this,u=n(r);return u.bind(i.onStarting,function(){t.call(r),u.unbind(i.onStarting)}),r},send:function(n){var t=this;if(!t.transport)throw"SignalR: Connection must be started before data can be sent. Call .start() before .send()";return t.transport.send(t,n),t},sending:function(t){var r=this;return n(r).bind(i.onSending,function(){t.call(r)}),r},received:function(t){var r=this;return n(r).bind(i.onReceived,function(n,i){t.call(r,i)}),r},error:function(t){var r=this;return n(r).bind(i.onError,function(n,i){t.call(r,i)}),r},disconnected:function(t){var r=this;return n(r).bind(i.onDisconnect,function(){t.call(r)}),r},reconnected:function(t){var r=this;return n(r).bind(i.onReconnect,function(){t.call(r)}),r},stop:function(){var n=this;return n.transport&&(n.transport.stop(n),n.transport=null),delete n.messageId,delete n.groups,$connection.trigger(i.onDisconnect),n},log:r},f.fn.init.prototype=f.fn,u={addQs:function(t,i){return i.qs?typeof i.qs=="object"?t+"&"+n.param(i.qs):typeof i.qs=="string"?t+"&"+i.qs:t+"&"+escape(i.qs.toString()):t},getUrl:function(n,i,r){var u=n.url,f="transport="+i+"&connectionId="+t.escape(n.id);return n.data&&(f+="&connectionData="+t.escape(n.data)),r?(n.messageId&&(f+="&messageId="+n.messageId),n.groups&&(f+="&groups="+t.escape(JSON.stringify(n.groups)))):u=u+"/connect",u+="?"+f,u=this.addQs(u,n)},ajaxSend:function(r,u){var f=r.url+"/send?transport="+r.transport.name+"&connectionId="+t.escape(r.id);f=this.addQs(f,r),n.ajax(f,{global:!1,type:"POST",dataType:"json",data:{data:u},success:function(t){t&&n(r).trigger(i.onReceived,[t])},error:function(t,u){if(u==="abort")return;n(r).trigger(i.onError,[t])}})},processMessages:function(t,u){var f=n(t);if(u){if(u.Disconnect){r("Disconnect command received from server",t.logging),t.stop(),f.trigger(i.onDisconnect);return}u.Messages&&n.each(u.Messages,function(){try{f.trigger(i.onReceived,[this])}catch(u){r("Error raising received "+u,t.logging),n(t).trigger(i.onError,[u])}}),t.messageId=u.MessageId,t.groups=u.TransportData.Groups}},foreverFrame:{count:0,connections:{}}},f.transports={webSockets:{name:"webSockets",send:function(n,t){n.socket.send(t)},start:function(u,f,e){var o,h=!1,s;t.MozWebSocket&&(t.WebSocket=t.MozWebSocket);if(!t.WebSocket){e();return}u.socket||(u.webSocketServerUrl?o=u.webSocketServerUrl:(s=document.location.protocol==="https:"?"wss://":"ws://",o=s+document.location.host+u.appRelativeUrl),n(u).trigger(i.onSending),o+=u.data?"?connectionData="+u.data+"&transport=webSockets&connectionId="+u.id:"?transport=webSockets&connectionId="+u.id,u.socket=new t.WebSocket(o),u.socket.onopen=function(){h=!0,f&&f()},u.socket.onclose=function(t){h?typeof t.wasClean!="undefined"&&t.wasClean===!1&&n(u).trigger(i.onError):e&&e(),u.socket=null},u.socket.onmessage=function(f){var e=t.JSON.parse(f.data),o;e&&(o=n(u),e.Messages?n.each(e.Messages,function(){try{o.trigger(i.onReceived,[this])}catch(n){r("Error raising received "+n,u.logging)}}):o.trigger(i.onReceived,[e]))})},stop:function(n){n.socket!==null&&(n.socket.close(),n.socket=null)}},serverSentEvents:{name:"serverSentEvents",timeOut:3e3,start:function(f,e,o){var s=this,l=!1,c=n(f),h=!e,v,a;f.eventSource&&f.stop();if(!t.EventSource){o&&o();return}c.trigger(i.onSending),v=u.getUrl(f,this.name,h);try{f.eventSource=new t.EventSource(v)}catch(y){r("EventSource failed trying to connect with error "+y.Message,f.logging),o?o():(c.trigger(i.onError,[y]),h&&(r("EventSource reconnecting",f.logging),s.reconnect(f)));return}a=t.setTimeout(function(){l===!1&&(r("EventSource timed out trying to connect",f.logging),o&&o(),h?(r("EventSource reconnecting",f.logging),s.reconnect(f)):s.stop(f))},s.timeOut),f.eventSource.addEventListener("open",function(){r("EventSource connected",f.logging),a&&t.clearTimeout(a),l===!1&&(l=!0,e&&e(),h&&c.trigger(i.onReconnect))},!1),f.eventSource.addEventListener("message",function(n){if(n.data==="initialized")return;u.processMessages(f,t.JSON.parse(n.data))},!1),f.eventSource.addEventListener("error",function(n){if(!l){o&&o();return}r("EventSource readyState: "+f.eventSource.readyState,f.logging),n.eventPhase===t.EventSource.CLOSED?f.eventSource.readyState===t.EventSource.CONNECTING?(r("EventSource reconnecting due to the server connection ending",f.logging),s.reconnect(f)):(r("EventSource closed",f.logging),s.stop(f)):(r("EventSource error",f.logging),c.trigger(i.onError))},!1)},reconnect:function(n){var i=this;t.setTimeout(function(){i.stop(n),i.start(n)},n.reconnectDelay)},send:function(n,t){u.ajaxSend(n,t)},stop:function(n){n&&n.eventSource&&(n.eventSource.close(),n.eventSource=null,delete n.eventSource)}},foreverFrame:{name:"foreverFrame",timeOut:3e3,start:function(f,e,o){var h=this,l=u.foreverFrame.count+=1,c,a,s=n("");if(t.EventSource){o&&o();return}n(f).trigger(i.onSending),c=u.getUrl(f,this.name),c+="&frameId="+l,s.prop("src",c),u.foreverFrame.connections[l]=f,s.bind("readystatechange",function(){n.inArray(this.readyState,["loaded","complete"])<0||(r("Forever frame iframe readyState changed to "+this.readyState+", reconnecting",f.logging),h.reconnect(f))}),f.frame=s[0],f.frameId=l,e&&(f.onSuccess=e),n("body").append(s),a=t.setTimeout(function(){f.onSuccess&&(h.stop(f),o&&o())},h.timeOut)},reconnect:function(n){var i=this;t.setTimeout(function(){var r=n.frame,t=u.getUrl(n,i.name,!0)+"&frameId="+n.frameId;r.src=t},n.reconnectDelay)},send:function(n,t){u.ajaxSend(n,t)},receive:u.processMessages,stop:function(t){t.frame&&(t.frame.stop?t.frame.stop():t.frame.document&&t.frame.document.execCommand&&t.frame.document.execCommand("Stop"),n(t.frame).remove(),delete u.foreverFrame.connections[t.frameId],t.frame=null,t.frameId=null,delete t.frame,delete t.frameId)},getConnection:function(n){return u.foreverFrame.connections[n]},started:function(t){t.onSuccess?(t.onSuccess(),t.onSuccess=null,delete t.onSuccess):n(t).trigger(i.onReconnect)}},longPolling:{name:"longPolling",reconnectDelay:3e3,start:function(r,f){var o=this;r.pollXhr&&r.stop(),r.messageId=null,t.setTimeout(function(){(function e(f,s){n(f).trigger(i.onSending);var a=f.messageId,l=a===null,v=u.getUrl(f,o.name,!l),c=null,h=!1;f.pollXhr=n.ajax(v,{global:!1,type:"GET",dataType:"json",success:function(r){var c=0,o=!1;s===!0&&h===!1&&(n(f).trigger(i.onReconnect),h=!0),u.processMessages(f,r),r&&n.type(r.TransportData.LongPollDelay)==="number"&&(c=r.TransportData.LongPollDelay),r&&r.TimedOut&&(o=r.TimedOut),c>0?t.setTimeout(function(){e(f,o)},c):e(f,o)},error:function(u,o){if(o==="abort")return;c&&clearTimeout(c),n(f).trigger(i.onError,[u]),t.setTimeout(function(){e(f,!0)},r.reconnectDelay)}}),s===!0&&(c=t.setTimeout(function(){h===!1&&(n(f).trigger(i.onReconnect),h=!0)},o.reconnectDelay))})(r),t.setTimeout(f,150)},250)},send:function(n,t){u.ajaxSend(n,t)},stop:function(n){n.pollXhr&&(n.pollXhr.abort(),n.pollXhr=null,delete n.pollXhr)}}},f.noConflict=function(){return n.connection===f&&(n.connection=e),f},n.connection&&(e=n.connection),n.connection=n.signalR=f})(window.jQuery,window) \ No newline at end of file diff --git a/packages/SignalR.Server.0.4.0.0/SignalR.Server.0.4.0.0.nupkg b/packages/SignalR.Server.0.4.0.0/SignalR.Server.0.4.0.0.nupkg new file mode 100644 index 000000000..5011cccc0 Binary files /dev/null and b/packages/SignalR.Server.0.4.0.0/SignalR.Server.0.4.0.0.nupkg differ diff --git a/packages/SignalR.Server.0.4.0.0/lib/net40/SignalR.dll b/packages/SignalR.Server.0.4.0.0/lib/net40/SignalR.dll new file mode 100644 index 000000000..b845a49c6 Binary files /dev/null and b/packages/SignalR.Server.0.4.0.0/lib/net40/SignalR.dll differ