diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less index 8bf71358e..bf90d7856 100644 --- a/src/UI/Content/theme.less +++ b/src/UI/Content/theme.less @@ -46,6 +46,21 @@ .page-toolbar { margin-top : 10px; margin-bottom : 30px; + + .toolbar-group { + display: inline-block; + } + + .sorting-buttons { + li { + a { + span { + display: inline-block; + width: 110px; + } + } + } + } } .page-container { diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js index 721c4c68a..2a957cc84 100644 --- a/src/UI/Mixins/AsPersistedStateCollection.js +++ b/src/UI/Mixins/AsPersistedStateCollection.js @@ -1,8 +1,8 @@ 'use strict'; define( - ['Config'], - function (Config) { + ['underscore', 'Config'], + function (_, Config) { return function () { @@ -22,7 +22,8 @@ define( _setInitialState.call(this); - this.on('backgrid:sort', _storeState, this); + this.on('backgrid:sort', _storeStateFromBackgrid, this); + this.on('drone:sort', _storeState, this); if (originalInit) { originalInit.call(this, options); @@ -38,9 +39,17 @@ define( this.state.order = order; }; - var _storeState = function (column, sortDirection) { + var _storeStateFromBackgrid = function (column, sortDirection) { var order = _convertDirectionToInt(sortDirection); - var sortKey = column.has('sortValue') ? column.get('sortValue') : column.get('name'); + var sortKey = column.has('sortValue') && _.isString(column.get('sortValue')) ? column.get('sortValue') : column.get('name'); + + Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); + Config.setValue('{0}.sortDirection'.format(this.tableName), order); + }; + + var _storeState = function (sortModel, sortDirection) { + var order = _convertDirectionToInt(sortDirection); + var sortKey = sortModel.get('name'); Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); Config.setValue('{0}.sortDirection'.format(this.tableName), order); diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js index 9a6390cb7..adbd0a84c 100644 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ b/src/UI/Series/Index/SeriesIndexLayout.js @@ -41,66 +41,65 @@ define( template: 'Series/Index/SeriesIndexLayoutTemplate', regions: { - seriesRegion: '#x-series', - toolbar : '#x-toolbar', - footer : '#x-series-footer' + seriesRegion : '#x-series', + toolbar : '#x-toolbar', + footer : '#x-series-footer' }, - columns: - [ - { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell - }, - { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue: 'this' - }, - { - name : 'seasonCount', - label: 'Seasons', - cell : 'integer' - }, - { - name : 'qualityProfileId', - label: 'Quality', - cell : QualityProfileCell - }, - { - name : 'network', - label: 'Network', - cell : 'string' - }, - { - name : 'nextAiring', - label : 'Next Airing', - cell : RelativeDateCell, - sortValue : function (model) { - var nextAiring = model.get('nextAiring'); + columns: [ + { + name : 'statusWeight', + label : '', + cell : SeriesStatusCell + }, + { + name : 'title', + label : 'Title', + cell : SeriesTitleCell, + cellValue: 'this' + }, + { + name : 'seasonCount', + label: 'Seasons', + cell : 'integer' + }, + { + name : 'qualityProfileId', + label: 'Quality', + cell : QualityProfileCell + }, + { + name : 'network', + label: 'Network', + cell : 'string' + }, + { + name : 'nextAiring', + label : 'Next Airing', + cell : RelativeDateCell, + sortValue : function (model) { + var nextAiring = model.get('nextAiring'); - if (!nextAiring) { - return Number.MAX_VALUE; - } - - return Moment(nextAiring).unix(); + if (!nextAiring) { + return Number.MAX_VALUE; } - }, - { - name : 'percentOfEpisodes', - label : 'Episodes', - cell : EpisodeProgressCell, - className: 'episode-progress-cell' - }, - { - name : 'this', - label : '', - sortable: false, - cell : SeriesActionsCell + + return Moment(nextAiring).unix(); } - ], + }, + { + name : 'percentOfEpisodes', + label : 'Episodes', + cell : EpisodeProgressCell, + className: 'episode-progress-cell' + }, + { + name : 'this', + label : '', + sortable: false, + cell : SeriesActionsCell + } + ], leftSideButtons: { type : 'default', @@ -138,25 +137,46 @@ define( ] }, - _showTable: function () { - this.currentView = new Backgrid.Grid({ - collection: SeriesCollection, - columns : this.columns, - className : 'table table-hover' - }); + sortingOptions: { + type : 'sorting', + storeState : false, + viewCollection: SeriesCollection, + items : + [ + { + title: 'Title', + name : 'title' + }, + { + title: 'Seasons', + name : 'seasonCount' + }, + { + title: 'Quality', + name : 'qualityProfileId' + }, + { + title: 'Network', + name : 'network' + }, + { + title : 'Next Airing', + name : 'nextAiring', + sortValue : function (model) { + var nextAiring = model.get('nextAiring'); - this._renderView(); - this._fetchCollection(); - }, + if (!nextAiring) { + return Number.MAX_VALUE; + } - _showList: function () { - this.currentView = new ListCollectionView(); - this._fetchCollection(); - }, - - _showPosters: function () { - this.currentView = new PosterCollectionView(); - this._fetchCollection(); + return Moment(nextAiring).unix(); + } + }, + { + title: 'Episodes', + name : 'percentOfEpisodes' + } + ] }, initialize: function () { @@ -164,39 +184,8 @@ define( this.listenTo(SeriesCollection, 'sync', this._renderView); this.listenTo(SeriesCollection, 'remove', this._renderView); - }, - _renderView: function () { - - if (SeriesCollection.length === 0) { - this.seriesRegion.show(new EmptyView()); - this.toolbar.close(); - } - else { - this.currentView.collection = SeriesCollection; - this.seriesRegion.show(this.currentView); - - this._showToolbar(); - this._showFooter(); - } - }, - - onShow: function () { - this._showToolbar(); - this._renderView(); - }, - - _fetchCollection: function () { - SeriesCollection.fetch(); - }, - - _showToolbar: function () { - - if (this.toolbar.currentView) { - return; - } - - var viewButtons = { + this.viewButtons = { type : 'radio', storeState : true, menuKey : 'seriesViewMode', @@ -226,12 +215,71 @@ define( } ] }; + }, + + _showTable: function () { + this.currentView = new Backgrid.Grid({ + collection: SeriesCollection, + columns : this.columns, + className : 'table table-hover' + }); + + this._fetchCollection(); + }, + + _showList: function () { + this.currentView = new ListCollectionView({ collection: SeriesCollection }); + + this._fetchCollection(); + }, + + _showPosters: function () { + this.currentView = new PosterCollectionView({ collection: SeriesCollection }); + + this._fetchCollection(); + }, + + _renderView: function () { + + if (SeriesCollection.length === 0) { + this.seriesRegion.show(new EmptyView()); + this.toolbar.close(); + } + else { + this.seriesRegion.show(this.currentView); + + this._showToolbar(); + this._showFooter(); + } + }, + + onShow: function () { + this._showToolbar(); + this._renderView(); + }, + + _fetchCollection: function () { + SeriesCollection.fetch(); + }, + + _showToolbar: function () { + + if (this.toolbar.currentView) { + return; + } + + var rightButtons = [ + this.viewButtons + ]; + + if (this.showSortingButton) { + rightButtons.splice(0, 0, this.sortingOptions); + } + + rightButtons.splice(0, 0, this.sortingOptions); this.toolbar.show(new ToolbarLayout({ - right : - [ - viewButtons - ], + right : rightButtons, left : [ this.leftSideButtons diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js index 74c16f27d..2c7b049f8 100644 --- a/src/UI/Series/SeriesCollection.js +++ b/src/UI/Series/SeriesCollection.js @@ -8,7 +8,7 @@ define( 'api!series', 'Mixins/AsPersistedStateCollection' ], function (_, Backbone, PageableCollection, SeriesModel, SeriesData, AsPersistedStateCollection) { - var Collection = Backbone.Collection.extend({ + var Collection = PageableCollection.extend({ url : window.NzbDrone.ApiRoot + '/series', model: SeriesModel, tableName: 'series', diff --git a/src/UI/Shared/Grid/HeaderCell.js b/src/UI/Shared/Grid/HeaderCell.js index 2f1f3c429..45b402589 100644 --- a/src/UI/Shared/Grid/HeaderCell.js +++ b/src/UI/Shared/Grid/HeaderCell.js @@ -11,6 +11,14 @@ define( 'click': 'onClick' }, + _originalInit: Backgrid.HeaderCell.prototype.initialize, + + initialize: function (options) { + this._originalInit.call(this, options); + + this.listenTo(this.collection, 'drone:sort', this.render); + }, + render: function () { this.$el.empty(); this.$el.append(this.column.get('label')); @@ -37,6 +45,10 @@ define( if (key === this.column.get('name')) { this._setSortIcon(order); } + + else { + this._removeSortIcon(); + } } return this; diff --git a/src/UI/Shared/Toolbar/ButtonModel.js b/src/UI/Shared/Toolbar/ButtonModel.js index f6c7c8a64..b7bd6d4dc 100644 --- a/src/UI/Shared/Toolbar/ButtonModel.js +++ b/src/UI/Shared/Toolbar/ButtonModel.js @@ -1,13 +1,29 @@ 'use strict'; define( [ + 'underscore', 'backbone' - ], function (Backbone) { + ], function (_, Backbone) { return Backbone.Model.extend({ defaults: { 'target' : '/nzbdrone/route', 'title' : '', 'active' : false, - 'tooltip': undefined } + 'tooltip': undefined + }, + + sortValue: function () { + var sortValue = this.get('sortValue'); + if (_.isString(sortValue)) { + return this[sortValue]; + } + else if (_.isFunction(sortValue)) { + return sortValue; + } + + return function (model, colName) { + return model.get(colName); + }; + } }); }); diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js index 3f5be2a6f..1fb788250 100644 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js @@ -13,7 +13,6 @@ define( 'click': 'onClick' }, - initialize: function () { this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); @@ -53,7 +52,6 @@ define( callback.call(this.model.ownerContext); } } - }); }); diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js new file mode 100644 index 000000000..a79abc2f0 --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js @@ -0,0 +1,87 @@ +'use strict'; +define( + [ + 'backbone.pageable', + 'marionette', + 'Shared/Toolbar/Sorting/SortingButtonView' + ], function (PageableCollection, Marionette, ButtonView) { + return Marionette.CompositeView.extend({ + itemView : ButtonView, + template : 'Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate', + itemViewContainer: '.dropdown-menu', + + initialize: function (options) { + this.viewCollection = options.viewCollection; + this.listenTo(this.viewCollection, 'drone:sort', this.sort); + }, + + itemViewOptions: function () { + return { + viewCollection: this.viewCollection + }; + }, + + sort: function (sortModel, sortDirection) { + var collection = this.viewCollection; + + var order; + if (sortDirection === 'ascending') { + order = -1; + } + else if (sortDirection === 'descending') { + order = 1; + } + else { + order = null; + } + + var comparator = this.makeComparator(sortModel.get('name'), order, + order ? + sortModel.sortValue() : + function (model) { + return model.cid; + }); + + if (PageableCollection && + collection instanceof PageableCollection) { + + collection.setSorting(order && sortModel.get('name'), order, + {sortValue: sortModel.sortValue()}); + + if (collection.mode === 'client') { + if (collection.fullCollection.comparator === null) { + collection.fullCollection.comparator = comparator; + } + collection.fullCollection.sort(); + } + else { + collection.fetch({reset: true}); + } + } + else { + collection.comparator = comparator; + collection.sort(); + } + + return this; + }, + + makeComparator: function (attr, order, func) { + + return function (left, right) { + // extract the values from the models + var l = func(left, attr), r = func(right, attr), t; + + // if descending order, swap left and right + if (order === 1) t = l, l = r, r = t; + + // compare as usual + if (l === r) return 0; + else if (l < r) return -1; + return 1; + }; + } + }); + }); + + diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html new file mode 100644 index 000000000..62f6da91e --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.html @@ -0,0 +1,8 @@ +
+ + Sort + + +
diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js new file mode 100644 index 000000000..7421e628f --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js @@ -0,0 +1,84 @@ +'use strict'; +define( + [ + 'backbone', + 'marionette', + 'underscore' + ], function (Backbone, Marionette, _) { + + return Marionette.ItemView.extend({ + template : 'Shared/Toolbar/Sorting/SortingButtonViewTemplate', + tagName : 'li', + + ui: { + icon: 'i' + }, + + events: { + 'click': 'onClick' + }, + + initialize: function (options) { + this.viewCollection = options.viewCollection; + this.listenTo(this.viewCollection, 'drone:sort', this.render); + this.listenTo(this.viewCollection, 'backgrid:sort', this.render); + }, + + onRender: function () { + if (this.viewCollection.state) { + var key = this.viewCollection.state.sortKey; + var order = this.viewCollection.state.order; + + if (key === this.model.get('name')) { + this._setSortIcon(order); + } + + else { + this._removeSortIcon(); + } + } + }, + + onClick: function (e) { + e.preventDefault(); + + var collection = this.viewCollection; + var event = 'drone:sort'; + + collection.state.sortKey = this.model.get('name'); + var direction = collection.state.order; + + if (direction === 'ascending' || direction === -1) + { + collection.state.order = 'descending'; + collection.trigger(event, this.model, 'descending'); + } + else + { + collection.state.order = 'ascending'; + collection.trigger(event, this.model, 'ascending'); + } + }, + + _convertDirectionToIcon: function (dir) { + if (dir === 'ascending' || dir === -1) { + return 'icon-sort-up'; + } + + return 'icon-sort-down'; + }, + + _setSortIcon: function (dir) { + this._removeSortIcon(); + this.ui.icon.addClass(this._convertDirectionToIcon(dir)); + }, + + _removeSortIcon: function () { + this.ui.icon.removeClass('icon-sort-up icon-sort-down'); + } + }); + }); + + + + diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html b/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html new file mode 100644 index 000000000..a969d5dc2 --- /dev/null +++ b/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.html @@ -0,0 +1,4 @@ + + {{title}} + + \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js index 7f4b64bf9..8a907dad6 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ b/src/UI/Shared/Toolbar/ToolbarLayout.js @@ -6,8 +6,9 @@ define( 'Shared/Toolbar/ButtonModel', 'Shared/Toolbar/Radio/RadioButtonCollectionView', 'Shared/Toolbar/Button/ButtonCollectionView', + 'Shared/Toolbar/Sorting/SortingButtonCollectionView', 'underscore' - ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView,_) { + ], function (Marionette, ButtonCollection, ButtonModel, RadioButtonCollectionView, ButtonCollectionView, SortingButtonCollectionView, _) { return Marionette.Layout.extend({ template: 'Shared/Toolbar/ToolbarLayoutTemplate', @@ -78,6 +79,15 @@ define( }); break; } + case 'sorting': + { + buttonGroupView = new SortingButtonCollectionView({ + collection : groupCollection, + menu : buttonGroup, + viewCollection: buttonGroup.viewCollection + }); + break; + } default : { buttonGroupView = new ButtonCollectionView({ diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html index c2f497b3f..b4cd4dcda 100644 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html +++ b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.html @@ -1,8 +1,8 @@ 
-
-
+
+
-
-
+
+