From 7b4c75f0dd755e28fcffc0e4fdd05452458a3b09 Mon Sep 17 00:00:00 2001 From: RaNaN Date: Sun, 24 Mar 2013 21:27:43 +0100 Subject: added view type for input fields --- module/AccountManager.py | 4 +- module/plugins/addons/MultiHoster.py | 4 +- module/remote/apitypes.py | 5 +- module/remote/apitypes_debug.py | 2 +- module/remote/pyload.thrift | 1 - module/remote/wsbackend/AsyncHandler.py | 1 + module/web/static/js/default.js | 2 +- module/web/static/js/views/abstract/modalView.js | 79 +++++----- .../web/static/js/views/dashboard/dashboardView.js | 164 +++++++++++++++++++++ module/web/static/js/views/dashboard/fileView.js | 97 ++++++++++++ module/web/static/js/views/dashboard/filterView.js | 133 +++++++++++++++++ .../web/static/js/views/dashboard/packageView.js | 74 ++++++++++ .../web/static/js/views/dashboard/selectionView.js | 150 +++++++++++++++++++ module/web/static/js/views/dashboardView.js | 164 --------------------- module/web/static/js/views/fileView.js | 97 ------------ module/web/static/js/views/filterView.js | 133 ----------------- module/web/static/js/views/input/inputLoader.js | 7 + module/web/static/js/views/input/inputView.js | 55 +++++++ module/web/static/js/views/input/textInput.js | 33 +++++ module/web/static/js/views/packageView.js | 74 ---------- module/web/static/js/views/queryModal.js | 35 +++-- module/web/static/js/views/selectionView.js | 150 ------------------- .../templates/default/backbone/queryDialog.html | 14 +- 23 files changed, 799 insertions(+), 679 deletions(-) create mode 100644 module/web/static/js/views/dashboard/dashboardView.js create mode 100644 module/web/static/js/views/dashboard/fileView.js create mode 100644 module/web/static/js/views/dashboard/filterView.js create mode 100644 module/web/static/js/views/dashboard/packageView.js create mode 100644 module/web/static/js/views/dashboard/selectionView.js delete mode 100644 module/web/static/js/views/dashboardView.js delete mode 100644 module/web/static/js/views/fileView.js delete mode 100644 module/web/static/js/views/filterView.js create mode 100644 module/web/static/js/views/input/inputLoader.js create mode 100644 module/web/static/js/views/input/inputView.js create mode 100644 module/web/static/js/views/input/textInput.js delete mode 100644 module/web/static/js/views/packageView.js delete mode 100644 module/web/static/js/views/selectionView.js (limited to 'module') diff --git a/module/AccountManager.py b/module/AccountManager.py index 45b4eef95..d90c957c3 100644 --- a/module/AccountManager.py +++ b/module/AccountManager.py @@ -100,7 +100,7 @@ class AccountManager: if plugin in self.accounts and user in self.accounts[plugin]: del self.accounts[plugin][user] self.core.db.removeAccount(plugin, user) - self.core.eventManager.dispatchEvent("accountDeleted", plugin, user) + self.core.eventManager.dispatchEvent("account:deleted", plugin, user) else: self.core.log.debug("Remove non existing account %s %s" % (plugin, user)) @@ -137,4 +137,4 @@ class AccountManager: acc.getAccountInfo(True) def sendChange(self, plugin, name): - self.core.eventManager.dispatchEvent("accountUpdated", plugin, name) \ No newline at end of file + self.core.eventManager.dispatchEvent("account:updated", plugin, name) \ No newline at end of file diff --git a/module/plugins/addons/MultiHoster.py b/module/plugins/addons/MultiHoster.py index 05d25b958..825085df8 100644 --- a/module/plugins/addons/MultiHoster.py +++ b/module/plugins/addons/MultiHoster.py @@ -66,7 +66,7 @@ class MultiHoster(Addon): - @AddEventListener("accountDeleted") + @AddEventListener("account:deleted") def refreshAccounts(self, plugin=None, user=None): self.plugins = {} @@ -75,7 +75,7 @@ class MultiHoster(Addon): if isinstance(account, MultiHosterAccount) and account.isUsable(): self.addHoster(account) - @AddEventListener("accountUpdated") + @AddEventListener("account:updated") def refreshAccount(self, plugin, user): account = self.core.accountManager.getAccount(plugin, user) diff --git a/module/remote/apitypes.py b/module/remote/apitypes.py index 83eb19450..e81c960c8 100644 --- a/module/remote/apitypes.py +++ b/module/remote/apitypes.py @@ -127,16 +127,15 @@ class AddonService(BaseObject): self.media = media class ConfigHolder(BaseObject): - __slots__ = ['name', 'label', 'description', 'long_description', 'items', 'info', 'handler'] + __slots__ = ['name', 'label', 'description', 'long_description', 'items', 'info'] - def __init__(self, name=None, label=None, description=None, long_description=None, items=None, info=None, handler=None): + def __init__(self, name=None, label=None, description=None, long_description=None, items=None, info=None): self.name = name self.label = label self.description = description self.long_description = long_description self.items = items self.info = info - self.handler = handler class ConfigInfo(BaseObject): __slots__ = ['name', 'label', 'description', 'category', 'user_context', 'activated'] diff --git a/module/remote/apitypes_debug.py b/module/remote/apitypes_debug.py index 6909464d4..7b1b5e7f3 100644 --- a/module/remote/apitypes_debug.py +++ b/module/remote/apitypes_debug.py @@ -21,7 +21,7 @@ classes = { 'AccountInfo' : [basestring, basestring, int, bool, int, int, int, bool, bool, bool, (dict, basestring, basestring)], 'AddonInfo' : [basestring, basestring, basestring], 'AddonService' : [basestring, basestring, (list, basestring), (None, int)], - 'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo)), (None, (list, InteractionTask))], + 'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo))], 'ConfigInfo' : [basestring, basestring, basestring, basestring, bool, (None, bool)], 'ConfigItem' : [basestring, basestring, basestring, Input, basestring, basestring], 'DownloadInfo' : [basestring, basestring, basestring, int, basestring, basestring], diff --git a/module/remote/pyload.thrift b/module/remote/pyload.thrift index 76e755de0..2aeb54091 100644 --- a/module/remote/pyload.thrift +++ b/module/remote/pyload.thrift @@ -250,7 +250,6 @@ struct ConfigHolder { 4: string long_description, 5: list items, 6: optional list info, - 7: optional list handler, // if null plugin is not loaded } struct ConfigInfo { diff --git a/module/remote/wsbackend/AsyncHandler.py b/module/remote/wsbackend/AsyncHandler.py index b40f0ea4e..d9e302fbb 100644 --- a/module/remote/wsbackend/AsyncHandler.py +++ b/module/remote/wsbackend/AsyncHandler.py @@ -80,6 +80,7 @@ class AsyncHandler(AbstractHandler): for req in self.clients: # filter events that these user is no owner of # TODO: events are security critical, this should be revised later + # TODO: permissions? interaction etc if not req.api.user.isAdmin(): skip = False for arg in args: diff --git a/module/web/static/js/default.js b/module/web/static/js/default.js index afe624ff9..44c8842fa 100644 --- a/module/web/static/js/default.js +++ b/module/web/static/js/default.js @@ -1,4 +1,4 @@ -define('default', ['require', 'jquery', 'app', 'views/headerView', 'views/dashboardView'], +define('default', ['require', 'jquery', 'app', 'views/headerView', 'views/dashboard/dashboardView'], function(require, $, App, HeaderView, DashboardView) { App.init = function() { diff --git a/module/web/static/js/views/abstract/modalView.js b/module/web/static/js/views/abstract/modalView.js index d3ac34bd6..1e45e942b 100644 --- a/module/web/static/js/views/abstract/modalView.js +++ b/module/web/static/js/views/abstract/modalView.js @@ -31,47 +31,57 @@ define(['jquery', 'backbone', 'underscore', 'omniwindow'], function($, Backbone, }, + // TODO: whole modal stuff is not very elegant render: function() { this.$el.html(this.template(this.renderContent())); - this.$el.addClass('modal hide'); - this.$el.css({opacity: 0, scale: 0.7}); - $("body").append(this.el); - - var self = this; - - this.dialog = this.$el.omniWindow({ - overlay: { - selector: '#modal-overlay', - hideClass: 'hide', - animations: { - hide: function(subjects, internalCallback) { - subjects.overlay.transition({opacity: 'hide', delay: 100}, 300, function() { + this.onRender(); + + if (this.dialog === null) { + this.$el.addClass('modal hide'); + this.$el.css({opacity: 0, scale: 0.7}); + + var self = this; + $("body").append(this.el); + this.dialog = this.$el.omniWindow({ + overlay: { + selector: '#modal-overlay', + hideClass: 'hide', + animations: { + hide: function(subjects, internalCallback) { + subjects.overlay.transition({opacity: 'hide', delay: 100}, 300, function() { + internalCallback(subjects); + self.onHide(); + if (self.onHideDestroy) + self.destroy(); + }); + }, + show: function(subjects, internalCallback) { + subjects.overlay.fadeIn(300); internalCallback(subjects); - if (self.onHideDestroy) - self.destroy(); - }); - }, - show: function(subjects, internalCallback) { - subjects.overlay.fadeIn(300); - internalCallback(subjects); - }}}, - modal: { - hideClass: 'hide', - animations: { - hide: function(subjects, internalCallback) { - subjects.modal.transition({opacity: 'hide', scale: 0.7}, 300); - internalCallback(subjects); - }, - - show: function(subjects, internalCallback) { - subjects.modal.transition({opacity: 'show', scale: 1, delay: 100}, 300, function() { + }}}, + modal: { + hideClass: 'hide', + animations: { + hide: function(subjects, internalCallback) { + subjects.modal.transition({opacity: 'hide', scale: 0.7}, 300); internalCallback(subjects); - }); - }} - }}); + }, + + show: function(subjects, internalCallback) { + subjects.modal.transition({opacity: 'show', scale: 1, delay: 100}, 300, function() { + internalCallback(subjects); + }); + }} + }}); + } return this; }, + + onRender: function() { + + }, + renderContent: function() { return {content: $('

Content!

').html()}; }, @@ -91,7 +101,6 @@ define(['jquery', 'backbone', 'underscore', 'omniwindow'], function($, Backbone, hide: function() { this.dialog.trigger('hide'); - this.onHide(); }, onHide: function() { diff --git a/module/web/static/js/views/dashboard/dashboardView.js b/module/web/static/js/views/dashboard/dashboardView.js new file mode 100644 index 000000000..c888214df --- /dev/null +++ b/module/web/static/js/views/dashboard/dashboardView.js @@ -0,0 +1,164 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', + './packageView', './fileView', './selectionView', './filterView', 'select2'], + function($, Backbone, _, App, TreeCollection, packageView, fileView, selectionView, filterView) { + + // Renders whole dashboard + return Backbone.View.extend({ + + el: '#content', + active: $('.breadcrumb .active'), + + events: { + }, + + //
    holding the packages + packageUL: null, + //
      displaying the files + fileUL: null, + // Package tree + tree: null, + // Current open files + files: null, + // True when loading animation is running + isLoading: false, + + initialize: function() { + var self = this; + this.tree = new TreeCollection(); + + var view = new selectionView(); + view = new filterView(); + + // When package is added we reload the data + App.vent.on('package:added', function() { + console.log('Package tree caught, package:added event'); + self.tree.fetch(); + }); + + App.vent.on('file:updated', _.bind(this.fileUpdated, this)); + + // TODO: file:added + // TODO: package:deleted + // TODO: package:updated + }, + + init: function() { + var self = this; + // TODO: put in separated function + // TODO: order of elements? + // Init the tree and callback for package added + this.tree.fetch({success: function() { + self.render(); + self.tree.get('packages').on('add', function(pack) { + console.log('Package ' + pack.get('pid') + ' added to tree'); + self.appendPackage(pack, 0, true); + self.openPackage(pack); + }); + }}); + + this.$('.input').select2({tags: ["a", "b", "sdf"]}); + }, + + render: function() { + console.log('Render package list'); + var packs = this.tree.get('packages'); + this.files = this.tree.get('files'); + + this.packageUL = this.$('.package-list'); + packs.each(_.bind(this.appendPackage, this)); + + this.fileUL = this.$('.file-list'); + if (this.files.length === 0) { + // no files are displayed + this.files = null; + // Open the first package + if (packs.length >= 1) + this.openPackage(packs.at(0)); + } + else + this.files.each(_.bind(this.appendFile, this)); + + return this; + }, + + // TODO sorting ?! + // Append a package to the list, index, animate it + appendPackage: function(pack, i, animation) { + var el = new packageView({model: pack}).render().el; + this.packageUL.appendWithAnimation(el, animation); + }, + + appendFile: function(file, i, animation) { + var el = new fileView({model: file}).render().el; + this.fileUL.appendWithAnimation(el, animation); + }, + + // Show content of the packages on main view + openPackage: function(pack) { + var self = this; + + // load animation only when something is shown and its different from current package + if (this.files && this.files !== pack.get('files')) + self.loading(); + + pack.fetch({silent: true, success: function() { + console.log('Package ' + pack.get('pid') + ' loaded'); + self.active.text(pack.get('name')); + self.contentReady(pack.get('files')); + }, failure: function() { + self.failure(); + }}); + + }, + + contentReady: function(files) { + var old_files = this.files; + this.files = files; + App.vent.trigger('dashboard:contentReady'); + + // show the files when no loading animation is running and not already open + if (!this.isLoading && old_files !== files) + this.show(); + }, + + // Do load animation, remove the old stuff + loading: function() { + this.isLoading = true; + this.files = null; + var self = this; + this.fileUL.fadeOut({complete: function() { + // All file views should vanish + App.vent.trigger('dashboard:destroyContent'); + + // Loading was faster than animation + if (self.files) + self.show(); + + self.isLoading = false; + }}); + }, + + failure: function() { + // TODO + }, + + show: function() { + // fileUL has to be resetted before + this.files.each(_.bind(this.appendFile, this)); + //TODO: show placeholder when nothing is displayed (filtered content empty) + this.fileUL.fadeIn(); + App.vent.trigger('dashboard:updated'); + }, + + // Refresh the file if it is currently shown + fileUpdated: function(data) { + // this works with ids and object + var file = this.files.get(data); + if (file) + if (_.isObject(data)) // update directly + file.set(data); + else // fetch from server + file.fetch(); + } + }); + }); \ No newline at end of file diff --git a/module/web/static/js/views/dashboard/fileView.js b/module/web/static/js/views/dashboard/fileView.js new file mode 100644 index 000000000..c673041b5 --- /dev/null +++ b/module/web/static/js/views/dashboard/fileView.js @@ -0,0 +1,97 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', 'helpers/formatTime'], + function($, Backbone, _, App, Api, ItemView, formatTime) { + + // Renders single file item + return ItemView.extend({ + + tagName: 'li', + className: 'file-view row-fluid', + template: _.compile($("#template-file").html()), + events: { + 'click .checkbox': 'select', + 'click .btn-delete': 'deleteItem', + 'click .btn-restart': 'restart' + }, + + initialize: function() { + this.listenTo(this.model, 'change', this.render); + // This will be triggered manually and changed before with silent=true + this.listenTo(this.model, 'change:visible', this.visibility_changed); + this.listenTo(this.model, 'change:progress', this.progress_changed); + this.listenTo(this.model, 'remove', this.unrender); + this.listenTo(App.vent, 'dashboard:destroyContent', this.destroy); + }, + + onDestroy: function() { + }, + + render: function() { + var data = this.model.toJSON(); + if (data.download) { + var status = data.download.status; + if (status === Api.DownloadStatus.Offline || status === Api.DownloadStatus.TempOffline) + data.offline = true; + else if (status === Api.DownloadStatus.Online) + data.online = true; + else if (status === Api.DownloadStatus.Waiting) + data.waiting = true; + else if (status === Api.DownloadStatus.Downloading) + data.downloading = true; + else if (this.model.isFailed()) + data.failed = true; + else if (this.model.isFinished()) + data.finished = true; + } + + this.$el.html(this.template(data)); + if (this.model.get('selected')) + this.$el.addClass('ui-selected'); + else + this.$el.removeClass('ui-selected'); + + if (this.model.get('visible')) + this.$el.show(); + else + this.$el.hide(); + + return this; + }, + + select: function(e) { + e.preventDefault(); + var checked = this.$el.hasClass('ui-selected'); + // toggle class immediately, so no re-render needed + this.model.set('selected', !checked, {silent: true}); + this.$el.toggleClass('ui-selected'); + App.vent.trigger('file:selection'); + }, + + visibility_changed: function(visible) { + // TODO: improve animation, height is not available when element was not visible + if (visible) + this.$el.slideOut(true); + else { + this.$el.calculateHeight(true); + this.$el.slideIn(true); + } + }, + + progress_changed: function() { + if(!this.model.isDownload()) + return; + + if (this.model.get('download').status === Api.DownloadStatus.Downloading) { + var bar = this.$('.progress .bar'); + if (!bar) { // ensure that the dl bar is rendered + this.render(); + bar = this.$('.progress .bar'); + } + + bar.width(this.model.get('progress') + '%'); + bar.html('  ' + formatTime(this.model.get('eta'))); + } else // Every else state can be renderred normally + this.render(); + + } + }); + }); \ No newline at end of file diff --git a/module/web/static/js/views/dashboard/filterView.js b/module/web/static/js/views/dashboard/filterView.js new file mode 100644 index 000000000..14968f2cc --- /dev/null +++ b/module/web/static/js/views/dashboard/filterView.js @@ -0,0 +1,133 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'models/Package'], + function($, Backbone, _, App, Api, Package) { + + // Modified version of type ahead show, nearly the same without absolute positioning + function show() { + this.$menu + .insertAfter(this.$element) + .show(); + + this.shown = true; + return this; + } + + // Renders the actionbar for the dashboard, handles everything related to filtering displayed files + return Backbone.View.extend({ + el: 'ul.actionbar', + + events: { + 'click .filter-type': 'filter_type', + 'click .filter-state': 'switch_filter', + 'submit .form-search': 'search' + }, + + state: null, + stateMenu: null, + + initialize: function() { + + // use our modified method + $.fn.typeahead.Constructor.prototype.show = show; + this.$('.search-query').typeahead({ + minLength: 2, + source: this.getSuggestions + }); + + this.stateMenu = this.$('.dropdown-toggle .state'); + this.state = Api.DownloadState.All; + + // Apply the filter before the content is shown + App.vent.on('dashboard:contentReady', _.bind(this.apply_filter, this)); + }, + + render: function() { + return this; + }, + + // TODO: app level api request + + search: function(e) { + e.stopPropagation(); + var input = this.$('.search-query'); + var query = input.val(); + input.val(''); + + var pack = new Package(); + // Overwrite fetch method to use a search + // TODO: quite hackish, could be improved to filter packages + // or show performed search + pack.fetch = function(options) { + pack.search(query, options); + }; + + App.dashboard.openPackage(pack); + }, + + getSuggestions: function(query, callback) { + $.ajax('/api/searchSuggestions', { + method: 'POST', + data: {pattern: JSON.stringify(query)}, + success: function(data) { + callback(data); + } + }); + }, + + switch_filter: function(e) { + e.stopPropagation(); + var element = $(e.target); + var state = parseInt(element.data('state'), 10); + var menu = this.stateMenu.parent().parent(); + menu.removeClass('open'); + + if (state === Api.DownloadState.Finished) { + menu.removeClass().addClass('dropdown finished'); + } else if (state === Api.DownloadState.Unfinished) { + menu.removeClass().addClass('dropdown active'); + } else if (state === Api.DownloadState.Failed) { + menu.removeClass().addClass('dropdown failed'); + } else { + menu.removeClass().addClass('dropdown'); + } + + this.state = state; + this.stateMenu.text(element.text()); + this.apply_filter(); + }, + + // Applies the filtering to current open files + apply_filter: function() { + if (!App.dashboard.files) + return; + + var self = this; + App.dashboard.files.map(function(file) { + var visible = file.get('visible'); + if (visible !== self.is_visible(file)) { + file.set('visible', !visible, {silent: true}); + file.trigger('change:visible', !visible); + } + }); + + App.vent.trigger('dashboard:filtered'); + }, + + // determine if a file should be visible + // TODO: non download files + is_visible: function(file) { + if (this.state === Api.DownloadState.Finished) + return file.isFinished(); + else if (this.state === Api.DownloadState.Unfinished) + return file.isUnfinished(); + else if (this.state === Api.DownloadState.Failed) + return file.isFailed(); + + return true; + }, + + filter_type: function(e) { + + } + + }); + }); \ No newline at end of file diff --git a/module/web/static/js/views/dashboard/packageView.js b/module/web/static/js/views/dashboard/packageView.js new file mode 100644 index 000000000..547c1470d --- /dev/null +++ b/module/web/static/js/views/dashboard/packageView.js @@ -0,0 +1,74 @@ +define(['jquery', 'app', 'views/abstract/itemView', 'underscore'], + function($, App, itemView, _) { + + // Renders a single package item + return itemView.extend({ + + tagName: 'li', + className: 'package-view', + template: _.compile($("#template-package").html()), + events: { + 'click .package-name, .btn-open': 'open', + 'click .iconf-refresh': 'restart', + 'click .select': 'select', + 'click .btn-delete': 'deleteItem' + }, + + // Ul for child packages (unused) + ul: null, + // Currently unused + expanded: false, + + initialize: function() { + this.listenTo(this.model, 'filter:added', this.hide); + this.listenTo(this.model, 'filter:removed', this.show); + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'remove', this.unrender); + + // Clear drop down menu + var self = this; + this.$el.on('mouseleave', function() { + self.$('.dropdown-menu').parent().removeClass('open'); + }); + }, + + onDestroy: function() { + }, + + // Render everything, optional only the fileViews + render: function() { + this.$el.html(this.template(this.model.toJSON())); + this.$el.initTooltips(); + + return this; + }, + + unrender: function() { + itemView.prototype.unrender.apply(this); + + // TODO: display other package + App.vent.trigger('dashboard:loading', null); + }, + + + // TODO + // Toggle expanding of packages + expand: function(e) { + e.preventDefault(); + }, + + open: function(e) { + e.preventDefault(); + App.dashboard.openPackage(this.model); + }, + + select: function(e) { + e.preventDefault(); + var checked = this.$('.select').hasClass('iconf-check'); + // toggle class immediately, so no re-render needed + this.model.set('selected', !checked, {silent: true}); + this.$('.select').toggleClass('iconf-check').toggleClass('iconf-check-empty'); + App.vent.trigger('package:selection'); + } + }); + }); \ No newline at end of file diff --git a/module/web/static/js/views/dashboard/selectionView.js b/module/web/static/js/views/dashboard/selectionView.js new file mode 100644 index 000000000..546cda847 --- /dev/null +++ b/module/web/static/js/views/dashboard/selectionView.js @@ -0,0 +1,150 @@ +define(['jquery', 'backbone', 'underscore', 'app'], + function($, Backbone, _, App) { + + // Renders context actions for selection packages and files + return Backbone.View.extend({ + el: '#selection-area', + template: _.compile($("#template-select").html()), + + events: { + 'click .iconf-check': 'deselect', + 'click .iconf-pause': 'pause', + 'click .iconf-trash': 'trash', + 'click .iconf-refresh': 'restart' + }, + + // Element of the action bar + actionBar: null, + // number of currently selected elements + current: 0, + + initialize: function() { + this.$el.calculateHeight().height(0); + + var render = _.bind(this.render, this); + + App.vent.on('dashboard:updated', render); + App.vent.on('dashboard:filtered', render); + App.vent.on('package:selection', render); + App.vent.on('file:selection', render); + + this.actionBar = $('.actionbar .btn-check'); + this.actionBar.parent().click(_.bind(this.select_toggle, this)); + + // API events, maybe better to rely on internal ones? + App.vent.on('package:deleted', render); + App.vent.on('file:deleted', render); + }, + + get_files: function(all) { + var files = []; + if (App.dashboard.files) + if (all) + files = App.dashboard.files.where({visible: true}); + else + files = App.dashboard.files.where({selected: true, visible: true}); + + return files; + }, + + get_packs: function() { + return App.dashboard.tree.get('packages').where({selected: true}); + }, + + render: function() { + var files = this.get_files().length; + var packs = this.get_packs().length; + + if (files + packs > 0) { + this.$el.html(this.template({files: files, packs: packs})); + this.$el.initTooltips('bottom'); + } + + if (files + packs > 0 && this.current === 0) + this.$el.slideOut(); + else if (files + packs === 0 && this.current > 0) + this.$el.slideIn(); + + if (files > 0) { + this.actionBar.addClass('iconf-check').removeClass('iconf-check-empty'); + App.dashboard.packageUL.addClass('ui-files-selected'); + } + else { + this.actionBar.addClass('iconf-check-empty').removeClass('iconf-check'); + App.dashboard.packageUL.removeClass('ui-files-selected'); + } + + this.current = files + packs; + }, + + // Deselects all items + deselect: function() { + this.get_files().map(function(file) { + file.set('selected', false); + }); + + this.get_packs().map(function(pack) { + pack.set('selected', false); + }); + + this.render(); + }, + + pause: function() { + alert("Not implemented yet"); + this.deselect(); + }, + + trash: function() { + _.confirm('default/confirmDialog.html', function() { + + var pids = []; + // TODO: delete many at once + this.get_packs().map(function(pack) { + pids.push(pack.get('pid')); + pack.destroy(); + }); + + // get only the fids of non deleted packages + var fids = _.filter(this.get_files(),function(file) { + return !_.contains(pids, file.get('package')); + }).map(function(file) { + file.destroyLocal(); + return file.get('fid'); + }); + + if (fids.length > 0) + $.ajax(App.apiRequest('deleteFiles', {fids: fids})); + + this.deselect(); + }, this); + }, + + restart: function() { + this.get_files().map(function(file) { + file.restart(); + }); + this.get_packs().map(function(pack) { + pack.restart(); + }); + + this.deselect(); + }, + + // Select or deselect all visible files + select_toggle: function() { + var files = this.get_files(); + if (files.length === 0) { + this.get_files(true).map(function(file) { + file.set('selected', true); + }); + + } else + files.map(function(file) { + file.set('selected', false); + }); + + this.render(); + } + }); + }); \ No newline at end of file diff --git a/module/web/static/js/views/dashboardView.js b/module/web/static/js/views/dashboardView.js deleted file mode 100644 index 58a50777c..000000000 --- a/module/web/static/js/views/dashboardView.js +++ /dev/null @@ -1,164 +0,0 @@ -define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', - 'views/packageView', 'views/fileView', 'views/selectionView', 'views/filterView', 'select2'], - function($, Backbone, _, App, TreeCollection, packageView, fileView, selectionView, filterView) { - - // Renders whole dashboard - return Backbone.View.extend({ - - el: '#content', - active: $('.breadcrumb .active'), - - events: { - }, - - //
        holding the packages - packageUL: null, - //
          displaying the files - fileUL: null, - // Package tree - tree: null, - // Current open files - files: null, - // True when loading animation is running - isLoading: false, - - initialize: function() { - var self = this; - this.tree = new TreeCollection(); - - var view = new selectionView(); - view = new filterView(); - - // When package is added we reload the data - App.vent.on('package:added', function() { - console.log('Package tree caught, package:added event'); - self.tree.fetch(); - }); - - App.vent.on('file:updated', _.bind(this.fileUpdated, this)); - - // TODO: file:added - // TODO: package:deleted - // TODO: package:updated - }, - - init: function() { - var self = this; - // TODO: put in separated function - // TODO: order of elements? - // Init the tree and callback for package added - this.tree.fetch({success: function() { - self.render(); - self.tree.get('packages').on('add', function(pack) { - console.log('Package ' + pack.get('pid') + ' added to tree'); - self.appendPackage(pack, 0, true); - self.openPackage(pack); - }); - }}); - - this.$('.input').select2({tags: ["a", "b", "sdf"]}); - }, - - render: function() { - console.log('Render package list'); - var packs = this.tree.get('packages'); - this.files = this.tree.get('files'); - - this.packageUL = this.$('.package-list'); - packs.each(_.bind(this.appendPackage, this)); - - this.fileUL = this.$('.file-list'); - if (this.files.length === 0) { - // no files are displayed - this.files = null; - // Open the first package - if (packs.length >= 1) - this.openPackage(packs.at(0)); - } - else - this.files.each(_.bind(this.appendFile, this)); - - return this; - }, - - // TODO sorting ?! - // Append a package to the list, index, animate it - appendPackage: function(pack, i, animation) { - var el = new packageView({model: pack}).render().el; - this.packageUL.appendWithAnimation(el, animation); - }, - - appendFile: function(file, i, animation) { - var el = new fileView({model: file}).render().el; - this.fileUL.appendWithAnimation(el, animation); - }, - - // Show content of the packages on main view - openPackage: function(pack) { - var self = this; - - // load animation only when something is shown and its different from current package - if (this.files && this.files !== pack.get('files')) - self.loading(); - - pack.fetch({silent: true, success: function() { - console.log('Package ' + pack.get('pid') + ' loaded'); - self.active.text(pack.get('name')); - self.contentReady(pack.get('files')); - }, failure: function() { - self.failure(); - }}); - - }, - - contentReady: function(files) { - var old_files = this.files; - this.files = files; - App.vent.trigger('dashboard:contentReady'); - - // show the files when no loading animation is running and not already open - if (!this.isLoading && old_files !== files) - this.show(); - }, - - // Do load animation, remove the old stuff - loading: function() { - this.isLoading = true; - this.files = null; - var self = this; - this.fileUL.fadeOut({complete: function() { - // All file views should vanish - App.vent.trigger('dashboard:destroyContent'); - - // Loading was faster than animation - if (self.files) - self.show(); - - self.isLoading = false; - }}); - }, - - failure: function() { - // TODO - }, - - show: function() { - // fileUL has to be resetted before - this.files.each(_.bind(this.appendFile, this)); - //TODO: show placeholder when nothing is displayed (filtered content empty) - this.fileUL.fadeIn(); - App.vent.trigger('dashboard:updated'); - }, - - // Refresh the file if it is currently shown - fileUpdated: function(data) { - // this works with ids and object - var file = this.files.get(data); - if (file) - if (_.isObject(data)) // update directly - file.set(data); - else // fetch from server - file.fetch(); - } - }); - }); \ No newline at end of file diff --git a/module/web/static/js/views/fileView.js b/module/web/static/js/views/fileView.js deleted file mode 100644 index c673041b5..000000000 --- a/module/web/static/js/views/fileView.js +++ /dev/null @@ -1,97 +0,0 @@ -define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', 'helpers/formatTime'], - function($, Backbone, _, App, Api, ItemView, formatTime) { - - // Renders single file item - return ItemView.extend({ - - tagName: 'li', - className: 'file-view row-fluid', - template: _.compile($("#template-file").html()), - events: { - 'click .checkbox': 'select', - 'click .btn-delete': 'deleteItem', - 'click .btn-restart': 'restart' - }, - - initialize: function() { - this.listenTo(this.model, 'change', this.render); - // This will be triggered manually and changed before with silent=true - this.listenTo(this.model, 'change:visible', this.visibility_changed); - this.listenTo(this.model, 'change:progress', this.progress_changed); - this.listenTo(this.model, 'remove', this.unrender); - this.listenTo(App.vent, 'dashboard:destroyContent', this.destroy); - }, - - onDestroy: function() { - }, - - render: function() { - var data = this.model.toJSON(); - if (data.download) { - var status = data.download.status; - if (status === Api.DownloadStatus.Offline || status === Api.DownloadStatus.TempOffline) - data.offline = true; - else if (status === Api.DownloadStatus.Online) - data.online = true; - else if (status === Api.DownloadStatus.Waiting) - data.waiting = true; - else if (status === Api.DownloadStatus.Downloading) - data.downloading = true; - else if (this.model.isFailed()) - data.failed = true; - else if (this.model.isFinished()) - data.finished = true; - } - - this.$el.html(this.template(data)); - if (this.model.get('selected')) - this.$el.addClass('ui-selected'); - else - this.$el.removeClass('ui-selected'); - - if (this.model.get('visible')) - this.$el.show(); - else - this.$el.hide(); - - return this; - }, - - select: function(e) { - e.preventDefault(); - var checked = this.$el.hasClass('ui-selected'); - // toggle class immediately, so no re-render needed - this.model.set('selected', !checked, {silent: true}); - this.$el.toggleClass('ui-selected'); - App.vent.trigger('file:selection'); - }, - - visibility_changed: function(visible) { - // TODO: improve animation, height is not available when element was not visible - if (visible) - this.$el.slideOut(true); - else { - this.$el.calculateHeight(true); - this.$el.slideIn(true); - } - }, - - progress_changed: function() { - if(!this.model.isDownload()) - return; - - if (this.model.get('download').status === Api.DownloadStatus.Downloading) { - var bar = this.$('.progress .bar'); - if (!bar) { // ensure that the dl bar is rendered - this.render(); - bar = this.$('.progress .bar'); - } - - bar.width(this.model.get('progress') + '%'); - bar.html('  ' + formatTime(this.model.get('eta'))); - } else // Every else state can be renderred normally - this.render(); - - } - }); - }); \ No newline at end of file diff --git a/module/web/static/js/views/filterView.js b/module/web/static/js/views/filterView.js deleted file mode 100644 index 14968f2cc..000000000 --- a/module/web/static/js/views/filterView.js +++ /dev/null @@ -1,133 +0,0 @@ -define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'models/Package'], - function($, Backbone, _, App, Api, Package) { - - // Modified version of type ahead show, nearly the same without absolute positioning - function show() { - this.$menu - .insertAfter(this.$element) - .show(); - - this.shown = true; - return this; - } - - // Renders the actionbar for the dashboard, handles everything related to filtering displayed files - return Backbone.View.extend({ - el: 'ul.actionbar', - - events: { - 'click .filter-type': 'filter_type', - 'click .filter-state': 'switch_filter', - 'submit .form-search': 'search' - }, - - state: null, - stateMenu: null, - - initialize: function() { - - // use our modified method - $.fn.typeahead.Constructor.prototype.show = show; - this.$('.search-query').typeahead({ - minLength: 2, - source: this.getSuggestions - }); - - this.stateMenu = this.$('.dropdown-toggle .state'); - this.state = Api.DownloadState.All; - - // Apply the filter before the content is shown - App.vent.on('dashboard:contentReady', _.bind(this.apply_filter, this)); - }, - - render: function() { - return this; - }, - - // TODO: app level api request - - search: function(e) { - e.stopPropagation(); - var input = this.$('.search-query'); - var query = input.val(); - input.val(''); - - var pack = new Package(); - // Overwrite fetch method to use a search - // TODO: quite hackish, could be improved to filter packages - // or show performed search - pack.fetch = function(options) { - pack.search(query, options); - }; - - App.dashboard.openPackage(pack); - }, - - getSuggestions: function(query, callback) { - $.ajax('/api/searchSuggestions', { - method: 'POST', - data: {pattern: JSON.stringify(query)}, - success: function(data) { - callback(data); - } - }); - }, - - switch_filter: function(e) { - e.stopPropagation(); - var element = $(e.target); - var state = parseInt(element.data('state'), 10); - var menu = this.stateMenu.parent().parent(); - menu.removeClass('open'); - - if (state === Api.DownloadState.Finished) { - menu.removeClass().addClass('dropdown finished'); - } else if (state === Api.DownloadState.Unfinished) { - menu.removeClass().addClass('dropdown active'); - } else if (state === Api.DownloadState.Failed) { - menu.removeClass().addClass('dropdown failed'); - } else { - menu.removeClass().addClass('dropdown'); - } - - this.state = state; - this.stateMenu.text(element.text()); - this.apply_filter(); - }, - - // Applies the filtering to current open files - apply_filter: function() { - if (!App.dashboard.files) - return; - - var self = this; - App.dashboard.files.map(function(file) { - var visible = file.get('visible'); - if (visible !== self.is_visible(file)) { - file.set('visible', !visible, {silent: true}); - file.trigger('change:visible', !visible); - } - }); - - App.vent.trigger('dashboard:filtered'); - }, - - // determine if a file should be visible - // TODO: non download files - is_visible: function(file) { - if (this.state === Api.DownloadState.Finished) - return file.isFinished(); - else if (this.state === Api.DownloadState.Unfinished) - return file.isUnfinished(); - else if (this.state === Api.DownloadState.Failed) - return file.isFailed(); - - return true; - }, - - filter_type: function(e) { - - } - - }); - }); \ No newline at end of file diff --git a/module/web/static/js/views/input/inputLoader.js b/module/web/static/js/views/input/inputLoader.js new file mode 100644 index 000000000..5ccf07695 --- /dev/null +++ b/module/web/static/js/views/input/inputLoader.js @@ -0,0 +1,7 @@ +define(['./textInput'], function(textInput) { + + // selects appropriate input element + return function(input, value, default_value, description) { + return textInput; + }; +}); \ No newline at end of file diff --git a/module/web/static/js/views/input/inputView.js b/module/web/static/js/views/input/inputView.js new file mode 100644 index 000000000..15dc71aad --- /dev/null +++ b/module/web/static/js/views/input/inputView.js @@ -0,0 +1,55 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { + + // Renders input elements + return Backbone.View.extend({ + + tagName: 'input', + + model: null, + value: null, + default_value: null, + description: null, + + initialize: function(model, value, default_value, description) { + this.model = model; + this.value = value; + this.default_value = default_value; + this.description = description; + }, + + render: function() { + return this; + }, + + destroy: function() { + this.undelegateEvents(); + this.unbind(); + if (this.onDestroy) { + this.onDestroy(); + } + this.$el.removeData().unbind(); + this.remove(); + }, + + // focus the input element + focus: function() { + this.$el.focus(); + }, + + // Clear the input + clear: function() { + + }, + + // retrieve value of the input + getVal: function() { + return this.value; + }, + + // the child class must call this when the value changed + setVal: function(value) { + this.value = value; + this.trigger('change', value); + } + }); +}); \ No newline at end of file diff --git a/module/web/static/js/views/input/textInput.js b/module/web/static/js/views/input/textInput.js new file mode 100644 index 000000000..7252ce289 --- /dev/null +++ b/module/web/static/js/views/input/textInput.js @@ -0,0 +1,33 @@ +define(['jquery', 'backbone', 'underscore', './inputView'], function($, Backbone, _, inputView) { + + return inputView.extend({ + + // TODO + tagName: 'input', + events: { + 'keypress': 'onChange' + }, + + render: function() { + this.$el.attr('type', 'text'); + this.$el.attr('name', 'textInput'); + + if (this.default_value) + this.$el.attr('placeholder', this.default_value); + + if (this.value) + this.$el.val(this.value); + + return this; + }, + + clear: function() { + this.$el.val(''); + }, + + onChange: function(e) { + this.setVal(this.$el.val()); + } + + }); +}); \ No newline at end of file diff --git a/module/web/static/js/views/packageView.js b/module/web/static/js/views/packageView.js deleted file mode 100644 index 547c1470d..000000000 --- a/module/web/static/js/views/packageView.js +++ /dev/null @@ -1,74 +0,0 @@ -define(['jquery', 'app', 'views/abstract/itemView', 'underscore'], - function($, App, itemView, _) { - - // Renders a single package item - return itemView.extend({ - - tagName: 'li', - className: 'package-view', - template: _.compile($("#template-package").html()), - events: { - 'click .package-name, .btn-open': 'open', - 'click .iconf-refresh': 'restart', - 'click .select': 'select', - 'click .btn-delete': 'deleteItem' - }, - - // Ul for child packages (unused) - ul: null, - // Currently unused - expanded: false, - - initialize: function() { - this.listenTo(this.model, 'filter:added', this.hide); - this.listenTo(this.model, 'filter:removed', this.show); - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.model, 'remove', this.unrender); - - // Clear drop down menu - var self = this; - this.$el.on('mouseleave', function() { - self.$('.dropdown-menu').parent().removeClass('open'); - }); - }, - - onDestroy: function() { - }, - - // Render everything, optional only the fileViews - render: function() { - this.$el.html(this.template(this.model.toJSON())); - this.$el.initTooltips(); - - return this; - }, - - unrender: function() { - itemView.prototype.unrender.apply(this); - - // TODO: display other package - App.vent.trigger('dashboard:loading', null); - }, - - - // TODO - // Toggle expanding of packages - expand: function(e) { - e.preventDefault(); - }, - - open: function(e) { - e.preventDefault(); - App.dashboard.openPackage(this.model); - }, - - select: function(e) { - e.preventDefault(); - var checked = this.$('.select').hasClass('iconf-check'); - // toggle class immediately, so no re-render needed - this.model.set('selected', !checked, {silent: true}); - this.$('.select').toggleClass('iconf-check').toggleClass('iconf-check-empty'); - App.vent.trigger('package:selection'); - } - }); - }); \ No newline at end of file diff --git a/module/web/static/js/views/queryModal.js b/module/web/static/js/views/queryModal.js index 5d1585a0d..86fd5b78b 100644 --- a/module/web/static/js/views/queryModal.js +++ b/module/web/static/js/views/queryModal.js @@ -1,15 +1,18 @@ -define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'text!tpl/default/queryDialog.html'], - function($, _, App, modalView, template) { +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', './input/inputLoader', 'text!tpl/default/queryDialog.html'], + function($, _, App, modalView, load_input, template) { return modalView.extend({ events: { 'click .btn-success': 'submit', 'submit form': 'submit' }, + template: _.compile(template), - model: null, + // the notificationView parent: null, - template: _.compile(template), + + model: null, + input: null, initialize: function() { // Inherit parent events @@ -23,32 +26,42 @@ define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'text!tpl/def description: this.model.get('description') }; + var input = this.model.get('input').data; if (this.model.isCaptcha()) { - var input = this.model.get('input').data; data.captcha = input[0]; data.type = input[1]; } - return data; }, + onRender: function() { + // instantiate the input + var input = this.model.get('input'); + var inputView = load_input(input); + this.input = new inputView(input); + // only renders after wards + this.$('#inputField').append(this.input.render().el); + }, + submit: function(e) { e.stopPropagation(); - // TODO: different input types // TODO: load next task - this.model.set('result', this.$('input').val()); + this.model.set('result', this.input.getVal()); var self = this; this.model.save({success: function() { self.hide(); }}); - this.$('input').val(''); + this.input.clear(); }, onShow: function() { - this.$('input').focus(); - } + this.input.focus(); + }, + onHide: function() { + this.input.destroy(); + } }); }); \ No newline at end of file diff --git a/module/web/static/js/views/selectionView.js b/module/web/static/js/views/selectionView.js deleted file mode 100644 index 546cda847..000000000 --- a/module/web/static/js/views/selectionView.js +++ /dev/null @@ -1,150 +0,0 @@ -define(['jquery', 'backbone', 'underscore', 'app'], - function($, Backbone, _, App) { - - // Renders context actions for selection packages and files - return Backbone.View.extend({ - el: '#selection-area', - template: _.compile($("#template-select").html()), - - events: { - 'click .iconf-check': 'deselect', - 'click .iconf-pause': 'pause', - 'click .iconf-trash': 'trash', - 'click .iconf-refresh': 'restart' - }, - - // Element of the action bar - actionBar: null, - // number of currently selected elements - current: 0, - - initialize: function() { - this.$el.calculateHeight().height(0); - - var render = _.bind(this.render, this); - - App.vent.on('dashboard:updated', render); - App.vent.on('dashboard:filtered', render); - App.vent.on('package:selection', render); - App.vent.on('file:selection', render); - - this.actionBar = $('.actionbar .btn-check'); - this.actionBar.parent().click(_.bind(this.select_toggle, this)); - - // API events, maybe better to rely on internal ones? - App.vent.on('package:deleted', render); - App.vent.on('file:deleted', render); - }, - - get_files: function(all) { - var files = []; - if (App.dashboard.files) - if (all) - files = App.dashboard.files.where({visible: true}); - else - files = App.dashboard.files.where({selected: true, visible: true}); - - return files; - }, - - get_packs: function() { - return App.dashboard.tree.get('packages').where({selected: true}); - }, - - render: function() { - var files = this.get_files().length; - var packs = this.get_packs().length; - - if (files + packs > 0) { - this.$el.html(this.template({files: files, packs: packs})); - this.$el.initTooltips('bottom'); - } - - if (files + packs > 0 && this.current === 0) - this.$el.slideOut(); - else if (files + packs === 0 && this.current > 0) - this.$el.slideIn(); - - if (files > 0) { - this.actionBar.addClass('iconf-check').removeClass('iconf-check-empty'); - App.dashboard.packageUL.addClass('ui-files-selected'); - } - else { - this.actionBar.addClass('iconf-check-empty').removeClass('iconf-check'); - App.dashboard.packageUL.removeClass('ui-files-selected'); - } - - this.current = files + packs; - }, - - // Deselects all items - deselect: function() { - this.get_files().map(function(file) { - file.set('selected', false); - }); - - this.get_packs().map(function(pack) { - pack.set('selected', false); - }); - - this.render(); - }, - - pause: function() { - alert("Not implemented yet"); - this.deselect(); - }, - - trash: function() { - _.confirm('default/confirmDialog.html', function() { - - var pids = []; - // TODO: delete many at once - this.get_packs().map(function(pack) { - pids.push(pack.get('pid')); - pack.destroy(); - }); - - // get only the fids of non deleted packages - var fids = _.filter(this.get_files(),function(file) { - return !_.contains(pids, file.get('package')); - }).map(function(file) { - file.destroyLocal(); - return file.get('fid'); - }); - - if (fids.length > 0) - $.ajax(App.apiRequest('deleteFiles', {fids: fids})); - - this.deselect(); - }, this); - }, - - restart: function() { - this.get_files().map(function(file) { - file.restart(); - }); - this.get_packs().map(function(pack) { - pack.restart(); - }); - - this.deselect(); - }, - - // Select or deselect all visible files - select_toggle: function() { - var files = this.get_files(); - if (files.length === 0) { - this.get_files(true).map(function(file) { - file.set('selected', true); - }); - - } else - files.map(function(file) { - file.set('selected', false); - }); - - this.render(); - } - }); - }); \ No newline at end of file diff --git a/module/web/templates/default/backbone/queryDialog.html b/module/web/templates/default/backbone/queryDialog.html index a37ac6256..eb05c20e2 100755 --- a/module/web/templates/default/backbone/queryDialog.html +++ b/module/web/templates/default/backbone/queryDialog.html @@ -10,12 +10,16 @@ <% description %>
          <%= if captcha %> - -
          - -
          + + <% else %> + <%/if%> - <% content %> +
          +
          {% endblock %} -- cgit v1.2.3