Merge branch 'quality-definitions' into develop

Conflicts:
	src/UI/Settings/Quality/Profile/EditQualityProfileTemplate.html
	src/UI/app.js
This commit is contained in:
Mark McDowall
2014-02-03 17:29:22 -08:00
120 changed files with 7114 additions and 1218 deletions
@@ -0,0 +1,16 @@
<fieldset>
<legend>Quality Definitions</legend>
<div class="span11">
<div id="quality-definition-list">
<div class="quality-header x-header">
<div class="row">
<span class="span2">Quality</span>
<span class="span2">Title</span>
<span class="offset1 span4">Size Limit</span>
</div>
</div>
<div class="rows x-rows">
</div>
</div>
</div>
</fieldset>
@@ -0,0 +1,17 @@
'use strict';
define(
[
'marionette',
'backgrid',
'Settings/Quality/Definition/QualityDefinitionView'
], function (Marionette, Backgrid, QualityDefinitionView) {
return Marionette.CompositeView.extend({
template: 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate',
itemViewContainer: ".x-rows",
itemView: QualityDefinitionView
});
});
@@ -0,0 +1,31 @@
<span class="span2">
{{quality.name}}
</span>
<span class="span2">
<input type="text" class="x-title input-block-level" value="{{title}}">
</span>
<span class="offset1 span4">
<div class="x-slider"></div>
<div class="size-label-wrapper">
<div class="pull-left">
<span class="label label-warning x-min-thirty"
name="thirtyMinuteMinSize"
title="Minimum size for a 30 minute episode">
</span>
<span class="label label-info x-min-sixty"
name="sixtyMinuteMinSize"
title="Minimum size for a 60 minute episode">
</span>
</div>
<div class="pull-right">
<span class="label label-warning x-max-thirty"
name="thirtyMinuteMaxSize"
title="Maximum size for a 30 minute episode">
</span>
<span class="label label-info x-max-sixty"
name="sixtyMinuteMaxSize"
title="Maximum size for a 60 minute episode">
</span>
</div>
</div>
</span>
@@ -0,0 +1,86 @@
'use strict';
define(
[
'marionette',
'Mixins/AsModelBoundView',
'filesize',
'jquery-ui'
], function (Marionette, AsModelBoundView, fileSize) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Definition/QualityDefinitionTemplate',
className: 'row',
ui: {
title : '.x-title',
sizeSlider : '.x-slider',
thirtyMinuteMinSize: '.x-min-thirty',
sixtyMinuteMinSize : '.x-min-sixty',
thirtyMinuteMaxSize: '.x-max-thirty',
sixtyMinuteMaxSize : '.x-max-sixty'
},
events: {
'change .x-title': '_updateTitle',
'slide .x-slider': '_updateSize'
},
initialize: function (options) {
this.qualityProfileCollection = options.qualityProfiles;
this.filesize = fileSize;
},
onRender: function () {
this.ui.sizeSlider.slider({
range : true,
min : 0,
max : 200,
values : [ this.model.get('minSize'), this.model.get('maxSize') ],
});
this._changeSize();
},
_updateTitle: function() {
this.model.set('title', this.ui.title.val());
},
_updateSize: function (event, ui) {
this.model.set('minSize', ui.values[0]);
this.model.set('maxSize', ui.values[1]);
this._changeSize();
},
_changeSize: function () {
var minSize = this.model.get('minSize');
var maxSize = this.model.get('maxSize');
{
var minBytes = minSize * 1024 * 1024;
var minThirty = fileSize(minBytes * 30, 1, false);
var minSixty = fileSize(minBytes * 60, 1, false);
this.ui.thirtyMinuteMinSize.html(minThirty);
this.ui.sixtyMinuteMinSize.html(minSixty);
}
{
var maxBytes = maxSize * 1024 * 1024;
var maxThirty = fileSize(maxBytes * 30, 1, false);
var maxSixty = fileSize(maxBytes * 60, 1, false);
this.ui.thirtyMinuteMaxSize.html(maxThirty);
this.ui.sixtyMinuteMaxSize.html(maxSixty);
}
/*if (parseInt(maxSize, 10) === 0) {
thirty = 'No Limit';
sixty = 'No Limit';
}*/
}
});
return AsModelBoundView.call(view);
});
@@ -7,13 +7,14 @@ define(
Handlebars.registerHelper('allowedLabeler', function () {
var ret = '';
var cutoff = this.cutoff;
_.each(this.allowed, function (allowed) {
if (allowed.id === cutoff.id) {
ret += '<span class="label label-info" title="Cutoff">' + allowed.name + '</span> ';
}
else {
ret += '<span class="label">' + allowed.name + '</span> ';
_.each(this.items, function (item) {
if (item.allowed) {
if (item.quality.id === cutoff.id) {
ret += '<span class="label label-info" title="Cutoff">' + item.quality.name + '</span>&nbsp;';
}
else {
ret += '<span class="label">' + item.quality.name + '</span>&nbsp;';
}
}
});
@@ -0,0 +1,9 @@
'use strict';
define(
[
'marionette'
], function (Marionette) {
return Marionette.ItemView.extend({
template : 'Settings/Quality/Profile/Edit/EditQualityProfileItemViewTemplate'
});
});
@@ -0,0 +1,3 @@
<i class="select-handle pull-left x-select" />
<span class="quality-label">{{quality.name}}</span>
<i class="drag-handle pull-right icon-reorder advanced-setting x-drag-handle" />
@@ -0,0 +1,108 @@
'use strict';
define(
[
'underscore',
'vent',
'marionette',
'backbone',
'Settings/Quality/Profile/Edit/EditQualityProfileItemView',
'Settings/Quality/Profile/Edit/QualitySortableCollectionView',
'Settings/Quality/Profile/Edit/EditQualityProfileView',
'Config'
], function (_, vent, Marionette, Backbone, EditQualityProfileItemView, QualitySortableCollectionView, EditQualityProfileView, Config) {
return Marionette.Layout.extend({
template: 'Settings/Quality/Profile/Edit/EditQualityProfileLayoutTemplate',
regions: {
fields : '#x-fields',
qualities: '#x-qualities'
},
events: {
'click .x-save' : '_saveQualityProfile',
'click .x-cancel': '_cancelQualityProfile'
},
initialize: function (options) {
this.profileCollection = options.profileCollection;
this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse());
},
onShow: function () {
this.fieldsView = new EditQualityProfileView({ model: this.model });
this._showFieldsView();
this.sortableListView = new QualitySortableCollectionView({
selectable : true,
selectMultiple : true,
clickToSelect : true,
clickToToggle : true,
sortable : Config.getValueBoolean(Config.Keys.AdvancedSettings, false),
sortableOptions : {
handle: '.x-drag-handle'
},
collection: this.itemsCollection,
model : this.model
});
this.sortableListView.setSelectedModels(this.itemsCollection.filter(function(item) { return item.get('allowed') === true; }));
this.qualities.show(this.sortableListView);
this.listenTo(this.sortableListView, 'selectionChanged', this._selectionChanged);
this.listenTo(this.sortableListView, 'sortStop', this._updateModel);
},
_selectionChanged: function(newSelectedModels, oldSelectedModels) {
var addedModels = _.difference(newSelectedModels, oldSelectedModels);
var removeModels = _.difference(oldSelectedModels, newSelectedModels);
_.each(removeModels, function(item) { item.set('allowed', false); });
_.each(addedModels, function(item) { item.set('allowed', true); });
this._updateModel();
},
_updateModel: function() {
this.model.set('items', this.itemsCollection.toJSON().reverse());
this._showFieldsView();
},
_saveQualityProfile: function () {
var self = this;
var cutoff = this.fieldsView.getCutoff();
this.model.set('cutoff', cutoff);
var promise = this.model.save();
if (promise) {
promise.done(function () {
self.profileCollection.add(self.model, { merge: true });
vent.trigger(vent.Commands.CloseModalCommand);
});
}
},
_cancelQualityProfile: function () {
if (!this.model.has('id')) {
vent.trigger(vent.Commands.CloseModalCommand);
return;
}
var promise = this.model.fetch();
if (promise) {
promise.done(function () {
vent.trigger(vent.Commands.CloseModalCommand);
});
}
},
_showFieldsView: function () {
this.fields.show(this.fieldsView);
}
});
});
@@ -0,0 +1,29 @@
<div class="modal-header">
<button type="button" class="close x-cancel" aria-hidden="true">&times;</button>
{{#if id}}
<h3>Edit</h3>
{{else}}
<h3>Add</h3>
{{/if}}
</div>
<div class="modal-body">
<div class="form-horizontal">
<div id="x-fields"></div>
<div class="control-group">
<label class="control-label">Qualities</label>
<div class="controls qualities-controls">
<span id="x-qualities"></span>
<span class="help-inline">
<i class="icon-question-sign" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/>
</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">delete</button>
{{/if}}
<button class="btn x-cancel">cancel</button>
<button class="btn btn-primary x-save">save</button>
</div>
@@ -0,0 +1,26 @@
'use strict';
define(
[
'underscore',
'marionette',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView'
], function (_, Marionette, AsModelBoundView, AsValidatedView) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Profile/Edit/EditQualityProfileViewTemplate',
ui: {
cutoff : '.x-cutoff'
},
getCutoff: function () {
var self = this;
return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)});
}
});
AsValidatedView.call(view);
return AsModelBoundView.call(view);
});
@@ -0,0 +1,21 @@
<div class="control-group">
<label class="control-label">Name</label>
<div class="controls">
<input type="text" name="name">
</div>
</div>
<div class="control-group">
<label class="control-label">Cutoff</label>
<div class="controls">
<select class="x-cutoff" name="cutoff.id" validation-name="cutoff">
{{#eachReverse items}}
{{#if allowed}}
<option value="{{quality.id}}">{{quality.name}}</option>
{{/if}}
{{/eachReverse}}
</select>
<span class="help-inline">
<i class="icon-question-sign" title="Once this quality is reached NzbDrone will no longer download episodes"/>
</span>
</div>
</div>
@@ -0,0 +1,22 @@
'use strict';
define(
[
'backbone.collectionview',
'Settings/Quality/Profile/Edit/EditQualityProfileItemView'
], function (BackboneSortableCollectionView, EditQualityProfileItemView) {
return BackboneSortableCollectionView.extend({
className: 'qualities',
modelView: EditQualityProfileItemView,
attributes: {
'validation-name': 'items'
},
events: {
'click li, td' : '_listItem_onMousedown',
'dblclick li, td' : '_listItem_onDoubleClick',
'keydown' : '_onKeydown'
}
});
});
@@ -1,60 +0,0 @@
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
{{#if id}}
<h3>Edit</h3>
{{else}}
<h3>Add</h3>
{{/if}}
</div>
<div class="modal-body">
<div class="form-horizontal">
<div class="control-group">
<label class="control-label">Name</label>
<div class="controls">
<input type="text" name="name">
</div>
</div>
<div class="control-group">
<label class="control-label">Cutoff</label>
<div class="controls">
<select class="x-cutoff" name="cutoff.id" validation-name="cutoff">
{{#each allowed}}
<option value="{{id}}">{{name}}</option>
{{/each}}
</select>
<span class="help-inline">
<i class="icon-nd-form-info" title="Once this quality is reached NzbDrone will no longer download episodes"/>
</span>
</div>
</div>
</div>
<div>
<div class="offset1 span3">
<h3>Available</h3>
<select multiple="multiple" class="x-available-list">
{{#each available}}
<option value="{{id}}">{{name}}</option>
{{/each}}
</select>
</div>
<div class="span3">
<div class="control-group">
<div class="controls">
<h3>Allowed</h3>
<select multiple="multiple" class="x-allowed-list" validation-name="allowed">
{{#each allowed}}
<option value="{{id}}">{{name}}</option>
{{/each}}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
{{#if id}}
<button class="btn btn-danger pull-left x-delete">delete</button>
{{/if}}
<button class="btn" data-dismiss="modal">cancel</button>
<button class="btn btn-primary x-save">save</button>
</div>
@@ -1,83 +0,0 @@
'use strict';
define(
[
'vent',
'marionette',
'backbone',
'Mixins/AsModelBoundView',
'Mixins/AsValidatedView',
'underscore'
], function (vent, Marionette, Backbone, AsModelBoundView, AsValidatedView, _) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Profile/EditQualityProfileTemplate',
ui: {
cutoff: '.x-cutoff'
},
events: {
'click .x-save' : '_saveQualityProfile',
'dblclick .x-available-list': '_moveQuality',
'dblclick .x-allowed-list' : '_moveQuality'
},
initialize: function (options) {
this.profileCollection = options.profileCollection;
},
_moveQuality: function (event) {
var quality;
var qualityId = event.target.value;
var availableCollection = new Backbone.Collection(this.model.get('available'));
availableCollection.comparator = function (model) {
return model.get('weight');
};
var allowedCollection = new Backbone.Collection(this.model.get('allowed'));
allowedCollection.comparator = function (model) {
return model.get('weight');
};
if (availableCollection.get(qualityId)) {
quality = availableCollection.get(qualityId);
availableCollection.remove(quality);
allowedCollection.add(quality);
}
else if (allowedCollection.get(qualityId)) {
quality = allowedCollection.get(qualityId);
allowedCollection.remove(quality);
availableCollection.add(quality);
}
else {
throw 'couldnt find quality id ' + qualityId;
}
this.model.set('available', availableCollection.toJSON());
this.model.set('allowed', allowedCollection.toJSON());
this.render();
},
_saveQualityProfile: function () {
var self = this;
var cutoff = _.findWhere(this.model.get('allowed'), { id: parseInt(this.ui.cutoff.val(), 10)});
this.model.set('cutoff', cutoff);
var promise = this.model.save();
if (promise) {
promise.done(function () {
self.profileCollection.add(self.model, { merge: true });
vent.trigger(vent.Commands.CloseModalCommand);
});
}
}
});
AsValidatedView.call(view);
return AsModelBoundView.call(view);
});
@@ -3,7 +3,7 @@
define(['AppLayout',
'marionette',
'Settings/Quality/Profile/QualityProfileView',
'Settings/Quality/Profile/EditQualityProfileView',
'Settings/Quality/Profile/Edit/EditQualityProfileLayout',
'Settings/Quality/Profile/QualityProfileSchemaCollection',
'underscore'
], function (AppLayout, Marionette, QualityProfileView, EditProfileView, ProfileCollection, _) {
@@ -4,7 +4,7 @@ define(
[
'AppLayout',
'marionette',
'Settings/Quality/Profile/EditQualityProfileView',
'Settings/Quality/Profile/Edit/EditQualityProfileLayout',
'Settings/Quality/Profile/DeleteView',
'Series/SeriesCollection',
'Mixins/AsModelBoundView',
@@ -13,7 +13,7 @@ define(
], function (AppLayout, Marionette, EditProfileView, DeleteProfileView, SeriesCollection, AsModelBoundView) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Profile/QualityProfileTemplate',
template: 'Settings/Quality/Profile/QualityProfileViewTemplate',
tagName : 'li',
ui: {
+8 -8
View File
@@ -5,27 +5,27 @@ define(
'marionette',
'Quality/QualityProfileCollection',
'Settings/Quality/Profile/QualityProfileCollectionView',
'Quality/QualitySizeCollection',
'Settings/Quality/Size/QualitySizeCollectionView'
], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualitySizeCollection, QualitySizeCollectionView) {
'Quality/QualityDefinitionCollection',
'Settings/Quality/Definition/QualityDefinitionCollectionView'
], function (Marionette, QualityProfileCollection, QualityProfileCollectionView, QualityDefinitionCollection, QualityDefinitionCollectionView) {
return Marionette.Layout.extend({
template: 'Settings/Quality/QualityLayoutTemplate',
regions: {
qualityProfile : '#quality-profile',
qualitySize : '#quality-size'
qualityProfile : '#quality-profile',
qualityDefinition : '#quality-definition'
},
initialize: function (options) {
this.settings = options.settings;
QualityProfileCollection.fetch();
this.qualitySizeCollection = new QualitySizeCollection();
this.qualitySizeCollection.fetch();
this.qualityDefinitionCollection = new QualityDefinitionCollection();
this.qualityDefinitionCollection.fetch();
},
onShow: function () {
this.qualityProfile.show(new QualityProfileCollectionView({collection: QualityProfileCollection}));
this.qualitySize.show(new QualitySizeCollectionView({collection: this.qualitySizeCollection}));
this.qualityDefinition.show(new QualityDefinitionCollectionView({collection: this.qualityDefinitionCollection}));
}
});
});
@@ -5,5 +5,5 @@
<br/>
<div class="row advanced-setting">
<div class="span12" id="quality-size"/>
<div class="span12" id="quality-definition"/>
</div>
@@ -1,4 +0,0 @@
<fieldset>
<legend>Quality Size Limits</legend>
<ul class="quality-sizes"/>
</fieldset>
@@ -1,9 +0,0 @@
'use strict';
define(['marionette', 'Settings/Quality/Size/QualitySizeView'], function (Marionette, QualitySizeView) {
return Marionette.CompositeView.extend({
itemView : QualitySizeView,
itemViewContainer: '.quality-sizes',
template : 'Settings/Quality/Size/QualitySizeCollectionTemplate'
});
});
@@ -1,20 +0,0 @@
<div class="quality-size-item">
<h3 class="center-block">{{name}}</h3>
<div class="size">
<div class="size-value-wrapper">
<div>
<span class="label label-large label-warning x-size-thirty"
name="thirtyMinuteSize"
title="Maximum size for a 30 minute episode">
</span>
</div>
<div>
<span class="label label-large label-info x-size-sixty"
name="sixtyMinuteSize"
title="Maximum size for a 60 minute episode">
</span>
</div>
</div>
<input type="text" name="maxSize" class="knob x-knob" />
</div>
</div>
@@ -1,61 +0,0 @@
'use strict';
define(
[
'marionette',
'Mixins/AsModelBoundView',
'filesize',
'jquery.knob'
], function (Marionette, AsModelBoundView, fileSize) {
var view = Marionette.ItemView.extend({
template: 'Settings/Quality/Size/QualitySizeTemplate',
tagName : 'li',
ui: {
knob : '.x-knob',
thirtyMinuteSize: '.x-size-thirty',
sixtyMinuteSize : '.x-size-sixty'
},
events: {
'change .x-knob': '_changeMaxSize'
},
initialize: function (options) {
this.qualityProfileCollection = options.qualityProfiles;
this.filesize = fileSize;
},
onRender: function () {
this.ui.knob.knob({
min : 0,
max : 200,
step : 1,
cursor : 25,
width : 150,
stopper : true,
displayInput: false
});
this._changeMaxSize();
},
_changeMaxSize: function () {
var maxSize = this.model.get('maxSize');
var bytes = maxSize * 1024 * 1024;
var thirty = fileSize(bytes * 30, 1, false);
var sixty = fileSize(bytes * 60, 1, false);
if (parseInt(maxSize, 10) === 0) {
thirty = 'No Limit';
sixty = 'No Limit';
}
this.ui.thirtyMinuteSize.html(thirty);
this.ui.sixtyMinuteSize.html(sixty);
}
});
return AsModelBoundView.call(view);
});
+118 -27
View File
@@ -1,6 +1,8 @@
@import "../../Shared/Styles/card";
@import "../../Content/Bootstrap/mixins";
@import "../../Content/FontAwesome/font-awesome";
.quality-profiles, .quality-sizes {
.quality-profiles {
li {
display: inline-block;
vertical-align: top;
@@ -35,41 +37,130 @@
}
}
.quality-size-item {
ul.qualities {
.user-select(none);
.card;
text-align: center;
min-height: 100px;
margin: 0;
padding: 0;
list-style-type: none;
outline: none;
width: 220px;
display: inline-block;
li {
margin: 2px;
padding: 2px 4px;
line-height: 20px;
border: 1px solid #aaaaaa;
border-radius: 4px; /* may need vendor varients */
background: #fafafa;
cursor: pointer;
width: 200px;
height: 210px;
padding: 10px 15px;
&.selected {
.select-handle {
opacity: 1.0;
cursor: pointer;
}
h3 {
margin-top: 0px;
}
.quality-label {
color: #444444;
}
.size {
position: relative;
height: 100px;
margin: 10px;
text-align: center;
}
.select-handle:before {
.icon(@check);
}
}
.knob {
box-shadow: none;
}
&:hover {
border-color: #888888;
background: #eeeeee;
.size-value-wrapper {
position: absolute;
top: 50px;
width: 100%;
.drag-handle {
opacity: 1.0;
cursor: move;
}
}
div {
margin-top: 2px;
.quality-label {
color: #c6c6c6;
}
.drag-handle, .select-handle {
opacity: 0.2;
line-height: 20px;
cursor: pointer;
}
.select-handle:before {
.icon(@check-empty);
display: inline-block;
width: 16px;
margin-top: 1px;
}
}
}
#quality-size {
overflow: hidden;
.qualities-controls {
.help-inline {
vertical-align: top;
margin-top: 5px;
}
}
#quality-definition-list {
.quality-header .row {
font-weight: bold;
line-height: 40px;
}
.rows .row {
line-height: 30px;
border-top: 1px solid #ddd;
vertical-align: middle;
padding: 5px;
input {
margin-bottom: 0px;
}
.size-label-wrapper {
line-height: 20px;
}
.label {
min-width: 70px;
text-align: center;
margin: 0px 1px;
padding: 1px 4px;
}
.ui-slider {
position: relative;
text-align: left;
background-color: #f5f5f5;
border-radius: 3px;
border: 1px solid #ccc;
height: 8px;
.ui-slider-range {
position: absolute;
display: block;
background-color: #ddd;
height: 100%;
}
.ui-slider-handle {
position: absolute;
z-index: 2;
width: 6px;
height: 12px;
cursor: default;
background-color: #ccc;
border: 1px solid #aaa;
border-radius: 3px;
top: -3px;
}
}
}
}
+8 -6
View File
@@ -1,6 +1,7 @@
'use strict';
define(
[
'jquery',
'vent',
'marionette',
'backbone',
@@ -17,7 +18,8 @@ define(
'Settings/General/GeneralView',
'Shared/LoadingView',
'Config'
], function (vent,
], function ($,
vent,
Marionette,
Backbone,
SettingsModel,
@@ -192,24 +194,24 @@ define(
},
_setAdvancedSettingsState: function () {
var checked = Config.getValueBoolean('advancedSettings');
var checked = Config.getValueBoolean(Config.Keys.AdvancedSettings);
this.ui.advancedSettings.prop('checked', checked);
if (checked) {
this.$el.addClass('show-advanced-settings');
$('body').addClass('show-advanced-settings');
}
},
_toggleAdvancedSettings: function () {
var checked = this.ui.advancedSettings.prop('checked');
Config.setValue('advancedSettings', checked);
Config.setValue(Config.Keys.AdvancedSettings, checked);
if (checked) {
this.$el.addClass('show-advanced-settings');
$('body').addClass('show-advanced-settings');
}
else {
this.$el.removeClass('show-advanced-settings');
$('body').removeClass('show-advanced-settings');
}
}
});