From 9d5cb6f3e016af33a694520645c03d965a9c3464 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 30 Apr 2013 17:01:54 -0700 Subject: [PATCH] Model driven indexer settings --- ...switch.css => bootstrap.toggle-switch.css} | 0 UI/Index.html | 2 + UI/JsLibraries/backbone.deep.model.js | 438 ++++++++++++++++++ UI/Mixins/backbone.marionette.templates.js | 2 +- UI/Mixins/handlebars.mixin.js | 20 + UI/Settings/Indexers/Collection.js | 6 + UI/Settings/Indexers/CollectionTemplate.html | 5 + UI/Settings/Indexers/CollectionView.js | 10 + UI/Settings/Indexers/IndexersTemplate.html | 3 - UI/Settings/Indexers/IndexersView.js | 11 - UI/Settings/Indexers/ItemTemplate.html | 38 ++ UI/Settings/Indexers/ItemView.js | 38 ++ UI/Settings/Indexers/Model.js | 13 + UI/Settings/SettingsLayout.js | 7 +- 14 files changed, 576 insertions(+), 17 deletions(-) rename UI/Content/{toggle-switch.css => bootstrap.toggle-switch.css} (100%) create mode 100644 UI/JsLibraries/backbone.deep.model.js create mode 100644 UI/Mixins/handlebars.mixin.js create mode 100644 UI/Settings/Indexers/Collection.js create mode 100644 UI/Settings/Indexers/CollectionTemplate.html create mode 100644 UI/Settings/Indexers/CollectionView.js delete mode 100644 UI/Settings/Indexers/IndexersTemplate.html delete mode 100644 UI/Settings/Indexers/IndexersView.js create mode 100644 UI/Settings/Indexers/ItemTemplate.html create mode 100644 UI/Settings/Indexers/ItemView.js create mode 100644 UI/Settings/Indexers/Model.js diff --git a/UI/Content/toggle-switch.css b/UI/Content/bootstrap.toggle-switch.css similarity index 100% rename from UI/Content/toggle-switch.css rename to UI/Content/bootstrap.toggle-switch.css diff --git a/UI/Index.html b/UI/Index.html index 2af07e3e8..01d1fbf37 100644 --- a/UI/Index.html +++ b/UI/Index.html @@ -97,6 +97,7 @@ + @@ -120,6 +121,7 @@ + diff --git a/UI/JsLibraries/backbone.deep.model.js b/UI/JsLibraries/backbone.deep.model.js new file mode 100644 index 000000000..5307ffcf3 --- /dev/null +++ b/UI/JsLibraries/backbone.deep.model.js @@ -0,0 +1,438 @@ +/*jshint expr:true eqnull:true */ +/** + * + * Backbone.DeepModel v0.10.4 + * + * Copyright (c) 2013 Charles Davison, Pow Media Ltd + * + * https://github.com/powmedia/backbone-deep-model + * Licensed under the MIT License + */ + +/** + * Underscore mixins for deep objects + * + * Based on https://gist.github.com/echong/3861963 + */ +(function() { + var arrays, basicObjects, deepClone, deepExtend, deepExtendCouple, isBasicObject, + __slice = [].slice; + + deepClone = function(obj) { + var func, isArr; + if (!_.isObject(obj) || _.isFunction(obj)) { + return obj; + } + if (obj instanceof Backbone.Collection || obj instanceof Backbone.Model) { + return obj; + } + if (_.isDate(obj)) { + return new Date(obj.getTime()); + } + if (_.isRegExp(obj)) { + return new RegExp(obj.source, obj.toString().replace(/.*\//, "")); + } + isArr = _.isArray(obj || _.isArguments(obj)); + func = function(memo, value, key) { + if (isArr) { + memo.push(deepClone(value)); + } else { + memo[key] = deepClone(value); + } + return memo; + }; + return _.reduce(obj, func, isArr ? [] : {}); + }; + + isBasicObject = function(object) { + if (object == null) return false; + return (object.prototype === {}.prototype || object.prototype === Object.prototype) && _.isObject(object) && !_.isArray(object) && !_.isFunction(object) && !_.isDate(object) && !_.isRegExp(object) && !_.isArguments(object); + }; + + basicObjects = function(object) { + return _.filter(_.keys(object), function(key) { + return isBasicObject(object[key]); + }); + }; + + arrays = function(object) { + return _.filter(_.keys(object), function(key) { + return _.isArray(object[key]); + }); + }; + + deepExtendCouple = function(destination, source, maxDepth) { + var combine, recurse, sharedArrayKey, sharedArrayKeys, sharedObjectKey, sharedObjectKeys, _i, _j, _len, _len1; + if (maxDepth == null) { + maxDepth = 20; + } + if (maxDepth <= 0) { + console.warn('_.deepExtend(): Maximum depth of recursion hit.'); + return _.extend(destination, source); + } + sharedObjectKeys = _.intersection(basicObjects(destination), basicObjects(source)); + recurse = function(key) { + return source[key] = deepExtendCouple(destination[key], source[key], maxDepth - 1); + }; + for (_i = 0, _len = sharedObjectKeys.length; _i < _len; _i++) { + sharedObjectKey = sharedObjectKeys[_i]; + recurse(sharedObjectKey); + } + sharedArrayKeys = _.intersection(arrays(destination), arrays(source)); + combine = function(key) { + return source[key] = _.union(destination[key], source[key]); + }; + for (_j = 0, _len1 = sharedArrayKeys.length; _j < _len1; _j++) { + sharedArrayKey = sharedArrayKeys[_j]; + combine(sharedArrayKey); + } + return _.extend(destination, source); + }; + + deepExtend = function() { + var finalObj, maxDepth, objects, _i; + objects = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), maxDepth = arguments[_i++]; + if (!_.isNumber(maxDepth)) { + objects.push(maxDepth); + maxDepth = 20; + } + if (objects.length <= 1) { + return objects[0]; + } + if (maxDepth <= 0) { + return _.extend.apply(this, objects); + } + finalObj = objects.shift(); + while (objects.length > 0) { + finalObj = deepExtendCouple(finalObj, deepClone(objects.shift()), maxDepth); + } + return finalObj; + }; + + _.mixin({ + deepClone: deepClone, + isBasicObject: isBasicObject, + basicObjects: basicObjects, + arrays: arrays, + deepExtend: deepExtend + }); + +}).call(this); + +/** + * Main source + */ + +;(function(factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define(['underscore', 'backbone'], factory); + } else { + // globals + factory(_, Backbone); + } +}(function(_, Backbone) { + + /** + * Takes a nested object and returns a shallow object keyed with the path names + * e.g. { "level1.level2": "value" } + * + * @param {Object} Nested object e.g. { level1: { level2: 'value' } } + * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' } + */ + function objToPaths(obj) { + var ret = {}, + separator = DeepModel.keyPathSeparator; + + for (var key in obj) { + var val = obj[key]; + + if (val && val.constructor === Object && !_.isEmpty(val)) { + //Recursion for embedded objects + var obj2 = objToPaths(val); + + for (var key2 in obj2) { + var val2 = obj2[key2]; + + ret[key + separator + key2] = val2; + } + } else { + ret[key] = val; + } + } + + return ret; + } + + /** + * @param {Object} Object to fetch attribute from + * @param {String} Object path e.g. 'user.name' + * @return {Mixed} + */ + function getNested(obj, path, return_exists) { + var separator = DeepModel.keyPathSeparator; + + var fields = path.split(separator); + var result = obj; + return_exists || (return_exists === false); + for (var i = 0, n = fields.length; i < n; i++) { + if (return_exists && !_.has(result, fields[i])) { + return false; + } + result = result[fields[i]]; + + if (result == null && i < n - 1) { + result = {}; + } + + if (typeof result === 'undefined') { + if (return_exists) + { + return true; + } + return result; + } + } + if (return_exists) + { + return true; + } + return result; + } + + /** + * @param {Object} obj Object to fetch attribute from + * @param {String} path Object path e.g. 'user.name' + * @param {Object} [options] Options + * @param {Boolean} [options.unset] Whether to delete the value + * @param {Mixed} Value to set + */ + function setNested(obj, path, val, options) { + options = options || {}; + + var separator = DeepModel.keyPathSeparator; + + var fields = path.split(separator); + var result = obj; + for (var i = 0, n = fields.length; i < n && result !== undefined ; i++) { + var field = fields[i]; + + //If the last in the path, set the value + if (i === n - 1) { + options.unset ? delete result[field] : result[field] = val; + } else { + //Create the child object if it doesn't exist, or isn't an object + if (typeof result[field] === 'undefined' || ! _.isObject(result[field])) { + result[field] = {}; + } + + //Move onto the next part of the path + result = result[field]; + } + } + } + + function deleteNested(obj, path) { + setNested(obj, path, null, { unset: true }); + } + + var DeepModel = Backbone.Model.extend({ + + // Override constructor + // Support having nested defaults by using _.deepExtend instead of _.extend + constructor: function(attributes, options) { + var defaults; + var attrs = attributes || {}; + this.cid = _.uniqueId('c'); + this.attributes = {}; + if (options && options.collection) this.collection = options.collection; + if (options && options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, 'defaults')) { + // + // Replaced the call to _.defaults with _.deepExtend. + attrs = _.deepExtend({}, defaults, attrs); + // + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.deepClone(this.attributes); + }, + + // Override get + // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' + get: function(attr) { + return getNested(this.attributes, attr); + }, + + // Override set + // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val || {}; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.deepClone(this.attributes); //: Replaced _.clone with _.deepClone + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // + attrs = objToPaths(attrs); + // + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + + //: Using getNested, setNested and deleteNested + if (!_.isEqual(getNested(current, attr), val)) changes.push(attr); + if (!_.isEqual(getNested(prev, attr), val)) { + setNested(this.changed, attr, val); + } else { + deleteNested(this.changed, attr); + } + unset ? deleteNested(current, attr) : setNested(current, attr, val); + // + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + + // + var separator = DeepModel.keyPathSeparator; + + for (var i = 0, l = changes.length; i < l; i++) { + var key = changes[i]; + + this.trigger('change:' + key, this, getNested(current, key), options); + + var fields = key.split(separator); + + //Trigger change events for parent keys with wildcard (*) notation + for(var n = fields.length - 1; n > 0; n--) { + var parentKey = _.first(fields, n).join(separator), + wildcardKey = parentKey + separator + '*'; + + this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); + } + // + } + } + + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Clear all attributes on the model, firing `"change"` unless you choose + // to silence it. + clear: function(options) { + var attrs = {}; + var shallowAttributes = objToPaths(this.attributes); + for (var key in shallowAttributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return getNested(this.changed, attr) !== undefined; + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + //: objToPaths + if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false; + // + + var old = this._changing ? this._previousAttributes : this.attributes; + + // + diff = objToPaths(diff); + old = objToPaths(old); + // + + var val, changed = false; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + + // + return getNested(this._previousAttributes, attr); + // + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + // + return _.deepClone(this._previousAttributes); + // + } + }); + + + //Config; override in your app to customise + DeepModel.keyPathSeparator = '.'; + + + //Exports + Backbone.DeepModel = DeepModel; + + //For use in NodeJS + if (typeof module != 'undefined') module.exports = DeepModel; + + return Backbone; + +})); + diff --git a/UI/Mixins/backbone.marionette.templates.js b/UI/Mixins/backbone.marionette.templates.js index 876cbd58b..48342d1a2 100644 --- a/UI/Mixins/backbone.marionette.templates.js +++ b/UI/Mixins/backbone.marionette.templates.js @@ -4,7 +4,7 @@ Marionette.TemplateCache.get = function (templateId) { var templateKey = templateId.toLowerCase(); - var templateFunction = window.Templates[templateKey.toLowerCase()]; + var templateFunction = window.Templates[templateKey]; if (!templateFunction) { throw 'couldn\'t find pre-compiled template ' + templateKey; diff --git a/UI/Mixins/handlebars.mixin.js b/UI/Mixins/handlebars.mixin.js new file mode 100644 index 000000000..322a8f8cc --- /dev/null +++ b/UI/Mixins/handlebars.mixin.js @@ -0,0 +1,20 @@ +'use strict'; + +Handlebars.registerHelper('partial', function(templateName, context){ + //TODO: We should be able to pass in the context, either an object or a property + + var templateFunction = Marionette.TemplateCache.get(templateName); + return new Handlebars.SafeString(templateFunction(this)); +}); + +Handlebars.registerHelper("debug", function(optionalValue) { + console.log("Current Context"); + console.log("===================="); + console.log(this); + + if (optionalValue) { + console.log("Value"); + console.log("===================="); + console.log(optionalValue); + } +}); \ No newline at end of file diff --git a/UI/Settings/Indexers/Collection.js b/UI/Settings/Indexers/Collection.js new file mode 100644 index 000000000..fec1cead2 --- /dev/null +++ b/UI/Settings/Indexers/Collection.js @@ -0,0 +1,6 @@ +define(['app', 'Settings/Indexers/Model'], function () { + NzbDrone.Settings.Indexers.Collection = Backbone.Collection.extend({ + url : NzbDrone.Constants.ApiRoot + '/indexer', + model: NzbDrone.Settings.Indexers.Model + }); +}); \ No newline at end of file diff --git a/UI/Settings/Indexers/CollectionTemplate.html b/UI/Settings/Indexers/CollectionTemplate.html new file mode 100644 index 000000000..c42cbaa79 --- /dev/null +++ b/UI/Settings/Indexers/CollectionTemplate.html @@ -0,0 +1,5 @@ +
+
+
+
+
\ No newline at end of file diff --git a/UI/Settings/Indexers/CollectionView.js b/UI/Settings/Indexers/CollectionView.js new file mode 100644 index 000000000..51023fc28 --- /dev/null +++ b/UI/Settings/Indexers/CollectionView.js @@ -0,0 +1,10 @@ +'use strict'; + +define(['app', 'Settings/Indexers/ItemView'], function (app) { + + NzbDrone.Settings.Indexers.CollectionView = Backbone.Marionette.CompositeView.extend({ + itemView : NzbDrone.Settings.Indexers.ItemView, + itemViewContainer : '#x-indexers', + template : 'Settings/Indexers/CollectionTemplate' + }); +}); \ No newline at end of file diff --git a/UI/Settings/Indexers/IndexersTemplate.html b/UI/Settings/Indexers/IndexersTemplate.html deleted file mode 100644 index 70e312099..000000000 --- a/UI/Settings/Indexers/IndexersTemplate.html +++ /dev/null @@ -1,3 +0,0 @@ -
- Indexer settings will go here -
\ No newline at end of file diff --git a/UI/Settings/Indexers/IndexersView.js b/UI/Settings/Indexers/IndexersView.js deleted file mode 100644 index 2592b802e..000000000 --- a/UI/Settings/Indexers/IndexersView.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -define([ - 'app', 'Settings/SettingsModel' - -], function () { - - NzbDrone.Settings.Indexers.IndexersView = Backbone.Marionette.ItemView.extend({ - template: 'Settings/Indexers/IndexersTemplate' - }); -}); diff --git a/UI/Settings/Indexers/ItemTemplate.html b/UI/Settings/Indexers/ItemTemplate.html new file mode 100644 index 000000000..0a1536002 --- /dev/null +++ b/UI/Settings/Indexers/ItemTemplate.html @@ -0,0 +1,38 @@ +
+ {{name}} + +
+ + +
+ + + + + +
+
+ + {{debug}} + + {{#each fields}} +
+ + +
+ + + + +
+
+ {{/each}} +
\ No newline at end of file diff --git a/UI/Settings/Indexers/ItemView.js b/UI/Settings/Indexers/ItemView.js new file mode 100644 index 000000000..24d9feae5 --- /dev/null +++ b/UI/Settings/Indexers/ItemView.js @@ -0,0 +1,38 @@ +"use strict"; + +define([ + 'app', + 'Settings/Indexers/Collection' + +], function () { + + NzbDrone.Settings.Indexers.ItemView = Backbone.Marionette.ItemView.extend({ + template: 'Settings/Indexers/ItemTemplate', + initialize: function () { + this.model.set('fields', [ + { key: 'username', title: 'Username', helpText: 'HALP!', value: 'mark' }, + { key: 'apiKey', title: 'API Key', helpText: 'HALP!', value: 'private' } + ]); + + NzbDrone.vent.on(NzbDrone.Commands.SaveSettings, this.saveSettings, this); + }, + + saveSettings: function () { + var test = 1; + //this.model.save(undefined, this.syncNotification("Naming Settings Saved", "Couldn't Save Naming Settings")); + }, + + + syncNotification: function (success, error) { + return { + success: function () { + window.alert(success); + }, + + error: function () { + window.alert(error); + } + }; + } + }); +}); diff --git a/UI/Settings/Indexers/Model.js b/UI/Settings/Indexers/Model.js new file mode 100644 index 000000000..f1cf3792a --- /dev/null +++ b/UI/Settings/Indexers/Model.js @@ -0,0 +1,13 @@ +"use strict"; +define(['app'], function (app) { + NzbDrone.Settings.Indexers.Model = Backbone.DeepModel.extend({ + mutators: { + fields: function () { + return [ + { key: 'username', title: 'Username', helpText: 'HALP!', value: 'mark', index: 0 }, + { key: 'apiKey', title: 'API Key', helpText: 'HALP!', value: '', index: 1 } + ]; + } + } + }); +}); diff --git a/UI/Settings/SettingsLayout.js b/UI/Settings/SettingsLayout.js index fcfca04f1..4a3e6da9f 100644 --- a/UI/Settings/SettingsLayout.js +++ b/UI/Settings/SettingsLayout.js @@ -3,7 +3,7 @@ define([ 'app', 'Settings/Naming/NamingView', 'Settings/Quality/QualityLayout', - 'Settings/Indexers/IndexersView', + 'Settings/Indexers/CollectionView', 'Settings/DownloadClient/DownloadClientView', 'Settings/Notifications/NotificationsView', 'Settings/System/SystemView', @@ -112,6 +112,9 @@ define([ this.namingSettings = new NzbDrone.Settings.Naming.NamingModel(); this.namingSettings.fetch(); + this.indexerSettings = new NzbDrone.Settings.Indexers.Collection(); + this.indexerSettings.fetch(); + if (options.action) { this.action = options.action.toLowerCase(); } @@ -120,7 +123,7 @@ define([ onRender: function () { this.naming.show(new NzbDrone.Settings.Naming.NamingView()); this.quality.show(new NzbDrone.Settings.Quality.QualityLayout({settings: this.settings})); - this.indexers.show(new NzbDrone.Settings.Indexers.IndexersView({model: this.settings})); + this.indexers.show(new NzbDrone.Settings.Indexers.CollectionView({collection: this.indexerSettings})); this.downloadClient.show(new NzbDrone.Settings.DownloadClient.DownloadClientView({model: this.settings})); this.notifications.show(new NzbDrone.Settings.Notifications.NotificationsView({model: this.settings})); this.system.show(new NzbDrone.Settings.System.SystemView({model: this.settings}));