From 7798e8b591d90f1fded1ae78bebf29166043306d Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Fri, 4 Oct 2013 20:47:20 -0700 Subject: [PATCH] Full page searching for missing episodes New: Search for an entire page of missing episodes --- Gruntfile.js | 3 + .../IndexerSearch/EpisodeSearchCommand.cs | 5 +- .../IndexerSearch/EpisodeSearchService.cs | 9 +- src/UI/Cells/EpisodeActionsCell.js | 2 +- src/UI/Content/Backgrid/backgrid.less | 1 + src/UI/Content/Backgrid/selectall.css | 12 + src/UI/Episode/Search/EpisodeSearchLayout.js | 2 +- .../backbone.backgrid.selectall.js | 243 ++++++++++++++++++ src/UI/Missing/MissingLayout.js | 122 ++++++--- src/UI/Series/Edit/EditSeriesView.js | 2 +- ...plate.html => EditSeriesViewTemplate.html} | 0 src/UI/Shared/Grid/PagerTemplate.html | 6 +- src/UI/app.js | 26 +- 13 files changed, 383 insertions(+), 50 deletions(-) create mode 100644 src/UI/Content/Backgrid/selectall.css create mode 100644 src/UI/JsLibraries/backbone.backgrid.selectall.js rename src/UI/Series/Edit/{EditSeriesTemplate.html => EditSeriesViewTemplate.html} (100%) diff --git a/Gruntfile.js b/Gruntfile.js index 21dfd6b5a..1e8968fd3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -22,6 +22,9 @@ module.exports = function (grunt) { 'src/UI/JsLibraries/backbone.backgrid.paginator.js' : 'http://raw.github.com/wyuenho/backgrid/master/lib/extensions/paginator/backgrid-paginator.js', 'src/UI/JsLibraries/backbone.backgrid.filter.js' : 'http://raw.github.com/wyuenho/backgrid/master/lib/extensions/filter/backgrid-filter.js', + 'src/UI/JsLibraries/backbone.backgrid.selectall.js' : 'http://raw.github.com/wyuenho/backgrid-select-all/master/backgrid-select-all.js', + 'src/UI/Content/Backgrid/selectall.css' : 'http://raw.github.com/wyuenho/backgrid-select-all/master/backgrid-select-all.css', + 'src/UI/JsLibraries/backbone.validation.js' : 'https://raw.github.com/thedersen/backbone.validation/master/dist/backbone-validation.js', 'src/UI/JsLibraries/handlebars.runtime.js' : 'http://raw.github.com/wycats/handlebars.js/master/dist/handlebars.runtime.js', diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs index b69659b77..27861c869 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs @@ -1,10 +1,11 @@ -using NzbDrone.Core.Messaging.Commands; +using System.Collections.Generic; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.IndexerSearch { public class EpisodeSearchCommand : Command { - public int EpisodeId { get; set; } + public List EpisodeIds { get; set; } public override bool SendUpdatesToClient { diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs index 34fba629d..f365b1fd5 100644 --- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs @@ -22,10 +22,13 @@ namespace NzbDrone.Core.IndexerSearch public void Execute(EpisodeSearchCommand message) { - var decisions = _nzbSearchService.EpisodeSearch(message.EpisodeId); - var downloaded = _downloadApprovedReports.DownloadApproved(decisions); + foreach (var episodeId in message.EpisodeIds) + { + var decisions = _nzbSearchService.EpisodeSearch(episodeId); + var downloaded = _downloadApprovedReports.DownloadApproved(decisions); - _logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count); + _logger.ProgressInfo("Episode search completed. {0} reports downloaded.", downloaded.Count); + } } } } diff --git a/src/UI/Cells/EpisodeActionsCell.js b/src/UI/Cells/EpisodeActionsCell.js index 27bcd893e..229c52f06 100644 --- a/src/UI/Cells/EpisodeActionsCell.js +++ b/src/UI/Cells/EpisodeActionsCell.js @@ -43,7 +43,7 @@ define( _automaticSearch: function () { CommandController.Execute('episodeSearch', { name : 'episodeSearch', - episodeId: this.model.get('id') + episodeIds: [ this.model.get('id') ] }); }, diff --git a/src/UI/Content/Backgrid/backgrid.less b/src/UI/Content/Backgrid/backgrid.less index 969597665..3de45b21d 100644 --- a/src/UI/Content/Backgrid/backgrid.less +++ b/src/UI/Content/Backgrid/backgrid.less @@ -1,2 +1,3 @@ @import "filter"; @import "paginator"; +@import (css) "selectall.css"; \ No newline at end of file diff --git a/src/UI/Content/Backgrid/selectall.css b/src/UI/Content/Backgrid/selectall.css new file mode 100644 index 000000000..17471c1b7 --- /dev/null +++ b/src/UI/Content/Backgrid/selectall.css @@ -0,0 +1,12 @@ +/* + backgrid-select-all + http://github.com/wyuenho/backgrid + + Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors + Licensed under the MIT @license. +*/ + +.backgrid .select-row-cell, +.backgrid .select-all-header-cell { + text-align: center; +} \ No newline at end of file diff --git a/src/UI/Episode/Search/EpisodeSearchLayout.js b/src/UI/Episode/Search/EpisodeSearchLayout.js index 08d003e2c..e346dac44 100644 --- a/src/UI/Episode/Search/EpisodeSearchLayout.js +++ b/src/UI/Episode/Search/EpisodeSearchLayout.js @@ -44,7 +44,7 @@ define( } CommandController.Execute('episodeSearch', { - episodeId: this.model.get('id') + episodeIds: [ this.model.get('id') ] }); App.vent.trigger(App.Commands.CloseModalCommand); diff --git a/src/UI/JsLibraries/backbone.backgrid.selectall.js b/src/UI/JsLibraries/backbone.backgrid.selectall.js new file mode 100644 index 000000000..7d36c73ae --- /dev/null +++ b/src/UI/JsLibraries/backbone.backgrid.selectall.js @@ -0,0 +1,243 @@ +/* + backgrid-select-all + http://github.com/wyuenho/backgrid + + Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors + Licensed under the MIT @license. +*/ +(function (factory) { + + // CommonJS + if (typeof exports == "object") { + module.exports = factory(require("backbone"), require("backgrid")); + } + // Browser + else if (typeof Backbone !== "undefined" && typeof Backgrid !== "undefined") { + factory(Backbone, Backgrid); + } + +}(function (Backbone, Backgrid) { + + "use strict"; + + var $ = Backbone.$; + + /** + Renders a checkbox for row selection. + + @class Backgrid.Extension.SelectRowCell + @extends Backbone.View + */ + var SelectRowCell = Backgrid.Extension.SelectRowCell = Backbone.View.extend({ + + /** @property */ + className: "select-row-cell", + + /** @property */ + tagName: "td", + + /** @property */ + events: { + "keydown :checkbox": "onKeydown", + "change :checkbox": "onChange", + "click :checkbox": "enterEditMode" + }, + + /** + Initializer. If the underlying model triggers a `select` event, this cell + will change its checked value according to the event's `selected` value. + + @param {Object} options + @param {Backgrid.Column} options.column + @param {Backbone.Model} options.model + */ + initialize: function (options) { + + this.column = options.column; + if (!(this.column instanceof Backgrid.Column)) { + this.column = new Backgrid.Column(this.column); + } + + this.listenTo(this.model, "backgrid:select", function (model, selected) { + this.$el.find(":checkbox").prop("checked", selected).change(); + }); + + var column = this.column, $el = this.$el; + this.listenTo(column, "change:renderable", function (column, renderable) { + $el.toggleClass("renderable", renderable); + }); + + if (column.get("renderable")) $el.addClass("renderable"); + }, + + /** + Focuses the checkbox. + */ + enterEditMode: function () { + this.$el.find(":checkbox").focus(); + }, + + /** + Unfocuses the checkbox. + */ + exitEditMode: function () { + this.$el.find(":checkbox").blur(); + }, + + /** + Process keyboard navigation. + */ + onKeydown: function (e) { + var command = new Backgrid.Command(e); + if (command.passThru()) return true; // skip ahead to `change` + if (command.cancel()) { + e.stopPropagation(); + this.$el.find(":checkbox").blur(); + } + else if (command.save() || command.moveLeft() || command.moveRight() || + command.moveUp() || command.moveDown()) { + e.preventDefault(); + e.stopPropagation(); + this.model.trigger("backgrid:edited", this.model, this.column, command); + } + }, + + /** + When the checkbox's value changes, this method will trigger a Backbone + `backgrid:selected` event with a reference of the model and the + checkbox's `checked` value. + */ + onChange: function (e) { + var checked = $(e.target).prop('checked'); + this.$el.parent().toggleClass('selected', checked); + this.model.trigger("backgrid:selected", this.model, checked); + }, + + /** + Renders a checkbox in a table cell. + */ + render: function () { + this.$el.empty().append(''); + this.delegateEvents(); + return this; + } + + }); + + /** + Renders a checkbox to select all rows on the current page. + + @class Backgrid.Extension.SelectAllHeaderCell + @extends Backgrid.Extension.SelectRowCell + */ + var SelectAllHeaderCell = Backgrid.Extension.SelectAllHeaderCell = SelectRowCell.extend({ + + /** @property */ + className: "select-all-header-cell", + + /** @property */ + tagName: "th", + + /** + Initializer. When this cell's checkbox is checked, a Backbone + `backgrid:select` event will be triggered for each model for the current + page in the underlying collection. If a `SelectRowCell` instance exists + for the rows representing the models, they will check themselves. If any + of the SelectRowCell instances trigger a Backbone `backgrid:selected` + event with a `false` value, this cell will uncheck its checkbox. In the + event of a Backbone `backgrid:refresh` event, which is triggered when the + body refreshes its rows, which can happen under a number of conditions + such as paging or the columns were reset, this cell will still remember + the previously selected models and trigger a Backbone `backgrid:select` + event on them such that the SelectRowCells can recheck themselves upon + refreshing. + + @param {Object} options + @param {Backgrid.Column} options.column + @param {Backbone.Collection} options.collection + */ + initialize: function (options) { + + this.column = options.column; + if (!(this.column instanceof Backgrid.Column)) { + this.column = new Backgrid.Column(this.column); + } + + var collection = this.collection; + var selectedModels = this.selectedModels = {}; + this.listenTo(collection, "backgrid:selected", function (model, selected) { + if (selected) selectedModels[model.id || model.cid] = model; + else { + delete selectedModels[model.id || model.cid]; + this.$el.find(":checkbox").prop("checked", false); + } + }); + + this.listenTo(collection, "remove", function (model) { + delete selectedModels[model.id || model.cid]; + }); + + this.listenTo(collection, "backgrid:refresh", function () { + this.$el.find(":checkbox").prop("checked", false); + for (var i = 0; i < collection.length; i++) { + var model = collection.at(i); + if (selectedModels[model.id || model.cid]) { + model.trigger('backgrid:select', model, true); + } + } + }); + + var column = this.column, $el = this.$el; + this.listenTo(column, "change:renderable", function (column, renderable) { + $el.toggleClass("renderable", renderable); + }); + + if (column.get("renderable")) $el.addClass("renderable"); + }, + + /** + Progagates the checked value of this checkbox to all the models of the + underlying collection by triggering a Backbone `backgrid:select` event on + the models themselves, passing each model and the current `checked` value + of the checkbox in each event. + */ + onChange: function (e) { + var checked = $(e.target).prop("checked"); + + var collection = this.collection; + collection.each(function (model) { + model.trigger("backgrid:select", model, checked); + }); + } + + }); + + /** + Convenient method to retrieve a list of selected models. This method only + exists when the `SelectAll` extension has been included. + + @member Backgrid.Grid + @return {Array.} + */ + Backgrid.Grid.prototype.getSelectedModels = function () { + var selectAllHeaderCell; + var headerCells = this.header.row.cells; + for (var i = 0, l = headerCells.length; i < l; i++) { + var headerCell = headerCells[i]; + if (headerCell instanceof SelectAllHeaderCell) { + selectAllHeaderCell = headerCell; + break; + } + } + + var result = []; + if (selectAllHeaderCell) { + for (var modelId in selectAllHeaderCell.selectedModels) { + result.push(this.collection.get(modelId)); + } + } + + return result; + }; + +})); diff --git a/src/UI/Missing/MissingLayout.js b/src/UI/Missing/MissingLayout.js index 3b4ae5195..a6ecef90a 100644 --- a/src/UI/Missing/MissingLayout.js +++ b/src/UI/Missing/MissingLayout.js @@ -1,6 +1,7 @@ 'use strict'; define( [ + 'underscore', 'marionette', 'backgrid', 'Missing/Collection', @@ -10,8 +11,23 @@ define( 'Cells/RelativeDateCell', 'Shared/Grid/Pager', 'Shared/Toolbar/ToolbarLayout', - 'Shared/LoadingView' - ], function (Marionette, Backgrid, MissingCollection, SeriesTitleCell, EpisodeNumberCell, EpisodeTitleCell, RelativeDateCell, GridPager, ToolbarLayout, LoadingView) { + 'Shared/LoadingView', + 'Shared/Messenger', + 'Commands/CommandController', + 'backgrid.selectall' + ], function (_, + Marionette, + Backgrid, + MissingCollection, + SeriesTitleCell, + EpisodeNumberCell, + EpisodeTitleCell, + RelativeDateCell, + GridPager, + ToolbarLayout, + LoadingView, + Messenger, + CommandController) { return Marionette.Layout.extend({ template: 'Missing/MissingLayoutTemplate', @@ -21,8 +37,18 @@ define( pager : '#x-pager' }, + ui: { + searchSelectedButton: '.btn i.icon-search' + }, + columns: [ + { + name : '', + cell : 'select-row', + headerCell: 'select-all', + sortable : false + }, { name : 'series', label : 'Series Title', @@ -48,54 +74,88 @@ define( } ], - leftSideButtons: { - type : 'default', - storeState: false, - items : - [ - { - title : 'Season Pass', - icon : 'icon-bookmark', - route : 'seasonpass' - } - ] + initialize: function () { + this.collection = new MissingCollection(); + + this.listenTo(this.collection, 'sync', this._showTable); + }, + + onShow: function () { + this.missing.show(new LoadingView()); + this.collection.fetch(); + this._showToolbar(); }, _showTable: function () { - this.missing.show(new Backgrid.Grid({ + this.missingGrid = new Backgrid.Grid({ columns : this.columns, - collection: this.missingCollection, + collection: this.collection, className : 'table table-hover' - })); + }); + + this.missing.show(this.missingGrid); this.pager.show(new GridPager({ columns : this.columns, - collection: this.missingCollection + collection: this.collection })); }, + _showToolbar: function () { + var leftSideButtons = { + type : 'default', + storeState: false, + items : + [ + { + title: 'Search Selected', + icon : 'icon-search', + callback: this._searchSelected, + ownerContext: this + }, + { + title: 'Season Pass', + icon : 'icon-bookmark', + route: 'seasonpass' + } + ] + }; - initialize: function () { - this.missingCollection = new MissingCollection(); - - this.listenTo(this.missingCollection, 'sync', this._showTable); - }, - - - onShow: function () { - this.missing.show(new LoadingView()); - this.missingCollection.fetch(); - this._showToolbar(); - }, - _showToolbar: function () { this.toolbar.show(new ToolbarLayout({ left : [ - this.leftSideButtons + leftSideButtons ], context: this })); + + CommandController.bindToCommand({ + element: this.$('.x-toolbar-left-1 .btn i.icon-search'), + command: { + name: 'episodeSearch' + } + }); + }, + + _searchSelected: function () { + var selected = this.missingGrid.getSelectedModels(); + + if (selected.length === 0) { + Messenger.show({ + type: 'error', + message: 'No episodes selected' + }); + + return; + } + + var ids = _.pluck(selected, 'id'); + + CommandController.Execute('episodeSearch', { + name : 'episodeSearch', + episodeIds: ids + }); } }); }); diff --git a/src/UI/Series/Edit/EditSeriesView.js b/src/UI/Series/Edit/EditSeriesView.js index 43bc46fde..b01926c80 100644 --- a/src/UI/Series/Edit/EditSeriesView.js +++ b/src/UI/Series/Edit/EditSeriesView.js @@ -10,7 +10,7 @@ define( ], function (App, Marionette, QualityProfiles, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ - template: 'Series/Edit/EditSeriesTemplate', + template: 'Series/Edit/EditSeriesViewTemplate', ui: { qualityProfile: '.x-quality-profile', diff --git a/src/UI/Series/Edit/EditSeriesTemplate.html b/src/UI/Series/Edit/EditSeriesViewTemplate.html similarity index 100% rename from src/UI/Series/Edit/EditSeriesTemplate.html rename to src/UI/Series/Edit/EditSeriesViewTemplate.html diff --git a/src/UI/Shared/Grid/PagerTemplate.html b/src/UI/Shared/Grid/PagerTemplate.html index 74b8eadab..3e829409e 100644 --- a/src/UI/Shared/Grid/PagerTemplate.html +++ b/src/UI/Shared/Grid/PagerTemplate.html @@ -10,6 +10,6 @@ {{/each}} - - Total Records: {{Number state.totalRecords}} - \ No newline at end of file + + Total Records: {{Number state.totalRecords}} + \ No newline at end of file diff --git a/src/UI/app.js b/src/UI/app.js index 1834d9163..0d1df7b88 100644 --- a/src/UI/app.js +++ b/src/UI/app.js @@ -16,6 +16,7 @@ require.config({ 'backbone.modelbinder': 'JsLibraries/backbone.modelbinder', 'backgrid' : 'JsLibraries/backbone.backgrid', 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', + 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', 'fullcalendar' : 'JsLibraries/fullcalendar', 'backstrech' : 'JsLibraries/jquery.backstretch', '$' : 'JsLibraries/jquery', @@ -172,6 +173,15 @@ require.config({ exports: 'Backgrid.Extension.Paginator', + deps: + [ + 'backgrid' + ] + }, + 'backgrid.selectall': { + + exports: 'Backgrid.Extension.SelectAll', + deps: [ 'backgrid' @@ -197,13 +207,13 @@ define( }; app.Commands = { - EditSeriesCommand : 'EditSeriesCommand', - DeleteSeriesCommand: 'DeleteSeriesCommand', - CloseModalCommand : 'CloseModalCommand', - ShowEpisodeDetails : 'ShowEpisodeDetails', - ShowHistoryDetails : 'ShowHistryDetails', - SaveSettings : 'saveSettings', - ShowLogFile : 'showLogFile' + EditSeriesCommand : 'EditSeriesCommand', + DeleteSeriesCommand : 'DeleteSeriesCommand', + CloseModalCommand : 'CloseModalCommand', + ShowEpisodeDetails : 'ShowEpisodeDetails', + ShowHistoryDetails : 'ShowHistoryDetails', + SaveSettings : 'saveSettings', + ShowLogFile : 'showLogFile' }; app.Reqres = { @@ -214,7 +224,7 @@ define( console.log('starting application'); }); - app.addInitializer(SignalRBroadcaster.appInitializer, {app: app}); + app.addInitializer(SignalRBroadcaster.appInitializer, { app: app }); app.addRegions({ navbarRegion: '#nav-region',