summaryrefslogtreecommitdiffstats
path: root/module/web/static/js/views
diff options
context:
space:
mode:
Diffstat (limited to 'module/web/static/js/views')
-rw-r--r--module/web/static/js/views/abstract/itemView.js46
-rw-r--r--module/web/static/js/views/abstract/modalView.js115
-rw-r--r--module/web/static/js/views/dashboardView.js164
-rw-r--r--module/web/static/js/views/fileView.js97
-rw-r--r--module/web/static/js/views/filterView.js133
-rw-r--r--module/web/static/js/views/headerView.js231
-rw-r--r--module/web/static/js/views/linkGrabberModal.js48
-rw-r--r--module/web/static/js/views/notificationView.js80
-rw-r--r--module/web/static/js/views/packageView.js74
-rw-r--r--module/web/static/js/views/progressView.js31
-rw-r--r--module/web/static/js/views/queryModal.js54
-rw-r--r--module/web/static/js/views/selectionView.js150
-rw-r--r--module/web/static/js/views/settingsView.js56
13 files changed, 1279 insertions, 0 deletions
diff --git a/module/web/static/js/views/abstract/itemView.js b/module/web/static/js/views/abstract/itemView.js
new file mode 100644
index 000000000..394044ec4
--- /dev/null
+++ b/module/web/static/js/views/abstract/itemView.js
@@ -0,0 +1,46 @@
+define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) {
+
+ // A view that is meant for temporary displaying
+ // All events must be unbound in onDestroy
+ return Backbone.View.extend({
+
+ tagName: 'li',
+ destroy: function() {
+ this.undelegateEvents();
+ this.unbind();
+ if (this.onDestroy) {
+ this.onDestroy();
+ }
+ this.$el.removeData().unbind();
+ this.remove();
+ },
+
+ hide: function() {
+ this.$el.slideUp();
+ },
+
+ show: function() {
+ this.$el.slideDown();
+ },
+
+ unrender: function() {
+ var self = this;
+ this.$el.slideUp(function() {
+ self.destroy();
+ });
+ },
+
+ deleteItem: function(e) {
+ if (e)
+ e.stopPropagation();
+ this.model.destroy();
+ },
+
+ restart: function(e) {
+ if(e)
+ e.stopPropagation();
+ this.model.restart();
+ }
+
+ });
+}); \ No newline at end of file
diff --git a/module/web/static/js/views/abstract/modalView.js b/module/web/static/js/views/abstract/modalView.js
new file mode 100644
index 000000000..d3ac34bd6
--- /dev/null
+++ b/module/web/static/js/views/abstract/modalView.js
@@ -0,0 +1,115 @@
+define(['jquery', 'backbone', 'underscore', 'omniwindow'], function($, Backbone, _) {
+
+ return Backbone.View.extend({
+
+ events: {
+ 'click .btn-confirm': 'confirm',
+ 'click .btn-close': 'hide',
+ 'click .close': 'hide'
+ },
+
+ template: null,
+ dialog: null,
+
+ onHideDestroy: false,
+ confirmCallback: null,
+
+ initialize: function(template, confirm) {
+ this.confirmCallback = confirm;
+ var self = this;
+ if (this.template === null) {
+ if (template) {
+ this.template = template;
+ // When template was provided this is a temporary dialog
+ this.onHideDestroy = true;
+ }
+ else
+ require(['text!tpl/default/modal.html'], function(template) {
+ self.template = template;
+ });
+ }
+
+ },
+
+ 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() {
+ 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() {
+ internalCallback(subjects);
+ });
+ }}
+ }});
+
+ return this;
+ },
+ renderContent: function() {
+ return {content: $('<h1>Content!</h1>').html()};
+ },
+
+ show: function() {
+ if (this.dialog === null)
+ this.render();
+
+ this.dialog.trigger('show');
+
+ this.onShow();
+ },
+
+ onShow: function() {
+
+ },
+
+ hide: function() {
+ this.dialog.trigger('hide');
+ this.onHide();
+ },
+
+ onHide: function() {
+
+ },
+
+ confirm: function() {
+ if (this.confirmCallback)
+ this.confirmCallback.apply();
+
+ this.hide();
+ },
+
+ destroy: function() {
+ this.$el.remove();
+ this.dialog = null;
+ this.remove();
+ }
+
+ });
+}); \ No newline at end of file
diff --git a/module/web/static/js/views/dashboardView.js b/module/web/static/js/views/dashboardView.js
new file mode 100644
index 000000000..58a50777c
--- /dev/null
+++ b/module/web/static/js/views/dashboardView.js
@@ -0,0 +1,164 @@
+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: {
+ },
+
+ // <ul> holding the packages
+ packageUL: null,
+ // <ul> 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
new file mode 100644
index 000000000..c673041b5
--- /dev/null
+++ b/module/web/static/js/views/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('&nbsp;&nbsp;' + 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
new file mode 100644
index 000000000..14968f2cc
--- /dev/null
+++ b/module/web/static/js/views/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/headerView.js b/module/web/static/js/views/headerView.js
new file mode 100644
index 000000000..25127a337
--- /dev/null
+++ b/module/web/static/js/views/headerView.js
@@ -0,0 +1,231 @@
+define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'collections/ProgressList',
+ 'views/progressView', 'views/notificationView', 'helpers/formatSize', 'flot'],
+ function($, _, Backbone, App, ServerStatus, ProgressList, ProgressView, notificationView, formatSize) {
+ // Renders the header with all information
+ return Backbone.View.extend({
+
+ el: 'header',
+
+ events: {
+ 'click .iconf-list': 'toggle_taskList',
+ 'click .popover .close': 'toggle_taskList',
+ 'click .btn-grabber': 'open_grabber'
+ },
+
+ // todo: maybe combine these
+ templateStatus: _.compile($('#template-header-status').html()),
+ templateHeader: _.compile($('#template-header').html()),
+
+ // html elements
+ grabber: null,
+ header: null,
+ progress: null,
+ speedgraph: null,
+
+ // models and data
+ ws: null,
+ status: null,
+ progressList: null,
+ speeds: null,
+
+ // sub view
+ notificationView: null,
+
+ // save if last progress was empty
+ wasEmpty: false,
+
+ initialize: function() {
+ var self = this;
+ this.notificationView = new notificationView();
+
+ this.status = new ServerStatus();
+ this.listenTo(this.status, 'change', this.render);
+
+ this.progress = this.$('.progress-list');
+ this.progressList = new ProgressList();
+ this.listenTo(this.progressList, 'add', function(model) {
+ self.progress.appendWithAnimation(new ProgressView({model: model}).render().el);
+ });
+
+ // TODO: button to start stop refresh
+ var ws = App.openWebSocket('/async');
+ ws.onopen = function() {
+ ws.send(JSON.stringify('start'));
+ };
+ // TODO compare with polling
+ ws.onmessage = _.bind(this.onData, this);
+ ws.onerror = function(error) {
+ alert(error);
+ };
+
+ this.ws = ws;
+
+ this.initGraph();
+ },
+
+ initGraph: function() {
+ var totalPoints = 120;
+ var data = [];
+
+ // init with empty data
+ while (data.length < totalPoints)
+ data.push([data.length, 0]);
+
+ this.speeds = data;
+ this.speedgraph = $.plot(this.$el.find("#speedgraph"), [this.speeds], {
+ series: {
+ lines: { show: true, lineWidth: 2 },
+ shadowSize: 0,
+ color: "#fee247"
+ },
+ xaxis: { ticks: [] },
+ yaxis: { ticks: [], min: 1, autoscaleMargin: 0.1, tickFormatter: function(data) {
+ return formatSize(data * 1024);
+ }, position: "right" },
+ grid: {
+ show: true,
+// borderColor: "#757575",
+ borderColor: "white",
+ borderWidth: 1,
+ labelMargin: 0,
+ axisMargin: 0,
+ minBorderMargin: 0
+ }
+ });
+
+ },
+
+ render: function() {
+ // TODO: what should be displayed in the header
+ // queue/processing size?
+
+ var status = this.status.toJSON();
+ status.maxspeed = _.max(this.speeds, function(speed) {
+ return speed[1];
+ })[1] * 1024;
+ this.$('.status-block').html(
+ this.templateStatus(status)
+ );
+
+ var data = {tasks: 0, downloads: 0, speed: 0, single: false};
+ this.progressList.each(function(progress) {
+ if (progress.isDownload()) {
+ data.downloads += 1;
+ data.speed += progress.get('download').speed;
+ } else
+ data.tasks++;
+ });
+
+ // Show progress of one task
+ if (data.tasks + data.downloads === 1) {
+ var progress = this.progressList.at(0);
+ data.single = true;
+ data.eta = progress.get('eta');
+ data.percent = progress.getPercent();
+ data.name = progress.get('name');
+ data.statusmsg = progress.get('statusmsg');
+ }
+ // TODO: better progressbar rendering
+
+ data.etaqueue = status.eta;
+ data.linksqueue = status.linksqueue;
+ data.sizequeue = status.sizequeue;
+
+ this.$('#progress-info').html(
+ this.templateHeader(data)
+ );
+ return this;
+ },
+
+ toggle_taskList: function() {
+ this.$('.popover').animate({opacity: 'toggle'});
+ },
+
+ open_grabber: function() {
+ var self = this;
+ _.requireOnce(['views/linkGrabberModal'], function(modalView) {
+ if (self.grabber === null)
+ self.grabber = new modalView();
+
+ self.grabber.show();
+ });
+ },
+
+ onData: function(evt) {
+ var data = JSON.parse(evt.data);
+ if (data === null) return;
+
+ if (data['@class'] === "ServerStatus") {
+ // TODO: load interaction when none available
+ this.status.set(data);
+
+ // There tasks at the server, but not in queue: so fetch them
+ // or there are tasks in our queue but not on the server
+ if (this.status.get('notifications') && !this.notificationView.tasks.hasTaskWaiting() ||
+ !this.status.get('notifications') && this.notificationView.tasks.hasTaskWaiting())
+ this.notificationView.tasks.fetch();
+
+ this.speeds = this.speeds.slice(1);
+ this.speeds.push([this.speeds[this.speeds.length - 1][0] + 1, Math.floor(data.speed / 1024)]);
+
+ // TODO: if everything is 0 rerender is not needed
+ this.speedgraph.setData([this.speeds]);
+ // adjust the axis
+ this.speedgraph.setupGrid();
+ this.speedgraph.draw();
+
+ }
+ else if (_.isArray(data))
+ this.onProgressUpdate(data);
+ else if (data['@class'] === 'EventInfo')
+ this.onEvent(data.eventname, data.event_args);
+ else
+ console.log('Unknown Async input', data);
+
+ },
+
+ onProgressUpdate: function(progress) {
+ // generate a unique id
+ _.each(progress, function(prog) {
+ if (prog.download)
+ prog.pid = prog.download.fid;
+ else
+ prog.pid = prog.plugin + prog.name;
+ });
+
+ this.progressList.update(progress);
+ // update currently open files with progress
+ this.progressList.each(function(prog) {
+ if (prog.isDownload() && App.dashboard.files) {
+ var file = App.dashboard.files.get(prog.get('download').fid);
+ if (file) {
+ file.set({
+ progress: prog.getPercent(),
+ eta: prog.get('eta'),
+ }, {silent: true});
+
+ file.trigger('change:progress');
+ }
+ }
+ });
+
+ if (progress.length === 0) {
+ // only render one time when last was not empty already
+ if (!this.wasEmpty) {
+ this.render();
+ this.wasEmpty = true;
+ }
+ } else {
+ this.wasEmpty = false;
+ this.render();
+ }
+ },
+
+ onEvent: function(event, args) {
+ args.unshift(event);
+ console.log('Core send event', args);
+ App.vent.trigger.apply(App.vent, args);
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/module/web/static/js/views/linkGrabberModal.js b/module/web/static/js/views/linkGrabberModal.js
new file mode 100644
index 000000000..e2b6e985d
--- /dev/null
+++ b/module/web/static/js/views/linkGrabberModal.js
@@ -0,0 +1,48 @@
+define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'text!tpl/default/linkgrabber.html'],
+ function($, _, App, modalView, template) {
+ // Modal dialog for package adding - triggers package:added when package was added
+ return modalView.extend({
+
+ events: {
+ 'click .btn-success': 'addPackage',
+ 'keypress #inputPackageName': 'addOnEnter'
+ },
+
+ template: _.compile(template),
+
+ initialize: function() {
+ // Inherit parent events
+ this.events = _.extend({}, modalView.prototype.events, this.events);
+ },
+
+ addOnEnter: function(e) {
+ if (e.keyCode != 13) return;
+ this.addPackage(e);
+ },
+
+ addPackage: function(e) {
+ var self = this;
+ var options = App.apiRequest('addPackage',
+ {
+ name: $('#inputPackageName').val(),
+ // TODO: better parsing / tokenization
+ links: $('#inputLinks').val().split("\n")
+ },
+ {
+ success: function() {
+ App.vent.trigger('package:added');
+ self.hide();
+ }
+ });
+
+ $.ajax(options);
+ $('#inputPackageName').val('');
+ $('#inputLinks').val('');
+ },
+
+ onShow: function() {
+ this.$('#inputPackageName').focus();
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/module/web/static/js/views/notificationView.js b/module/web/static/js/views/notificationView.js
new file mode 100644
index 000000000..afb542eed
--- /dev/null
+++ b/module/web/static/js/views/notificationView.js
@@ -0,0 +1,80 @@
+define(['jquery', 'backbone', 'underscore', 'app', 'collections/InteractionList'],
+ function($, Backbone, _, App, InteractionList, queryModal) {
+
+ // Renders context actions for selection packages and files
+ return Backbone.View.extend({
+ el: '#notification-area',
+ template: _.compile($("#template-notification").html()),
+
+ events: {
+ 'click .btn-query': 'openQuery',
+ 'click .btn-notification': 'openNotifications'
+ },
+
+ tasks: null,
+ // area is slided out
+ visible: false,
+ // the dialog
+ modal: null,
+
+ initialize: function() {
+ this.tasks = new InteractionList();
+
+ this.$el.calculateHeight().height(0);
+
+ App.vent.on('interaction:added', _.bind(this.onAdd, this));
+ App.vent.on('interaction:deleted', _.bind(this.onDelete, this));
+
+ var render = _.bind(this.render, this);
+ this.listenTo(this.tasks, 'add', render);
+ this.listenTo(this.tasks, 'remove', render);
+
+ },
+
+ onAdd: function(task) {
+ this.tasks.add(task);
+ },
+
+ onDelete: function(task) {
+ this.tasks.remove(task);
+ },
+
+ render: function() {
+
+ // only render when it will be visible
+ if (this.tasks.length > 0)
+ this.$el.html(this.template(this.tasks.toJSON()));
+
+ if (this.tasks.length > 0 && !this.visible) {
+ this.$el.slideOut();
+ this.visible = true;
+ }
+ else if (this.tasks.length === 0 && this.visible) {
+ this.$el.slideIn();
+ this.visible = false;
+ }
+
+ return this;
+ },
+
+ openQuery: function() {
+ var self = this;
+
+ _.requireOnce(['views/queryModal'], function(modalView) {
+ if (self.modal === null) {
+ self.modal = new modalView();
+ self.modal.parent = self;
+ }
+
+ self.modal.model = self.tasks.at(0);
+ self.modal.render();
+ self.modal.show();
+ });
+
+ },
+
+ openNotifications: function() {
+
+ }
+ });
+ }); \ No newline at end of file
diff --git a/module/web/static/js/views/packageView.js b/module/web/static/js/views/packageView.js
new file mode 100644
index 000000000..547c1470d
--- /dev/null
+++ b/module/web/static/js/views/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/progressView.js b/module/web/static/js/views/progressView.js
new file mode 100644
index 000000000..3c2908000
--- /dev/null
+++ b/module/web/static/js/views/progressView.js
@@ -0,0 +1,31 @@
+define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView'],
+ function($, Backbone, _, App, Api, ItemView) {
+
+ // Renders single file item
+ return ItemView.extend({
+
+ idAttribute: 'pid',
+ tagName: 'li',
+ template: _.compile($("#template-header-progress").html()),
+ events: {
+ },
+
+ initialize: function() {
+ this.listenTo(this.model, 'change', this.render);
+ this.listenTo(this.model, 'remove', this.unrender);
+ },
+
+ onDestroy: function() {
+ },
+
+ render: function() {
+ // TODO: icon
+ // TODO: other states
+ // TODO: non download progress
+ // TODO: better progressbar rendering
+ this.$el.css('background-image', 'url(icons/sdf)');
+ this.$el.html(this.template(this.model.toJSON()));
+ return this;
+ }
+ });
+ }); \ No newline at end of file
diff --git a/module/web/static/js/views/queryModal.js b/module/web/static/js/views/queryModal.js
new file mode 100644
index 000000000..5d1585a0d
--- /dev/null
+++ b/module/web/static/js/views/queryModal.js
@@ -0,0 +1,54 @@
+define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'text!tpl/default/queryDialog.html'],
+ function($, _, App, modalView, template) {
+ return modalView.extend({
+
+ events: {
+ 'click .btn-success': 'submit',
+ 'submit form': 'submit'
+ },
+
+ model: null,
+ parent: null,
+ template: _.compile(template),
+
+ initialize: function() {
+ // Inherit parent events
+ this.events = _.extend({}, modalView.prototype.events, this.events);
+ },
+
+ renderContent: function() {
+ var data = {
+ title: this.model.get('title'),
+ plugin: this.model.get('plugin'),
+ description: this.model.get('description')
+ };
+
+ if (this.model.isCaptcha()) {
+ var input = this.model.get('input').data;
+ data.captcha = input[0];
+ data.type = input[1];
+ }
+
+ return data;
+ },
+
+ submit: function(e) {
+ e.stopPropagation();
+ // TODO: different input types
+ // TODO: load next task
+
+ this.model.set('result', this.$('input').val());
+ var self = this;
+ this.model.save({success: function() {
+ self.hide();
+ }});
+
+ this.$('input').val('');
+ },
+
+ onShow: function() {
+ this.$('input').focus();
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/module/web/static/js/views/selectionView.js b/module/web/static/js/views/selectionView.js
new file mode 100644
index 000000000..546cda847
--- /dev/null
+++ b/module/web/static/js/views/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/settingsView.js b/module/web/static/js/views/settingsView.js
new file mode 100644
index 000000000..a322cdae7
--- /dev/null
+++ b/module/web/static/js/views/settingsView.js
@@ -0,0 +1,56 @@
+define(['jquery', 'underscore', 'backbone'],
+ function($, _, Backbone) {
+
+ // Renders settings over view page
+ return Backbone.View.extend({
+
+ el: "#content",
+ template_menu: _.compile($("#template-menu").html()),
+
+ events: {
+ 'click .settings-menu li > a': 'change_section'
+ },
+
+ menu: null,
+
+ core_config: null, // It seems models are not needed
+ plugin_config: null,
+
+ initialize: function() {
+ this.menu = $('.settings-menu');
+ this.refresh();
+
+ console.log("Settings initialized");
+ },
+
+ refresh: function() {
+ var self = this;
+ $.ajax("/api/getCoreConfig", {success: function(data) {
+ self.core_config = data;
+ self.render()
+ }});
+ $.ajax("/api/getPluginConfig", {success: function(data) {
+ self.plugin_config = data;
+ self.render();
+ }});
+ },
+
+ render: function() {
+ this.menu.html(this.template_menu({
+ core: this.core_config,
+ plugin: this.plugin_config
+ }));
+ },
+
+ change_section: function(e) {
+ var el = $(e.target).parent();
+ var name = el.data("name");
+ console.log("Section changed to " + name);
+
+ this.menu.find("li.active").removeClass("active");
+ el.addClass("active");
+ e.preventDefault();
+ }
+
+ });
+ }); \ No newline at end of file