diff options
Diffstat (limited to 'pyload/web/app/scripts')
57 files changed, 3795 insertions, 0 deletions
diff --git a/pyload/web/app/scripts/app.js b/pyload/web/app/scripts/app.js new file mode 100644 index 000000000..af5c50b14 --- /dev/null +++ b/pyload/web/app/scripts/app.js @@ -0,0 +1,104 @@ +/* + * Global Application Object + * Contains all necessary logic shared across views + */ +define([ + + // Libraries. + 'jquery', + 'underscore', + 'backbone', + 'handlebars', + 'utils/animations', + 'utils/lazyRequire', + 'utils/dialogs', + 'marionette', + 'bootstrap', + 'animate' + +], function($, _, Backbone, Handlebars) { + 'use strict'; + + Backbone.Marionette.TemplateCache.prototype.compileTemplate = function(rawTemplate) { + return Handlebars.compile(rawTemplate); + }; + + // TODO: configurable root + var App = new Backbone.Marionette.Application({ + root: '/' + }); + + App.addRegions({ + header: '#header', + notification: '#notification-area', + selection: '#selection-area', + content: '#content', + actionbar: '#actionbar' + }); + + App.navigate = function(url) { + return Backbone.history.navigate(url, true); + }; + + App.apiUrl = function(path) { + var url = window.hostProtocol + window.hostAddress + ':' + window.hostPort + window.pathPrefix + path; + return url; + }; + + // Add Global Helper functions + // Generates options dict that can be used for xhr requests + App.apiRequest = function(method, data, options) { + options || (options = {}); + options.url = App.apiUrl('api/' + method); + options.dataType = 'json'; + + if (data) { + options.type = 'POST'; + options.data = {}; + // Convert arguments to json + _.keys(data).map(function(key) { + options.data[key] = JSON.stringify(data[key]); + }); + } + + return options; + }; + + App.setTitle = function(name) { + var title = window.document.title; + var newTitle; + // page name separator + var index = title.indexOf('-'); + if (index >= 0) + newTitle = name + ' - ' + title.substr(index + 2, title.length); + else + newTitle = name + ' - ' + title; + + window.document.title = newTitle; + }; + + App.openWebSocket = function(path) { + return new WebSocket(window.wsAddress.replace('%s', window.hostAddress) + path); + }; + + App.on('initialize:after', function() { +// TODO pushState variable + Backbone.history.start({ + pushState: false, + root: App.root + }); + + // All links should be handled by backbone + $(document).on('click', 'a[data-nav]', function(evt) { + var href = { prop: $(this).prop('href'), attr: $(this).attr('href') }; + var root = location.protocol + '//' + location.host + App.root; + if (href.prop.slice(0, root.length) === root) { + evt.preventDefault(); + Backbone.history.navigate(href.attr, true); + } + }); + }); + + // Returns the app object to be available to other modules through require.js. + return App; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/AccountList.js b/pyload/web/app/scripts/collections/AccountList.js new file mode 100644 index 000000000..bfc2af5a3 --- /dev/null +++ b/pyload/web/app/scripts/collections/AccountList.js @@ -0,0 +1,24 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/Account'], function($, Backbone, _, App, Account) { + 'use strict'; + + return Backbone.Collection.extend({ + + model: Account, + + comparator: function(account) { + return account.get('plugin'); + }, + + initialize: function() { + + }, + + fetch: function(options) { + // TODO: refresh options? + options = App.apiRequest('getAccounts/false', null, options); + return Backbone.Collection.prototype.fetch.call(this, options); + } + + }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/FileList.js b/pyload/web/app/scripts/collections/FileList.js new file mode 100644 index 000000000..112dc5e51 --- /dev/null +++ b/pyload/web/app/scripts/collections/FileList.js @@ -0,0 +1,28 @@ +define(['jquery', 'backbone', 'underscore', 'models/File'], function($, Backbone, _, File) { + 'use strict'; + + return Backbone.Collection.extend({ + + model: File, + + comparator: function(file) { + return file.get('fileorder'); + }, + + isEqual: function(fileList) { + if (this.length !== fileList.length) return false; + + // Assuming same order would be faster in false case + var diff = _.difference(this.models, fileList.models); + + // If there is a difference models are unequal + return diff.length > 0; + }, + + initialize: function() { + + } + + }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/InteractionList.js b/pyload/web/app/scripts/collections/InteractionList.js new file mode 100644 index 000000000..24f8b9248 --- /dev/null +++ b/pyload/web/app/scripts/collections/InteractionList.js @@ -0,0 +1,49 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/InteractionTask'], + function($, Backbone, _, App, InteractionTask) { + 'use strict'; + + return Backbone.Collection.extend({ + + model: InteractionTask, + + comparator: function(task) { + return task.get('iid'); + }, + + fetch: function(options) { + options = App.apiRequest('getInteractionTasks/0', null, options); + var self = this; + options.success = function(data) { + self.set(data); + }; + + return $.ajax(options); + }, + + toJSON: function() { + var data = {queries: 0, notifications: 0}; + + this.map(function(task) { + if (task.isNotification()) + data.notifications++; + else + data.queries++; + }); + + return data; + }, + + // a task is waiting for attention (no notification) + hasTaskWaiting: function() { + var tasks = 0; + this.map(function(task) { + if (!task.isNotification()) + tasks++; + }); + + return tasks > 0; + } + + }); + + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/PackageList.js b/pyload/web/app/scripts/collections/PackageList.js new file mode 100644 index 000000000..7bee861a4 --- /dev/null +++ b/pyload/web/app/scripts/collections/PackageList.js @@ -0,0 +1,16 @@ +define(['jquery', 'backbone', 'underscore', 'models/Package'], function($, Backbone, _, Package) { + 'use strict'; + + return Backbone.Collection.extend({ + + model: Package, + + comparator: function(pack) { + return pack.get('packageorder'); + }, + + initialize: function() { + } + + }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/collections/ProgressList.js b/pyload/web/app/scripts/collections/ProgressList.js new file mode 100644 index 000000000..51849d8de --- /dev/null +++ b/pyload/web/app/scripts/collections/ProgressList.js @@ -0,0 +1,18 @@ +define(['jquery', 'backbone', 'underscore', 'models/Progress'], function($, Backbone, _, Progress) { + 'use strict'; + + return Backbone.Collection.extend({ + + model: Progress, + + comparator: function(progress) { + return progress.get('eta'); + }, + + initialize: function() { + + } + + }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/config.js b/pyload/web/app/scripts/config.js new file mode 100644 index 000000000..9d1d027d9 --- /dev/null +++ b/pyload/web/app/scripts/config.js @@ -0,0 +1,75 @@ +// Sets the require.js configuration for your application. +'use strict'; +require.config({ + + deps: ['default'], + + paths: { + + jquery: '../components/jquery/jquery', + flot: '../components/flot/jquery.flot', + transit: '../components/jquery.transit/jquery.transit', + animate: '../components/jquery.animate-enhanced/scripts/src/jquery.animate-enhanced', + cookie: '../components/jquery.cookie/jquery.cookie', + omniwindow: 'vendor/jquery.omniwindow', + select2: '../components/select2/select2', + bootstrap: '../components/bootstrap-assets/js/bootstrap', + underscore: '../components/underscore/underscore', + backbone: '../components/backbone/backbone', + marionette: '../components/backbone.marionette/lib/backbone.marionette', + handlebars: '../components/handlebars.js/dist/handlebars', + jed: '../components/jed/jed', + + // TODO: Two hbs dependencies could be replaced + i18nprecompile: '../components/require-handlebars-plugin/hbs/i18nprecompile', + json2: '../components/require-handlebars-plugin/hbs/json2', + + // Plugins +// text: '../components/requirejs-text/text', + hbs: '../components/require-handlebars-plugin/hbs', + + // Shortcut + tpl: '../templates/default' + }, + + hbs: { + disableI18n: true, + helperPathCallback: // Callback to determine the path to look for helpers + function(name) { + if (name === '_' || name === 'ngettext') + name = 'gettext'; + + // Some helpers are accumulated into one file + if (name.indexOf('file') === 0) + name = 'fileHelper'; + + return 'helpers/' + name; + }, + templateExtension: 'html' + }, + + // Sets the configuration for your third party scripts that are not AMD compatible + shim: { + underscore: { + exports: '_' + }, + + backbone: { + deps: ['underscore', 'jquery'], + exports: 'Backbone' + }, + + marionette: ['backbone'], + handlebars: { + exports: 'Handlebars' + }, + + flot: ['jquery'], + transit: ['jquery'], + cookie: ['jquery'], + omniwindow: ['jquery'], + select2: ['jquery'], + bootstrap: ['jquery'], + animate: ['jquery'] + } +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/controller.js b/pyload/web/app/scripts/controller.js new file mode 100644 index 000000000..60f604e5b --- /dev/null +++ b/pyload/web/app/scripts/controller.js @@ -0,0 +1,72 @@ +define([ + 'app', + 'backbone', + 'underscore', + + // Views + 'views/headerView', + 'views/notificationView', + 'views/dashboard/dashboardView', + 'views/dashboard/selectionView', + 'views/dashboard/filterView', + 'views/loginView', + 'views/settings/settingsView', + 'views/accounts/accountListView' +], function( + App, Backbone, _, HeaderView, NotificationView, DashboardView, SelectionView, FilterView, LoginView, SettingsView, AccountListView) { + 'use strict'; + // TODO some views does not need to be loaded instantly + + return { + + header: function() { + if (!App.header.currentView) { + App.header.show(new HeaderView()); + App.header.currentView.init(); + App.notification.attachView(new NotificationView()); + } + }, + + dashboard: function() { + this.header(); + + App.actionbar.show(new FilterView()); + + // TODO: not completely visible after reattaching + // now visible every time + if (_.isUndefined(App.selection.currentView) || _.isNull(App.selection.currentView)) + App.selection.attachView(new SelectionView()); + + App.content.show(new DashboardView()); + }, + + login: function() { + App.content.show(new LoginView()); + }, + + logout: function() { + alert('Not implemented'); + }, + + settings: function() { + this.header(); + + var view = new SettingsView(); + App.actionbar.show(new view.actionbar()); + App.content.show(view); + }, + + accounts: function() { + this.header(); + + var view = new AccountListView(); + App.actionbar.show(new view.actionbar()); + App.content.show(view); + }, + + admin: function() { + alert('Not implemented'); + } + }; + +}); diff --git a/pyload/web/app/scripts/default.js b/pyload/web/app/scripts/default.js new file mode 100644 index 000000000..6c5ee9afb --- /dev/null +++ b/pyload/web/app/scripts/default.js @@ -0,0 +1,30 @@ +define('default', ['backbone', 'jquery', 'app', 'router', 'models/UserSession'], + function(Backbone, $, App, Router, UserSession) { + 'use strict'; + + // Global ajax options + var options = { + statusCode: { + 401: function() { + console.log('Not logged in.'); + App.navigate('login'); + } + }, + xhrFields: {withCredentials: true} + }; + + $.ajaxSetup(options); + + Backbone.ajax = function() { + Backbone.$.ajaxSetup.call(Backbone.$, options); + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + $(function() { + App.session = new UserSession(); + App.router = new Router(); + App.start(); + }); + + return App; + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/fileHelper.js b/pyload/web/app/scripts/helpers/fileHelper.js new file mode 100644 index 000000000..156be58f0 --- /dev/null +++ b/pyload/web/app/scripts/helpers/fileHelper.js @@ -0,0 +1,55 @@ +// Helpers to render the file view +define('helpers/fileHelper', ['handlebars', 'utils/apitypes', 'helpers/formatTime'], + function(Handlebars, Api, formatTime) { + 'use strict'; + + function fileClass(file, options) { + if (file.finished) + return 'finished'; + else if (file.failed) + return 'failed'; + else if (file.offline) + return 'offline'; + else if (file.online) + return 'online'; + else if (file.waiting) + return 'waiting'; + else if (file.downloading) + return 'downloading'; + + return ''; + } + + // TODO + function fileIcon(media, options) { + return 'icon-music'; + } + + // TODO rest of the states + function fileStatus(file, options) { + var s; + var msg = file.download.statusmsg; + + if (file.failed) { + s = '<i class="icon-remove"></i> '; + if (file.download.error) + s += file.download.error; + else s += msg; + } else if (file.finished) + s = '<i class="icon-ok"></i> ' + msg; + else if (file.downloading) + s = '<div class="progress"><div class="bar" style="width: ' + file.progress + '%"> ' + + formatTime(file.eta) + '</div></div>'; + else if (file.waiting) + s = '<i class="icon-time"></i> ' + formatTime(file.eta); + else + s = msg; + + return new Handlebars.SafeString(s); + } + + Handlebars.registerHelper('fileClass', fileClass); + Handlebars.registerHelper('fileIcon', fileIcon); + Handlebars.registerHelper('fileStatus', fileStatus); + return fileClass; + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/formatSize.js b/pyload/web/app/scripts/helpers/formatSize.js new file mode 100644 index 000000000..3b62e74c7 --- /dev/null +++ b/pyload/web/app/scripts/helpers/formatSize.js @@ -0,0 +1,15 @@ +// Format bytes in human readable format +define('helpers/formatSize', ['handlebars'], function(Handlebars) { + 'use strict'; + + var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; + function formatSize(bytes, options) { + if (!bytes || bytes === 0) return '0 B'; + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); + // round to two digits + return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; + } + + Handlebars.registerHelper('formatSize', formatSize); + return formatSize; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/formatTime.js b/pyload/web/app/scripts/helpers/formatTime.js new file mode 100644 index 000000000..757ff73ad --- /dev/null +++ b/pyload/web/app/scripts/helpers/formatTime.js @@ -0,0 +1,17 @@ +// Format bytes in human readable format +define('helpers/formatTime', ['handlebars', 'vendor/remaining'], function(Handlebars, Remaining) { + 'use strict'; + + function formatTime(seconds, options) { + if (seconds === Infinity) + return 'â'; + else if (!seconds || seconds <= 0) + return '-'; + + // TODO: digital or written string + return Remaining.getStringDigital(seconds, window.dates); + } + + Handlebars.registerHelper('formatTime', formatTime); + return formatTime; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/gettext.js b/pyload/web/app/scripts/helpers/gettext.js new file mode 100644 index 000000000..d73b5e378 --- /dev/null +++ b/pyload/web/app/scripts/helpers/gettext.js @@ -0,0 +1,16 @@ +require(['underscore', 'handlebars', 'utils/i18n'], function(_, Handlebars, i18n) { + 'use strict'; + // These methods binds additional content directly to translated message + function ngettext(single, plural, n) { + return i18n.sprintf(i18n.ngettext(single, plural, n), n); + } + + function gettext(key, message) { + return i18n.sprintf(i18n.gettext(key), message); + } + + Handlebars.registerHelper('_', gettext); + Handlebars.registerHelper('gettext', gettext); + Handlebars.registerHelper('ngettext', ngettext); + return gettext; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/pluginIcon.js b/pyload/web/app/scripts/helpers/pluginIcon.js new file mode 100644 index 000000000..6b2fdc67f --- /dev/null +++ b/pyload/web/app/scripts/helpers/pluginIcon.js @@ -0,0 +1,14 @@ +// Resolves name of plugin to icon path +define('helpers/pluginIcon', ['handlebars', 'app'], function(Handlebars, App) { + 'use strict'; + + function pluginIcon(name) { + if (typeof name === 'object' && typeof name.get === 'function') + name = name.get('plugin'); + + return App.apiUrl('icons/' + name); + } + + Handlebars.registerHelper('pluginIcon', pluginIcon); + return pluginIcon; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/helpers/truncate.js b/pyload/web/app/scripts/helpers/truncate.js new file mode 100644 index 000000000..fb351b776 --- /dev/null +++ b/pyload/web/app/scripts/helpers/truncate.js @@ -0,0 +1,25 @@ +require(['underscore','handlebars'], function(_, Handlebars) { + 'use strict'; + + function truncate(fullStr, options) { + var strLen = 30; + if (_.isNumber(options)) + strLen = options; + + if (fullStr.length <= strLen) return fullStr; + + var separator = options.separator || 'âŠ'; + + var sepLen = separator.length, + charsToShow = strLen - sepLen, + frontChars = Math.ceil(charsToShow / 2), + backChars = Math.floor(charsToShow / 2); + + return fullStr.substr(0, frontChars) + + separator + + fullStr.substr(fullStr.length - backChars); + } + + Handlebars.registerHelper('truncate', truncate); + return truncate; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/Account.js b/pyload/web/app/scripts/models/Account.js new file mode 100644 index 000000000..a2e24b056 --- /dev/null +++ b/pyload/web/app/scripts/models/Account.js @@ -0,0 +1,51 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], function($, Backbone, _, App, Api) { + 'use strict'; + + return Backbone.Model.extend({ + + // TODO + // generated, not submitted + idAttribute: 'user', + + defaults: { + plugin: null, + loginname: null, + owner: -1, + valid: false, + validuntil: -1, + trafficleft: -1, + maxtraffic: -1, + premium: false, + activated: false, + shared: false, + options: null + }, + + // Model Constructor + initialize: function() { + }, + + // Any time a model attribute is set, this method is called + validate: function(attrs) { + + }, + + save: function(options) { + options = App.apiRequest('updateAccountInfo', {account: this.toJSON()}, options); + return $.ajax(options); + }, + + destroy: function(options) { + options = App.apiRequest('removeAccount', {account: this.toJSON()}, options); + var self = this; + options.success = function() { + self.trigger('destroy', self, self.collection, options); + }; + + // TODO request is not dispatched +// return Backbone.Model.prototype.destroy.call(this, options); + return $.ajax(options); + } + }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/ConfigHolder.js b/pyload/web/app/scripts/models/ConfigHolder.js new file mode 100644 index 000000000..40efbc7c0 --- /dev/null +++ b/pyload/web/app/scripts/models/ConfigHolder.js @@ -0,0 +1,68 @@ +define(['jquery', 'backbone', 'underscore', 'app', './ConfigItem'], + function($, Backbone, _, App, ConfigItem) { + 'use strict'; + + return Backbone.Model.extend({ + + defaults: { + name: '', + label: '', + description: '', + long_description: null, + // simple list but no collection + items: null, + info: null + }, + + // Model Constructor + initialize: function() { + + }, + + // Loads it from server by name + fetch: function(options) { + options = App.apiRequest('loadConfig/"' + this.get('name') + '"', null, options); + return Backbone.Model.prototype.fetch.call(this, options); + }, + + save: function(options) { + var config = this.toJSON(); + var items = []; + // Convert changed items to json + _.each(config.items, function(item) { + if (item.isChanged()) { + items.push(item.prepareSave()); + } + }); + config.items = items; + // TODO: only set new values on success + + options = App.apiRequest('saveConfig', {config: config}, options); + + return $.ajax(options); + }, + + parse: function(resp) { + // Create item models + resp.items = _.map(resp.items, function(item) { + return new ConfigItem(item); + }); + + return Backbone.Model.prototype.parse.call(this, resp); + }, + + isLoaded: function() { + return this.has('items') || this.has('long_description'); + }, + + // check if any of the items has changes + hasChanges: function() { + var items = this.get('items'); + if (!items) return false; + return _.reduce(items, function(a, b) { + return a || b.isChanged(); + }, false); + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/ConfigItem.js b/pyload/web/app/scripts/models/ConfigItem.js new file mode 100644 index 000000000..2d325c2a2 --- /dev/null +++ b/pyload/web/app/scripts/models/ConfigItem.js @@ -0,0 +1,40 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], + function($, Backbone, _, App, Api) { + 'use strict'; + + return Backbone.Model.extend({ + + defaults: { + name: '', + label: '', + description: '', + input: null, + default_value: null, + value: null, + // additional attributes + inputView: null + }, + + // Model Constructor + initialize: function() { + + }, + + isChanged: function() { + return this.get('inputView') && this.get('inputView').getVal() !== this.get('value'); + }, + + // set new value and return json + prepareSave: function() { + // set the new value + if (this.get('inputView')) + this.set('value', this.get('inputView').getVal()); + + var data = this.toJSON(); + delete data.inputView; + delete data.description; + + return data; + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/File.js b/pyload/web/app/scripts/models/File.js new file mode 100644 index 000000000..562e6b0ae --- /dev/null +++ b/pyload/web/app/scripts/models/File.js @@ -0,0 +1,97 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], function($, Backbone, _, App, Api) { + 'use strict'; + + var Finished = [Api.DownloadStatus.Finished, Api.DownloadStatus.Skipped]; + var Failed = [Api.DownloadStatus.Failed, Api.DownloadStatus.Aborted, Api.DownloadStatus.TempOffline, Api.DownloadStatus.Offline]; + // Unfinished - Other + + return Backbone.Model.extend({ + + idAttribute: 'fid', + + defaults: { + fid: -1, + name: null, + package: -1, + owner: -1, + size: -1, + status: -1, + media: -1, + added: -1, + fileorder: -1, + download: null, + + // UI attributes + selected: false, + visible: true, + progress: 0, + eta: 0 + }, + + // Model Constructor + initialize: function() { + + }, + + fetch: function(options) { + options = App.apiRequest( + 'getFileInfo', + {fid: this.get('fid')}, + options); + + return Backbone.Model.prototype.fetch.call(this, options); + }, + + destroy: function(options) { + // also not working when using data + options = App.apiRequest( + 'deleteFiles/[' + this.get('fid') + ']', + null, options); + options.method = 'post'; + + return Backbone.Model.prototype.destroy.call(this, options); + }, + + // Does not send a request to the server + destroyLocal: function(options) { + this.trigger('destroy', this, this.collection, options); + }, + + restart: function(options) { + options = App.apiRequest( + 'restartFile', + {fid: this.get('fid')}, + options); + + return $.ajax(options); + }, + + // Any time a model attribute is set, this method is called + validate: function(attrs) { + + }, + + setDownloadStatus: function(status) { + if (this.isDownload()) + this.get('download').status = status; + }, + + isDownload: function() { + return this.has('download'); + }, + + isFinished: function() { + return _.indexOf(Finished, this.get('download').status) > -1; + }, + + isUnfinished: function() { + return _.indexOf(Finished, this.get('download').status) === -1 && _.indexOf(Failed, this.get('download').status) === -1; + }, + + isFailed: function() { + return _.indexOf(Failed, this.get('download').status) > -1; + } + + }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/InteractionTask.js b/pyload/web/app/scripts/models/InteractionTask.js new file mode 100644 index 000000000..54c739d4b --- /dev/null +++ b/pyload/web/app/scripts/models/InteractionTask.js @@ -0,0 +1,41 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'], + function($, Backbone, _, App, Api) { + 'use strict'; + + return Backbone.Model.extend({ + + idAttribute: 'iid', + + defaults: { + iid: -1, + type: null, + input: null, + default_value: null, + title: '', + description: '', + plugin: '', + // additional attributes + result: '' + }, + + // Model Constructor + initialize: function() { + + }, + + save: function(options) { + options = App.apiRequest('setInteractionResult/' + this.get('iid'), + {result: this.get('result')}, options); + + return $.ajax(options); + }, + + isNotification: function() { + return this.get('type') === Api.Interaction.Notification; + }, + + isCaptcha: function() { + return this.get('type') === Api.Interaction.Captcha; + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/Package.js b/pyload/web/app/scripts/models/Package.js new file mode 100644 index 000000000..a34ec1c69 --- /dev/null +++ b/pyload/web/app/scripts/models/Package.js @@ -0,0 +1,119 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'collections/FileList', 'require'], + function($, Backbone, _, App, FileList, require) { + 'use strict'; + + return Backbone.Model.extend({ + + idAttribute: 'pid', + + defaults: { + pid: -1, + name: null, + folder: '', + root: -1, + owner: -1, + site: '', + comment: '', + password: '', + added: -1, + tags: null, + status: -1, + shared: false, + packageorder: -1, + stats: null, + fids: null, + pids: null, + files: null, // Collection + packs: null, // Collection + + selected: false // For Checkbox + }, + + // Model Constructor + initialize: function() { + }, + + toJSON: function(options) { + var obj = Backbone.Model.prototype.toJSON.call(this, options); + obj.percent = Math.round(obj.stats.linksdone * 100 / obj.stats.linkstotal); + + return obj; + }, + + // Changes url + method and delegates call to super class + fetch: function(options) { + options = App.apiRequest( + 'getFileTree/' + this.get('pid'), + {full: false}, + options); + + return Backbone.Model.prototype.fetch.call(this, options); + }, + + // Create a pseudo package und use search to populate data + search: function(qry, options) { + options = App.apiRequest( + 'findFiles', + {pattern: qry}, + options); + + return Backbone.Model.prototype.fetch.call(this, options); + }, + + save: function(options) { + // TODO + }, + + destroy: function(options) { + // TODO: Not working when using data?, array seems to break it + options = App.apiRequest( + 'deletePackages/[' + this.get('pid') + ']', + null, options); + options.method = 'post'; + + console.log(options); + + return Backbone.Model.prototype.destroy.call(this, options); + }, + + restart: function(options) { + options = App.apiRequest( + 'restartPackage', + {pid: this.get('pid')}, + options); + + var self = this; + options.success = function() { + self.fetch(); + }; + return $.ajax(options); + }, + + parse: function(resp) { + // Package is loaded from tree collection + if (_.has(resp, 'root')) { + if (!this.has('files')) + resp.root.files = new FileList(_.values(resp.files)); + else + this.get('files').set(_.values(resp.files)); + + // circular dependencies needs to be avoided + var PackageList = require('collections/PackageList'); + + if (!this.has('packs')) + resp.root.packs = new PackageList(_.values(resp.packages)); + else + this.get('packs').set(_.values(resp.packages)); + + return resp.root; + } + return Backbone.model.prototype.parse.call(this, resp); + }, + + // Any time a model attribute is set, this method is called + validate: function(attrs) { + + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/Progress.js b/pyload/web/app/scripts/models/Progress.js new file mode 100644 index 000000000..b0bbb684d --- /dev/null +++ b/pyload/web/app/scripts/models/Progress.js @@ -0,0 +1,50 @@ +define(['jquery', 'backbone', 'underscore', 'utils/apitypes'], function($, Backbone, _, Api) { + 'use strict'; + + return Backbone.Model.extend({ + + // generated, not submitted + idAttribute: 'pid', + + defaults: { + pid: -1, + plugin: null, + name: null, + statusmsg: -1, + eta: -1, + done: -1, + total: -1, + download: null + }, + + getPercent: function() { + if (this.get('total') > 0) + return Math.round(this.get('done') * 100 / this.get('total')); + return 0; + }, + + // Model Constructor + initialize: function() { + + }, + + // Any time a model attribute is set, this method is called + validate: function(attrs) { + + }, + + toJSON: function(options) { + var obj = Backbone.Model.prototype.toJSON.call(this, options); + obj.percent = this.getPercent(); + obj.downloading = this.isDownload() && this.get('download').status === Api.DownloadStatus.Downloading; + + return obj; + }, + + isDownload : function() { + return this.has('download'); + } + + }); + +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/ServerStatus.js b/pyload/web/app/scripts/models/ServerStatus.js new file mode 100644 index 000000000..59739b41e --- /dev/null +++ b/pyload/web/app/scripts/models/ServerStatus.js @@ -0,0 +1,47 @@ +define(['jquery', 'backbone', 'underscore'], + function($, Backbone, _) { + 'use strict'; + + return Backbone.Model.extend({ + + defaults: { + speed: 0, + linkstotal: 0, + linksqueue: 0, + sizetotal: 0, + sizequeue: 0, + notifications: -1, + paused: false, + download: false, + reconnect: false + }, + + // Model Constructor + initialize: function() { + + }, + + fetch: function(options) { + options || (options = {}); + options.url = 'api/getServerStatus'; + + return Backbone.Model.prototype.fetch.call(this, options); + }, + + toJSON: function(options) { + var obj = Backbone.Model.prototype.toJSON.call(this, options); + + obj.linksdone = obj.linkstotal - obj.linksqueue; + obj.sizedone = obj.sizetotal - obj.sizequeue; + if (obj.speed && obj.speed > 0) + obj.eta = Math.round(obj.sizequeue / obj.speed); + else if (obj.sizequeue > 0) + obj.eta = Infinity; + else + obj.eta = 0; + + return obj; + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/TreeCollection.js b/pyload/web/app/scripts/models/TreeCollection.js new file mode 100644 index 000000000..2f761e6cc --- /dev/null +++ b/pyload/web/app/scripts/models/TreeCollection.js @@ -0,0 +1,50 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/Package', 'collections/FileList', 'collections/PackageList'], + function($, Backbone, _, App, Package, FileList, PackageList) { + 'use strict'; + + // TreeCollection + // A Model and not a collection, aggregates other collections + return Backbone.Model.extend({ + + defaults: { + root: null, + packages: null, + files: null + }, + + initialize: function() { + + }, + + fetch: function(options) { + options || (options = {}); + var pid = options.pid || -1; + + options = App.apiRequest( + 'getFileTree/' + pid, + {full: false}, + options); + + console.log('Fetching package tree ' + pid); + return Backbone.Model.prototype.fetch.call(this, options); + }, + + // Parse the response and updates the collections + parse: function(resp) { + var ret = {}; + if (!this.has('packages')) + ret.packages = new PackageList(_.values(resp.packages)); + else + this.get('packages').set(_.values(resp.packages)); + + if (!this.has('files')) + ret.files = new FileList(_.values(resp.files)); + else + this.get('files').set(_.values(resp.files)); + + ret.root = new Package(resp.root); + return ret; + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/models/UserSession.js b/pyload/web/app/scripts/models/UserSession.js new file mode 100644 index 000000000..a7e9aa848 --- /dev/null +++ b/pyload/web/app/scripts/models/UserSession.js @@ -0,0 +1,20 @@ +define(['jquery', 'backbone', 'underscore', 'utils/apitypes', 'cookie'], + function($, Backbone, _, Api) { + 'use strict'; + + return Backbone.Model.extend({ + + idAttribute: 'username', + + defaults: { + username: null, + permissions: null, + session: null + }, + + // Model Constructor + initialize: function() { + this.set('session', $.cookie('beaker.session.id')); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/router.js b/pyload/web/app/scripts/router.js new file mode 100644 index 000000000..68ea5575d --- /dev/null +++ b/pyload/web/app/scripts/router.js @@ -0,0 +1,29 @@ +/** + * Router defines routes that are handled by registered controller + */ +define([ + // Libraries + 'backbone', + 'marionette', + + // Modules + 'controller' +], + function(Backbone, Marionette, Controller) { + 'use strict'; + + return Backbone.Marionette.AppRouter.extend({ + + appRoutes: { + '': 'dashboard', + 'login': 'login', + 'logout': 'logout', + 'settings': 'settings', + 'accounts': 'accounts', + 'admin': 'admin' + }, + + // Our controller to handle the routes + controller: Controller + }); + }); diff --git a/pyload/web/app/scripts/routers/defaultRouter.js b/pyload/web/app/scripts/routers/defaultRouter.js new file mode 100644 index 000000000..4b00d160c --- /dev/null +++ b/pyload/web/app/scripts/routers/defaultRouter.js @@ -0,0 +1,30 @@ +define(['jquery', 'backbone', 'views/headerView'], function($, Backbone, HeaderView) { + 'use strict'; + + var Router = Backbone.Router.extend({ + + initialize: function() { + Backbone.history.start(); + }, + + // All of your Backbone Routes (add more) + routes: { + + // When there is no hash bang on the url, the home method is called + '': 'home' + + }, + + 'home': function() { + // Instantiating mainView and anotherView instances + var headerView = new HeaderView(); + + // Renders the mainView template + headerView.render(); + + } + }); + + // Returns the Router class + return Router; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/routers/mobileRouter.js b/pyload/web/app/scripts/routers/mobileRouter.js new file mode 100644 index 000000000..e24cb7a34 --- /dev/null +++ b/pyload/web/app/scripts/routers/mobileRouter.js @@ -0,0 +1,56 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { + 'use strict'; + + return Backbone.Router.extend({ + + initialize: function() { + _.bindAll(this, 'changePage'); + + this.$el = $('#content'); + + // Tells Backbone to start watching for hashchange events + Backbone.history.start(); + + }, + + // All of your Backbone Routes (add more) + routes: { + + // When there is no hash bang on the url, the home method is called + '': 'home' + + }, + + 'home': function() { + + var self = this; + + $('#p1').fastClick(function() { + self.changePage($('<div class=\'page\' style=\'background-color: #9acd32;\'><h1>Page 1</h1><br>some content<br>sdfdsf<br>sdffg<h3>oiuzz</h3></div>')); + }); + + $('#p2').bind('click', function() { + self.changePage($('<div class=\'page\' style=\'background-color: blue;\'><h1>Page 2</h1><br>some content<br>sdfdsf<br><h2>sdfsdf</h2>sdffg</div>')); + }); + + }, + + changePage: function(content) { + + var oldpage = this.$el.find('.page'); + content.css({x: '100%'}); + this.$el.append(content); + content.transition({x: 0}, function() { + window.setTimeout(function() { + oldpage.remove(); + }, 400); + }); + +// $("#viewport").transition({x: "100%"}, function(){ +// $("#viewport").html(content); +// $("#viewport").transition({x: 0}); +// }); + } + + }); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/animations.js b/pyload/web/app/scripts/utils/animations.js new file mode 100644 index 000000000..7f89afef1 --- /dev/null +++ b/pyload/web/app/scripts/utils/animations.js @@ -0,0 +1,129 @@ +define(['jquery', 'underscore', 'transit'], function(jQuery, _) { + 'use strict'; + + // Adds an element and computes its height, which is saved as data attribute + // Important function to have slide animations + jQuery.fn.appendWithHeight = function(element, hide) { + var o = jQuery(this[0]); + element = jQuery(element); + + // TODO: additionally it could be placed out of viewport first + // The real height can only be retrieved when element is on DOM and display:true + element.css('visibility', 'hidden'); + o.append(element); + + var height = element.height(); + + // Hide the element + if (hide === true) { + element.hide(); + element.height(0); + } + + element.css('visibility', ''); + element.data('height', height); + + return this; + }; + + // Shortcut to have a animation when element is added + jQuery.fn.appendWithAnimation = function(element, animation) { + var o = jQuery(this[0]); + element = jQuery(element); + + if (animation === true) + element.hide(); + + o.append(element); + + if (animation === true) + element.fadeIn(); + +// element.calculateHeight(); + + return this; + }; + + // calculate the height and write it to data, should be used on invisible elements + jQuery.fn.calculateHeight = function(setHeight) { + var o = jQuery(this[0]); + var height = o.height(); + if (!height) { + var display = o.css('display'); + o.css('visibility', 'hidden'); + o.show(); + height = o.height(); + + o.css('display', display); + o.css('visibility', ''); + } + + if (setHeight) + o.css('height', height); + + o.data('height', height); + return this; + }; + + // TODO: carry arguments, optional height argument + + // reset arguments, sets overflow hidden + jQuery.fn.slideOut = function(reset) { + var o = jQuery(this[0]); + o.animate({height: o.data('height'), opacity: 'show'}, function() { + // reset css attributes; + if (reset) { + this.css('overflow', ''); + this.css('height', ''); + } + }); + return this; + }; + + jQuery.fn.slideIn = function(reset) { + var o = jQuery(this[0]); + if (reset) { + o.css('overflow', 'hidden'); + } + o.animate({height: 0, opacity: 'hide'}); + return this; + }; + + jQuery.fn.initTooltips = function(placement) { + placement || (placement = 'top'); + + var o = jQuery(this[0]); + o.find('[data-toggle="tooltip"]').tooltip( + { + delay: {show: 800, hide: 100}, + placement: placement + }); + + return this; + }; + + jQuery.fn._transit = jQuery.fn.transit; + + // Overriding transit plugin to support hide and show + jQuery.fn.transit = jQuery.fn.transition = function(props, duration, easing, callback) { + var self = this; + var cb = callback; + var newprops = _.extend({}, props); + + if (newprops && (newprops.opacity === 'hide')) { + newprops.opacity = 0; + + callback = function() { + self.css({display: 'none'}); + if (typeof cb === 'function') { + cb.apply(self); + } + }; + } else if (newprops && (newprops.opacity === 'show')) { + newprops.opacity = 1; + this.css({display: 'block'}); + } + + return this._transit(newprops, duration, easing, callback); + }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/apitypes.js b/pyload/web/app/scripts/utils/apitypes.js new file mode 100644 index 000000000..342f61f68 --- /dev/null +++ b/pyload/web/app/scripts/utils/apitypes.js @@ -0,0 +1,16 @@ +// Autogenerated, do not edit! +/*jslint -W070: false*/ +define([], function() { + 'use strict'; + return { + DownloadState: {'Failed': 3, 'All': 0, 'Unmanaged': 4, 'Finished': 1, 'Unfinished': 2}, + DownloadStatus: {'Downloading': 10, 'NA': 0, 'Processing': 14, 'Waiting': 9, 'Decrypting': 13, 'Paused': 4, 'Failed': 7, 'Finished': 5, 'Skipped': 6, 'Unknown': 16, 'Aborted': 12, 'Online': 2, 'TempOffline': 11, 'Offline': 1, 'Custom': 15, 'Starting': 8, 'Queued': 3}, + FileStatus: {'Remote': 2, 'Ok': 0, 'Missing': 1}, + InputType: {'PluginList': 13, 'Multiple': 11, 'Int': 2, 'NA': 0, 'Time': 7, 'List': 12, 'Bool': 8, 'File': 3, 'Text': 1, 'Table': 14, 'Folder': 4, 'Password': 6, 'Click': 9, 'Select': 10, 'Textbox': 5}, + Interaction: {'Captcha': 2, 'All': 0, 'Query': 4, 'Notification': 1}, + MediaType: {'All': 0, 'Audio': 2, 'Image': 4, 'Other': 1, 'Video': 8, 'Document': 16, 'Archive': 32}, + PackageStatus: {'Paused': 1, 'Remote': 3, 'Folder': 2, 'Ok': 0}, + Permission: {'All': 0, 'Interaction': 32, 'Modify': 4, 'Add': 1, 'Accounts': 16, 'Plugins': 64, 'Download': 8, 'Delete': 2}, + Role: {'Admin': 0, 'User': 1}, + }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/dialogs.js b/pyload/web/app/scripts/utils/dialogs.js new file mode 100644 index 000000000..3ceffc9c3 --- /dev/null +++ b/pyload/web/app/scripts/utils/dialogs.js @@ -0,0 +1,15 @@ +// Loads all helper and set own handlebars rules +define(['jquery', 'underscore', 'views/abstract/modalView'], function($, _, Modal) { + 'use strict'; + + // Shows the confirm dialog for given context + // on success executes func with context + _.confirm = function(template, func, context) { + template = 'hbs!tpl/' + template; + _.requireOnce([template], function(html) { + var dialog = new Modal(html, _.bind(func, context)); + dialog.show(); + }); + + }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/i18n.js b/pyload/web/app/scripts/utils/i18n.js new file mode 100644 index 000000000..a8d948b4a --- /dev/null +++ b/pyload/web/app/scripts/utils/i18n.js @@ -0,0 +1,5 @@ +define(['jed'], function(Jed) { + 'use strict'; + // TODO load i18n data + return new Jed({}); +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/lazyRequire.js b/pyload/web/app/scripts/utils/lazyRequire.js new file mode 100644 index 000000000..96c07aa24 --- /dev/null +++ b/pyload/web/app/scripts/utils/lazyRequire.js @@ -0,0 +1,97 @@ +// Define the module. +define( + [ + 'require', 'underscore' + ], + function( require, _ ){ + 'use strict'; + + + // Define the states of loading for a given set of modules + // within a require() statement. + var states = { + unloaded: 'UNLOADED', + loading: 'LOADING', + loaded: 'LOADED' + }; + + + // Define the top-level module container. Mostly, we're making + // the top-level container a non-Function so that users won't + // try to invoke this without calling the once() method below. + var lazyRequire = {}; + + + // I will return a new, unique instance of the requrieOnce() + // method. Each instance will only call the require() method + // once internally. + lazyRequire.once = function(){ + + // The modules start in an unloaded state before + // requireOnce() is invoked by the calling code. + var state = states.unloaded; + var args; + + var requireOnce = function(dependencies, loadCallback ){ + + // Use the module state to determine which method to + // invoke (or just to ignore the invocation). + if (state === states.loaded){ + loadCallback.apply(null, args); + + // The modules have not yet been requested - let's + // lazy load them. + } else if (state !== states.loading){ + + // We're about to load the modules asynchronously; + // flag the interim state. + state = states.loading; + + // Load the modules. + require( + dependencies, + function(){ + + args = arguments; + loadCallback.apply( null, args ); + state = states.loaded; + + + } + ); + + // RequireJS is currently loading the modules + // asynchronously, but they have not finished + // loading yet. + } else { + + // Simply ignore this call. + return; + + } + + }; + + // Return the new lazy loader. + return( requireOnce ); + + }; + + + // -------------------------------------------------- // + // -------------------------------------------------- // + + // Set up holder for underscore + var instances = {}; + _.requireOnce = function(dependencies, loadCallback) { + if (!_.has(instances, dependencies)) + instances[dependencies] = lazyRequire.once(); + + return instances[dependencies](dependencies, loadCallback); + }; + + + // Return the module definition. + return( lazyRequire ); + } +);
\ No newline at end of file diff --git a/pyload/web/app/scripts/vendor/jquery.omniwindow.js b/pyload/web/app/scripts/vendor/jquery.omniwindow.js new file mode 100644 index 000000000..e1f0b8f77 --- /dev/null +++ b/pyload/web/app/scripts/vendor/jquery.omniwindow.js @@ -0,0 +1,141 @@ +// jQuery OmniWindow plugin +// @version: 0.7.0 +// @author: Rudenka Alexander (mur.mailbox@gmail.com) +// @license: MIT + +;(function($) { + "use strict"; + $.fn.extend({ + omniWindow: function(options) { + + options = $.extend(true, { + animationsPriority: { + show: ['overlay', 'modal'], + hide: ['modal', 'overlay'] + }, + overlay: { + selector: '.ow-overlay', + hideClass: 'ow-closed', + animations: { + show: function(subjects, internalCallback) { return internalCallback(subjects); }, + hide: function(subjects, internalCallback) { return internalCallback(subjects); }, + internal: { + show: function(subjects){ subjects.overlay.removeClass(options.overlay.hideClass); }, + hide: function(subjects){ subjects.overlay.addClass(options.overlay.hideClass); } + } + } + }, + modal: { + hideClass: 'ow-closed', + animations: { + show: function(subjects, internalCallback) { return internalCallback(subjects); }, + hide: function(subjects, internalCallback) { return internalCallback(subjects); }, + internal: { + show: function(subjects){ subjects.modal.removeClass(options.modal.hideClass); }, + hide: function(subjects){ subjects.modal.addClass(options.modal.hideClass); } + } + }, + internal: { + stateAttribute: 'ow-active' + } + }, + eventsNames: { + show: 'show.ow', + hide: 'hide.ow', + internal: { + overlayClick: 'click.ow', + keyboardKeyUp: 'keyup.ow' + } + }, + callbacks: { // Callbacks execution chain + beforeShow: function(subjects, internalCallback) { return internalCallback(subjects); }, // 1 (stop if retruns false) + positioning: function(subjects, internalCallback) { return internalCallback(subjects); }, // 2 + afterShow: function(subjects, internalCallback) { return internalCallback(subjects); }, // 3 + beforeHide: function(subjects, internalCallback) { return internalCallback(subjects); }, // 4 (stop if retruns false) + afterHide: function(subjects, internalCallback) { return internalCallback(subjects); }, // 5 + internal: { + beforeShow: function(subjects) { + if (subjects.modal.data(options.modal.internal.stateAttribute)) { + return false; + } else { + subjects.modal.data(options.modal.internal.stateAttribute, true); + return true; + } + }, + afterShow: function(subjects) { + $(document).on(options.eventsNames.internal.keyboardKeyUp, function(e) { + if (e.keyCode === 27) { // if the key pressed is the ESC key + subjects.modal.trigger(options.eventsNames.hide); + } + }); + + subjects.overlay.on(options.eventsNames.internal.overlayClick, function(){ + subjects.modal.trigger(options.eventsNames.hide); + }); + }, + positioning: function(subjects) { + subjects.modal.css('margin-left', Math.round(subjects.modal.outerWidth() / -2)); + }, + beforeHide: function(subjects) { + if (subjects.modal.data(options.modal.internal.stateAttribute)) { + subjects.modal.data(options.modal.internal.stateAttribute, false); + return true; + } else { + return false; + } + }, + afterHide: function(subjects) { + subjects.overlay.off(options.eventsNames.internal.overlayClick); + $(document).off(options.eventsNames.internal.keyboardKeyUp); + + subjects.overlay.css('display', ''); // clear inline styles after jQ animations + subjects.modal.css('display', ''); + } + } + } + }, options); + + var animate = function(process, subjects, callbackName) { + var first = options.animationsPriority[process][0], + second = options.animationsPriority[process][1]; + + options[first].animations[process](subjects, function(subjs) { // call USER's FIRST animation (depends on priority) + options[first].animations.internal[process](subjs); // call internal FIRST animation + + options[second].animations[process](subjects, function(subjs) { // call USER's SECOND animation + options[second].animations.internal[process](subjs); // call internal SECOND animation + + // then we need to call USER's + // afterShow of afterHide callback + options.callbacks[callbackName](subjects, options.callbacks.internal[callbackName]); + }); + }); + }; + + var showModal = function(subjects) { + if (!options.callbacks.beforeShow(subjects, options.callbacks.internal.beforeShow)) { return; } // cancel showing if beforeShow callback return false + + options.callbacks.positioning(subjects, options.callbacks.internal.positioning); + + animate('show', subjects, 'afterShow'); + }; + + var hideModal = function(subjects) { + if (!options.callbacks.beforeHide(subjects, options.callbacks.internal.beforeHide)) { return; } // cancel hiding if beforeHide callback return false + + animate('hide', subjects, 'afterHide'); + }; + + + var $overlay = $(options.overlay.selector); + + return this.each(function() { + var $modal = $(this); + var subjects = {modal: $modal, overlay: $overlay}; + + $modal.bind(options.eventsNames.show, function(){ showModal(subjects); }) + .bind(options.eventsNames.hide, function(){ hideModal(subjects); }); + }); + } + }); +})(jQuery);
\ No newline at end of file diff --git a/pyload/web/app/scripts/vendor/remaining.js b/pyload/web/app/scripts/vendor/remaining.js new file mode 100644 index 000000000..d66a2931a --- /dev/null +++ b/pyload/web/app/scripts/vendor/remaining.js @@ -0,0 +1,149 @@ +/** + * Javascript Countdown + * Copyright (c) 2009 Markus Hedlund + * Version 1.1 + * Licensed under MIT license + * http://www.opensource.org/licenses/mit-license.php + * http://labs.mimmin.com/countdown + */ +define([], function() { + var remaining = { + /** + * Get the difference of the passed date, and now. The different formats of the taget parameter are: + * January 12, 2009 15:14:00 (Month dd, yyyy hh:mm:ss) + * January 12, 2009 (Month dd, yyyy) + * 09,00,12,15,14,00 (yy,mm,dd,hh,mm,ss) Months range from 0-11, not 1-12. + * 09,00,12 (yy,mm,dd) Months range from 0-11, not 1-12. + * 500 (milliseconds) + * 2009-01-12 15:14:00 (yyyy-mm-dd hh-mm-ss) + * 2009-01-12 15:14 (yyyy-mm-dd hh-mm) + * @param target Target date. Can be either a date object or a string (formated like '24 December, 2010 15:00:00') + * @return Difference in seconds + */ + getSeconds: function(target) { + var today = new Date(); + + if (typeof(target) == 'object') { + var targetDate = target; + } else { + var matches = target.match(/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(:(\d{2}))?/); // YYYY-MM-DD HH-MM-SS + if (matches != null) { + matches[7] = typeof(matches[7]) == 'undefined' ? '00' : matches[7]; + var targetDate = new Date(matches[1], matches[2] - 1, matches[3], matches[4], matches[5], matches[7]); + } else { + var targetDate = new Date(target); + } + } + + return Math.floor((targetDate.getTime() - today.getTime()) / 1000); + }, + + /** + * @param seconds Difference in seconds + * @param i18n A language object (see code) + * @param onlyLargestUnit Return only the largest unit (see documentation) + * @param hideEmpty Hide empty units (see documentation) + * @return String formated something like '1 week, 1 hours, 1 second' + */ + getString: function(seconds, i18n, onlyLargestUnit, hideEmpty) { + if (seconds < 1) { + return ''; + } + + if (typeof(hideEmpty) == 'undefined' || hideEmpty == null) { + hideEmpty = true; + } + if (typeof(onlyLargestUnit) == 'undefined' || onlyLargestUnit == null) { + onlyLargestUnit = false; + } + if (typeof(i18n) == 'undefined' || i18n == null) { + i18n = { + weeks: ['week', 'weeks'], + days: ['day', 'days'], + hours: ['hour', 'hours'], + minutes: ['minute', 'minutes'], + seconds: ['second', 'seconds'] + }; + } + + var units = { + weeks: 7 * 24 * 60 * 60, + days: 24 * 60 * 60, + hours: 60 * 60, + minutes: 60, + seconds: 1 + }; + + var returnArray = []; + var value; + for (unit in units) { + value = units[unit]; + if (seconds / value >= 1 || unit == 'seconds' || !hideEmpty) { + secondsConverted = Math.floor(seconds / value); + var i18nUnit = i18n[unit][secondsConverted == 1 ? 0 : 1]; + returnArray.push(secondsConverted + ' ' + i18nUnit); + seconds -= secondsConverted * value; + + if (onlyLargestUnit) { + break; + } + } + } + ; + + return returnArray.join(', '); + }, + + /** + * @param seconds Difference in seconds + * @return String formated something like '169:00:01' + */ + getStringDigital: function(seconds) { + if (seconds < 1) { + return ''; + } + + remainingTime = remaining.getArray(seconds); + + for (index in remainingTime) { + remainingTime[index] = remaining.padNumber(remainingTime[index]); + } + ; + + return remainingTime.join(':'); + }, + + /** + * @param seconds Difference in seconds + * @return Array with hours, minutes and seconds + */ + getArray: function(seconds) { + if (seconds < 1) { + return []; + } + + var units = [60 * 60, 60, 1]; + + var returnArray = []; + var value; + for (index in units) { + value = units[index]; + secondsConverted = Math.floor(seconds / value); + returnArray.push(secondsConverted); + seconds -= secondsConverted * value; + } + ; + + return returnArray; + }, + + /** + * @param number An integer + * @return Integer padded with a 0 if necessary + */ + padNumber: function(number) { + return (number >= 0 && number < 10) ? '0' + number : number; + } + }; + return remaining; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/abstract/itemView.js b/pyload/web/app/scripts/views/abstract/itemView.js new file mode 100644 index 000000000..c37118a4c --- /dev/null +++ b/pyload/web/app/scripts/views/abstract/itemView.js @@ -0,0 +1,47 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { + 'use strict'; + + // 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/pyload/web/app/scripts/views/abstract/modalView.js b/pyload/web/app/scripts/views/abstract/modalView.js new file mode 100644 index 000000000..65bc0a3c8 --- /dev/null +++ b/pyload/web/app/scripts/views/abstract/modalView.js @@ -0,0 +1,124 @@ +define(['jquery', 'backbone', 'underscore', 'omniwindow'], function($, Backbone, _) { + 'use strict'; + + 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(['hbs!tpl/dialogs/modal'], function(template) { + self.template = template; + }); + } + }, + + // TODO: whole modal stuff is not very elegant + render: function() { + this.$el.html(this.template(this.renderContent())); + 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); + }}}, + 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 {}; + }, + + show: function() { + if (this.dialog === null) + this.render(); + + this.dialog.trigger('show'); + + this.onShow(); + }, + + onShow: function() { + + }, + + hide: function() { + this.dialog.trigger('hide'); + }, + + 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/pyload/web/app/scripts/views/accounts/accountListView.js b/pyload/web/app/scripts/views/accounts/accountListView.js new file mode 100644 index 000000000..4eb5bfe7d --- /dev/null +++ b/pyload/web/app/scripts/views/accounts/accountListView.js @@ -0,0 +1,52 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'collections/AccountList', './accountView', + 'hbs!tpl/accounts/layout', 'hbs!tpl/accounts/actionbar'], + function($, _, Backbone, App, AccountList, accountView, template, templateBar) { + 'use strict'; + + // Renders settings over view page + return Backbone.Marionette.CollectionView.extend({ + + itemView: accountView, + template: template, + + collection: null, + modal: null, + + initialize: function() { + this.actionbar = Backbone.Marionette.ItemView.extend({ + template: templateBar, + events: { + 'click .btn': 'addAccount' + }, + addAccount: _.bind(this.addAccount, this) + }); + + this.collection = new AccountList(); + this.update(); + + this.listenTo(App.vent, 'accounts:updated', this.update); + }, + + update: function() { + this.collection.fetch(); + }, + + onBeforeRender: function() { + this.$el.html(template()); + }, + + appendHtml: function(collectionView, itemView, index) { + this.$('.account-list').append(itemView.el); + }, + + addAccount: function() { + var self = this; + _.requireOnce(['views/accounts/accountModal'], function(Modal) { + if (self.modal === null) + self.modal = new Modal(); + + self.modal.show(); + }); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/accounts/accountModal.js b/pyload/web/app/scripts/views/accounts/accountModal.js new file mode 100644 index 000000000..6c2b226df --- /dev/null +++ b/pyload/web/app/scripts/views/accounts/accountModal.js @@ -0,0 +1,72 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dialogs/addAccount', 'helpers/pluginIcon', 'select2'], + function($, _, App, modalView, template, pluginIcon) { + 'use strict'; + return modalView.extend({ + + events: { + 'submit form': 'add', + 'click .btn-add': 'add' + }, + template: template, + plugins: null, + select: null, + + initialize: function() { + // Inherit parent events + this.events = _.extend({}, modalView.prototype.events, this.events); + var self = this; + $.ajax(App.apiRequest('getAccountTypes', null, {success: function(data) { + self.plugins = _.sortBy(data, function(item) { + return item; + }); + self.render(); + }})); + }, + + onRender: function() { + // TODO: could be a separate input type if needed on multiple pages + if (this.plugins) + this.select = this.$('#pluginSelect').select2({ + escapeMarkup: function(m) { + return m; + }, + formatResult: this.format, + formatSelection: this.format, + data: {results: this.plugins, text: function(item) { + return item; + }}, + id: function(item) { + return item; + } + }); + }, + + onShow: function() { + }, + + onHide: function() { + }, + + format: function(data) { + return '<img class="logo-select" src="' + pluginIcon(data) + '"> ' + data; + }, + + add: function(e) { + e.stopPropagation(); + if (this.select) { + var plugin = this.select.val(), + login = this.$('#login').val(), + password = this.$('#password').val(), + self = this; + + $.ajax(App.apiRequest('updateAccount', { + plugin: plugin, login: login, password: password + }, { success: function() { + App.vent.trigger('accounts:updated'); + self.hide(); + }})); + } + return false; + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/accounts/accountView.js b/pyload/web/app/scripts/views/accounts/accountView.js new file mode 100644 index 000000000..89f69d7e7 --- /dev/null +++ b/pyload/web/app/scripts/views/accounts/accountView.js @@ -0,0 +1,18 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'hbs!tpl/accounts/account'], + function($, _, Backbone, App, template) { + 'use strict'; + + return Backbone.Marionette.ItemView.extend({ + + tagName: 'tr', + template: template, + + events: { + 'click .btn-danger': 'deleteAccount' + }, + + deleteAccount: function() { + this.model.destroy(); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/dashboardView.js b/pyload/web/app/scripts/views/dashboard/dashboardView.js new file mode 100644 index 000000000..8a0446203 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/dashboardView.js @@ -0,0 +1,172 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', 'collections/FileList', + './packageView', './fileView', 'hbs!tpl/dashboard/layout', 'select2'], + function($, Backbone, _, App, TreeCollection, FileList, PackageView, FileView, template) { + 'use strict'; + // Renders whole dashboard + return Backbone.Marionette.ItemView.extend({ + + template: template, + + events: { + }, + + ui: { + 'packages': '.package-list', + 'files': '.file-list' + }, + + // Package tree + tree: null, + // Current open files + files: null, + // True when loading animation is running + isLoading: false, + + initialize: function() { + App.dashboard = this; + this.tree = new TreeCollection(); + + var self = this; + // 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: merge? + this.init(); + // 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.update(); + 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']}); + }, + + update: function() { + console.log('Update package list'); + + var packs = this.tree.get('packages'); + this.files = this.tree.get('files'); + + if (packs) + packs.each(_.bind(this.appendPackage, this)); + + if (!this.files || this.files.length === 0) { + // no files are displayed + this.files = null; + // Open the first package + if (packs && 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.ui.packages).appendWithAnimation(el, animation); + }, + + appendFile: function(file, i, animation) { + var el = new FileView({model: file}).render().el; + $(this.ui.files).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.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.ui.files).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.ui.files).fadeIn(); + App.vent.trigger('dashboard:updated'); + }, + + // Refresh the file if it is currently shown + fileUpdated: function(data) { + var fid; + if (_.isObject(data)) + fid = data.fid; + else + fid = data; + // this works with ids and object TODO: not anymore + var file = this.files.get(fid); + if (file) + if (_.isObject(data)) { // update directly + file.set(data); + App.vent.trigger('dashboard:updated'); + } else { // fetch from server + file.fetch({success: function() { + App.vent.trigger('dashboard:updated'); + }}); + } + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/fileView.js b/pyload/web/app/scripts/views/dashboard/fileView.js new file mode 100644 index 000000000..ce91a5f38 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/fileView.js @@ -0,0 +1,103 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', 'helpers/formatTime', 'hbs!tpl/dashboard/file'], + function($, Backbone, _, App, Api, ItemView, formatTime, template) { + 'use strict'; + + // Renders single file item + return ItemView.extend({ + + tagName: 'li', + className: 'file-view row-fluid', + template: template, + 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() { + // TODO: progress for non download statuses + 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 if (this.model.get('download').status === Api.DownloadStatus.Waiting) { + this.$('.second').html( + '<i class="icon-time"></i> ' + formatTime(this.model.get('eta'))); + + } else // Every else state can be rendered normally + this.render(); + + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/filterView.js b/pyload/web/app/scripts/views/dashboard/filterView.js new file mode 100644 index 000000000..ad72cf926 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/filterView.js @@ -0,0 +1,147 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'models/Package', 'hbs!tpl/dashboard/actionbar'], + /*jslint -W040: false*/ + function($, Backbone, _, App, Api, Package, template) { + 'use strict'; + + // 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.Marionette.ItemView.extend({ + + events: { + 'click .li-check': 'toggle_selection', + 'click .filter-type': 'filter_type', + 'click .filter-state': 'switch_filter', + 'submit .form-search': 'search' + }, + + ui: { + 'search': '.search-query', + 'stateMenu': '.dropdown-toggle .state', + 'select': '.btn-check', + 'name': '.breadcrumb .active' + }, + + template: template, + state: null, + + initialize: function() { + this.state = Api.DownloadState.All; + + // Apply the filter before the content is shown + this.listenTo(App.vent, 'dashboard:contentReady', this.apply_filter); + this.listenTo(App.vent, 'dashboard:updated', this.apply_filter); + this.listenTo(App.vent, 'dashboard:updated', this.updateName); + }, + + onRender: function() { + // use our modified method + $.fn.typeahead.Constructor.prototype.show = show; + this.ui.search.typeahead({ + minLength: 2, + source: this.getSuggestions + }); + + }, + + // TODO: app level api request + search: function(e) { + e.stopPropagation(); + var query = this.ui.search.val(); + this.ui.search.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(App.apiRequest('searchSuggestions', {pattern: query}, { + method: 'POST', + 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.ui.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.ui.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; + }, + + updateName: function() { + // TODO +// this.ui.name.text(App.dashboard.package.get('name')); + }, + + toggle_selection: function() { + App.vent.trigger('selection:toggle'); + }, + + filter_type: function(e) { + + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/packageView.js b/pyload/web/app/scripts/views/dashboard/packageView.js new file mode 100644 index 000000000..2738fcbea --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/packageView.js @@ -0,0 +1,75 @@ +define(['jquery', 'app', 'views/abstract/itemView', 'underscore', 'hbs!tpl/dashboard/package'], + function($, App, itemView, _, template) { + 'use strict'; + + // Renders a single package item + return itemView.extend({ + + tagName: 'li', + className: 'package-view', + template: template, + events: { + 'click .package-name, .btn-open': 'open', + 'click .icon-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('icon-check'); + // toggle class immediately, so no re-render needed + this.model.set('selected', !checked, {silent: true}); + this.$('.select').toggleClass('icon-check').toggleClass('icon-check-empty'); + App.vent.trigger('package:selection'); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/selectionView.js b/pyload/web/app/scripts/views/dashboard/selectionView.js new file mode 100644 index 000000000..25b7998df --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/selectionView.js @@ -0,0 +1,154 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'hbs!tpl/dashboard/select'], + function($, Backbone, _, App, template) { + 'use strict'; + + // Renders context actions for selection packages and files + return Backbone.Marionette.ItemView.extend({ + + el: '#selection-area', + template: template, + + events: { + 'click .icon-check': 'deselect', + 'click .icon-pause': 'pause', + 'click .icon-trash': 'trash', + 'click .icon-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); + App.vent.on('selection:toggle', _.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() { + if (!App.dashboard.tree.get('packages')) + return []; // TODO + + 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(); + + // TODO: accessing ui directly, should be events + if (files > 0) { + App.actionbar.currentView.ui.select.addClass('icon-check').removeClass('icon-check-empty'); + App.dashboard.ui.packages.addClass('ui-files-selected'); + } + else { + App.actionbar.currentView.ui.select.addClass('icon-check-empty').removeClass('icon-check'); + App.dashboard.ui.packages.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('dialogs/confirmDelete', 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/pyload/web/app/scripts/views/headerView.js b/pyload/web/app/scripts/views/headerView.js new file mode 100644 index 000000000..2c83fb381 --- /dev/null +++ b/pyload/web/app/scripts/views/headerView.js @@ -0,0 +1,252 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'collections/ProgressList', + 'views/progressView', 'views/notificationView', 'helpers/formatSize', 'hbs!tpl/header/layout', + 'hbs!tpl/header/status', 'hbs!tpl/header/progressbar', 'hbs!tpl/header/progressSup', 'hbs!tpl/header/progressSub' , 'flot'], + function( + $, _, Backbone, App, ServerStatus, ProgressList, ProgressView, NotificationView, formatSize, template, templateStatus, templateProgress, templateSup, templateSub) { + 'use strict'; + // Renders the header with all information + return Backbone.Marionette.ItemView.extend({ + + events: { + 'click .icon-list': 'toggle_taskList', + 'click .popover .close': 'toggle_taskList', + 'click .btn-grabber': 'open_grabber', + 'click .logo': 'gotoDashboard' + }, + + ui: { + progress: '.progress-list', + speedgraph: '#speedgraph' + }, + + template: template, + + // view + grabber: 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, + lastStatus: null, + + initialize: function() { + var self = this; + this.notificationView = new NotificationView(); + + this.status = new ServerStatus(); + this.listenTo(this.status, 'change', this.update); + + this.progressList = new ProgressList(); + this.listenTo(this.progressList, 'add', function(model) { + self.ui.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) { + console.log(error); + alert('WebSocket error' + error); + }; + + this.ws = ws; + }, + + gotoDashboard: function() { + App.navigate(''); + }, + + 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.ui.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 + } + }); + + }, + + // Must be called after view was attached + init: function() { + this.initGraph(); + this.update(); + }, + + update: 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( + templateStatus(status) + ); + + var data = {tasks: 0, downloads: 0, speed: 0, single: false}; + this.progressList.each(function(progress) { + if (progress.isDownload()) { + data.downloads++; + 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'); + } + + data.etaqueue = status.eta; + data.linksqueue = status.linksqueue; + data.sizequeue = status.sizequeue; + + // Render progressbar only when needed + if (!_.isEqual([data.tasks, data.downloads], this.lastStatus)) { + console.log('render bar'); + this.lastStatus = [data.tasks, data.downloads]; + this.$('#progress-info').html(templateProgress(data)); + } else { + this.$('#progress-info .bar').width(data.percent + '%'); + } + + // render upper and lower part + this.$('.sup').html(templateSup(data)); + this.$('.sub').html(templateSub(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') { + 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 re-render 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.set(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'), + size: prog.get('total') + }, {silent: true}); + file.setDownloadStatus(prog.get('download').status); + file.trigger('change:progress'); + } + } + }); + + if (progress.length === 0) { + // only render one time when last was not empty already + if (!this.wasEmpty) { + this.update(); + this.wasEmpty = true; + } + } else { + this.wasEmpty = false; + this.update(); + } + }, + + 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/pyload/web/app/scripts/views/input/inputLoader.js b/pyload/web/app/scripts/views/input/inputLoader.js new file mode 100644 index 000000000..04d591d30 --- /dev/null +++ b/pyload/web/app/scripts/views/input/inputLoader.js @@ -0,0 +1,8 @@ +define(['./textInput'], function(textInput) { + 'use strict'; + + // selects appropriate input element + return function(input) { + return textInput; + }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/input/inputView.js b/pyload/web/app/scripts/views/input/inputView.js new file mode 100644 index 000000000..1860fcaf1 --- /dev/null +++ b/pyload/web/app/scripts/views/input/inputView.js @@ -0,0 +1,86 @@ +define(['jquery', 'backbone', 'underscore'], function($, Backbone, _) { + 'use strict'; + + // Renders input elements + return Backbone.View.extend({ + + tagName: 'input', + + input: null, + value: null, + description: null, + default_value: null, + + // enables tooltips + tooltip: true, + + initialize: function(options) { + this.input = options.input; + this.default_value = this.input.default_value; + this.value = options.value; + this.description = options.description; + }, + + render: function() { + this.renderInput(); + // data for tooltips + if (this.description && this.tooltip) { + this.$el.data('content', this.description); + // TODO: render default value in popup? +// this.$el.data('title', "TODO: title"); + this.$el.popover({ + placement: 'right', + trigger: 'hover' +// delay: { show: 500, hide: 100 } + }); + } + + return this; + }, + + renderInput: function() { + // Overwrite this + }, + + showTooltip: function() { + if (this.description && this.tooltip) + this.$el.popover('show'); + }, + + hideTooltip: function() { + if (this.description && this.tooltip) + this.$el.popover('hide'); + }, + + 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/pyload/web/app/scripts/views/input/textInput.js b/pyload/web/app/scripts/views/input/textInput.js new file mode 100644 index 000000000..0eebbf91e --- /dev/null +++ b/pyload/web/app/scripts/views/input/textInput.js @@ -0,0 +1,36 @@ +define(['jquery', 'backbone', 'underscore', './inputView'], function($, Backbone, _, inputView) { + 'use strict'; + + return inputView.extend({ + + // TODO + tagName: 'input', + events: { + 'keyup': 'onChange', + 'focus': 'showTooltip', + 'focusout': 'hideTooltip' + }, + + renderInput: 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/pyload/web/app/scripts/views/linkGrabberModal.js b/pyload/web/app/scripts/views/linkGrabberModal.js new file mode 100644 index 000000000..e6f59c134 --- /dev/null +++ b/pyload/web/app/scripts/views/linkGrabberModal.js @@ -0,0 +1,49 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dialogs/linkgrabber'], + function($, _, App, modalView, template) { + 'use strict'; + // Modal dialog for package adding - triggers package:added when package was added + return modalView.extend({ + + events: { + 'click .btn-success': 'addPackage', + 'keypress #inputPackageName': 'addOnEnter' + }, + + template: 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/pyload/web/app/scripts/views/loginView.js b/pyload/web/app/scripts/views/loginView.js new file mode 100644 index 000000000..891b3ec99 --- /dev/null +++ b/pyload/web/app/scripts/views/loginView.js @@ -0,0 +1,37 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'hbs!tpl/login'], + function($, Backbone, _, App, template) { + 'use strict'; + + // Renders context actions for selection packages and files + return Backbone.Marionette.ItemView.extend({ + template: template, + + events: { + 'submit form': 'login' + }, + + ui: { + 'form': 'form' + }, + + login: function(e) { + e.stopPropagation(); + + var options = App.apiRequest('login', null, { + data: this.ui.form.serialize(), + type : 'post', + success: function(data) { + // TODO: go to last page, better error + if (data) + App.navigate(''); + else + alert('Wrong login'); + } + }); + + $.ajax(options); + return false; + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/notificationView.js b/pyload/web/app/scripts/views/notificationView.js new file mode 100644 index 000000000..93d07a0f3 --- /dev/null +++ b/pyload/web/app/scripts/views/notificationView.js @@ -0,0 +1,85 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'collections/InteractionList', 'hbs!tpl/notification'], + function($, Backbone, _, App, InteractionList, template) { + 'use strict'; + + // Renders context actions for selection packages and files + return Backbone.Marionette.ItemView.extend({ + + // Only view for this area so it's hardcoded + el: '#notification-area', + template: template, + + 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(); + + 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); + }, + + onRender: function() { + this.$el.calculateHeight().height(0); + }, + + 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/pyload/web/app/scripts/views/progressView.js b/pyload/web/app/scripts/views/progressView.js new file mode 100644 index 000000000..7b9dbb74b --- /dev/null +++ b/pyload/web/app/scripts/views/progressView.js @@ -0,0 +1,46 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', + 'hbs!tpl/header/progress', 'hbs!tpl/header/progressStatus', 'helpers/pluginIcon'], + function($, Backbone, _, App, Api, ItemView, template, templateStatus, pluginIcon) { + 'use strict'; + + // Renders single file item + return ItemView.extend({ + + idAttribute: 'pid', + tagName: 'li', + template: template, + events: { + }, + + // Last name + name: null, + + initialize: function() { + this.listenTo(this.model, 'change', this.update); + this.listenTo(this.model, 'remove', this.unrender); + }, + + onDestroy: function() { + }, + + // Update html without re-rendering + update: function() { + if (this.name !== this.model.get('name')) { + this.name = this.model.get('name'); + this.render(); + } + + this.$('.bar').width(this.model.getPercent() + '%'); + this.$('.progress-status').html(templateStatus(this.model.toJSON())); + }, + + render: function() { + // TODO: icon + // TODO: other states + // TODO: non download progress + this.$el.css('background-image', 'url(' + pluginIcon('todo') + ')'); + this.$el.html(this.template(this.model.toJSON())); + return this; + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/queryModal.js b/pyload/web/app/scripts/views/queryModal.js new file mode 100644 index 000000000..ce624814a --- /dev/null +++ b/pyload/web/app/scripts/views/queryModal.js @@ -0,0 +1,69 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', './input/inputLoader', 'hbs!tpl/dialogs/interactionTask'], + function($, _, App, modalView, load_input, template) { + 'use strict'; + return modalView.extend({ + + events: { + 'click .btn-success': 'submit', + 'submit form': 'submit' + }, + template: template, + + // the notificationView + parent: null, + + model: null, + input: null, + + 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') + }; + + var input = this.model.get('input').data; + if (this.model.isCaptcha()) { + 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: input}); + // only renders after wards + this.$('#inputField').append(this.input.render().el); + }, + + submit: function(e) { + e.stopPropagation(); + // TODO: load next task + + this.model.set('result', this.input.getVal()); + var self = this; + this.model.save({success: function() { + self.hide(); + }}); + + this.input.clear(); + return false; + }, + + onShow: function() { + this.input.focus(); + }, + + onHide: function() { + this.input.destroy(); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/settings/configSectionView.js b/pyload/web/app/scripts/views/settings/configSectionView.js new file mode 100644 index 000000000..0d9b0762f --- /dev/null +++ b/pyload/web/app/scripts/views/settings/configSectionView.js @@ -0,0 +1,99 @@ +define(['jquery', 'underscore', 'backbone', 'app', '../abstract/itemView', '../input/inputLoader', + 'hbs!tpl/settings/config', 'hbs!tpl/settings/configItem'], + function($, _, Backbone, App, itemView, load_input, template, templateItem) { + 'use strict'; + + // Renders settings over view page + return itemView.extend({ + + tagName: 'div', + + template: template, + templateItem: templateItem, + + // Will only render one time with further attribute updates + rendered: false, + + events: { + 'click .btn-primary': 'submit', + 'click .btn-reset': 'reset' + }, + + initialize: function() { + this.listenTo(this.model, 'destroy', this.destroy); + }, + + render: function() { + if (!this.rendered) { + this.$el.html(this.template(this.model.toJSON())); + + // initialize the popover + this.$('.page-header a').popover({ + placement: 'left' +// trigger: 'hover' + }); + + var container = this.$('.control-content'); + var self = this; + _.each(this.model.get('items'), function(item) { + var json = item.toJSON(); + var el = $('<div>').html(self.templateItem(json)); + var InputView = load_input(item.get('input')); + var input = new InputView(json).render(); + item.set('inputView', input); + + self.listenTo(input, 'change', _.bind(self.render, self)); + el.find('.controls').append(input.el); + container.append(el); + }); + this.rendered = true; + } + // Enable button if something is changed + if (this.model.hasChanges()) + this.$('.btn-primary').removeClass('disabled'); + else + this.$('.btn-primary').addClass('disabled'); + + // Mark all inputs that are modified + _.each(this.model.get('items'), function(item) { + var input = item.get('inputView'); + var el = input.$el.parent().parent(); + if (item.isChanged()) + el.addClass('info'); + else + el.removeClass('info'); + }); + + return this; + }, + + onDestroy: function() { + // TODO: correct cleanup after building up so many views and models + }, + + submit: function(e) { + e.stopPropagation(); + // TODO: success / failure popups + var self = this; + this.model.save({success: function() { + self.render(); + App.vent.trigger('config:change'); + }}); + + }, + + reset: function(e) { + e.stopPropagation(); + // restore the original value + _.each(this.model.get('items'), function(item) { + if (item.has('inputView')) { + var input = item.get('inputView'); + input.setVal(item.get('value')); + input.render(); + } + }); + this.render(); + } + + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/settings/pluginChooserModal.js b/pyload/web/app/scripts/views/settings/pluginChooserModal.js new file mode 100644 index 000000000..242d11a5a --- /dev/null +++ b/pyload/web/app/scripts/views/settings/pluginChooserModal.js @@ -0,0 +1,72 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', 'hbs!tpl/dialogs/addPluginConfig', + 'helpers/pluginIcon', 'select2'], + function($, _, App, modalView, template, pluginIcon) { + 'use strict'; + return modalView.extend({ + + events: { + 'click .btn-add': 'add' + }, + template: template, + plugins: null, + select: null, + + initialize: function() { + // Inherit parent events + this.events = _.extend({}, modalView.prototype.events, this.events); + var self = this; + $.ajax(App.apiRequest('getAvailablePlugins', null, {success: function(data) { + self.plugins = _.sortBy(data, function(item) { + return item.name; + }); + self.render(); + }})); + }, + + onRender: function() { + // TODO: could be a seperate input type if needed on multiple pages + if (this.plugins) + this.select = this.$('#pluginSelect').select2({ + escapeMarkup: function(m) { + return m; + }, + formatResult: this.format, + formatSelection: this.formatSelection, + data: {results: this.plugins, text: function(item) { + return item.label; + }}, + id: function(item) { + return item.name; + } + }); + }, + + onShow: function() { + }, + + onHide: function() { + }, + + format: function(data) { + var s = '<div class="plugin-select" style="background-image: url(' + pluginIcon(data.name) + ')">' + data.label; + s += '<br><span>' + data.description + '<span></div>'; + return s; + }, + + formatSelection: function(data) { + if (!data || _.isEmpty(data)) + return ''; + + return '<img class="logo-select" src="' + pluginIcon(data.name) + '"> ' + data.label; + }, + + add: function(e) { + e.stopPropagation(); + if (this.select) { + var plugin = this.select.val(); + App.vent.trigger('config:open', plugin); + this.hide(); + } + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/settings/settingsView.js b/pyload/web/app/scripts/views/settings/settingsView.js new file mode 100644 index 000000000..ff86efdf9 --- /dev/null +++ b/pyload/web/app/scripts/views/settings/settingsView.js @@ -0,0 +1,184 @@ +define(['jquery', 'underscore', 'backbone', 'app', 'models/ConfigHolder', './configSectionView', + 'hbs!tpl/settings/layout', 'hbs!tpl/settings/menu', 'hbs!tpl/settings/actionbar'], + function($, _, Backbone, App, ConfigHolder, ConfigSectionView, template, templateMenu, templateBar) { + 'use strict'; + + // Renders settings over view page + return Backbone.Marionette.ItemView.extend({ + + template: template, + templateMenu: templateMenu, + + events: { + 'click .settings-menu li > a': 'change_section', + 'click .icon-remove': 'deleteConfig' + }, + + ui: { + 'menu': '.settings-menu', + 'content': '.setting-box > form' + }, + + selected: null, + modal: null, + + coreConfig: null, // It seems collections are not needed + pluginConfig: null, + + // currently open configHolder + config: null, + lastConfig: null, + isLoading: false, + + initialize: function() { + this.actionbar = Backbone.Marionette.ItemView.extend({ + template: templateBar, + events: { + 'click .btn': 'choosePlugin' + }, + choosePlugin: _.bind(this.choosePlugin, this) + + }); + this.listenTo(App.vent, 'config:open', this.openConfig); + this.listenTo(App.vent, 'config:change', this.refresh); + + this.refresh(); + }, + + refresh: function() { + var self = this; + $.ajax(App.apiRequest('getCoreConfig', null, {success: function(data) { + self.coreConfig = data; + self.renderMenu(); + }})); + $.ajax(App.apiRequest('getPluginConfig', null, {success: function(data) { + self.pluginConfig = data; + self.renderMenu(); + }})); + }, + + onRender: function() { + // set a height with css so animations will work + this.ui.content.height(this.ui.content.height()); + }, + + renderMenu: function() { + var plugins = [], + addons = []; + + // separate addons and default plugins + // addons have an activated state + _.each(this.pluginConfig, function(item) { + if (item.activated === null) + plugins.push(item); + else + addons.push(item); + }); + + this.$(this.ui.menu).html(this.templateMenu({ + core: this.coreConfig, + plugin: plugins, + addon: addons + })); + + // mark the selected element + this.$('li[data-name="' + this.selected + '"]').addClass('active'); + }, + + openConfig: function(name) { + // Do nothing when this config is already open + if (this.config && this.config.get('name') === name) + return; + + this.lastConfig = this.config; + this.config = new ConfigHolder({name: name}); + this.loading(); + + var self = this; + this.config.fetch({success: function() { + if (!self.isLoading) + self.show(); + + }, failure: _.bind(this.failure, this)}); + + }, + + loading: function() { + this.isLoading = true; + var self = this; + this.ui.content.fadeOut({complete: function() { + if (self.config.isLoaded()) + self.show(); + + self.isLoading = false; + }}); + + }, + + show: function() { + // TODO animations are bit sloppy + this.ui.content.css('display', 'block'); + var oldHeight = this.ui.content.height(); + + // this will destroy the old view + if (this.lastConfig) + this.lastConfig.trigger('destroy'); + else + this.ui.content.empty(); + + // reset the height + this.ui.content.css('height', ''); + // append the new element + this.ui.content.append(new ConfigSectionView({model: this.config}).render().el); + // get the new height + var height = this.ui.content.height(); + // set the old height again + this.ui.content.height(oldHeight); + this.ui.content.animate({ + opacity: 'show', + height: height + }); + }, + + failure: function() { + // TODO + this.config = null; + }, + + change_section: function(e) { + // TODO check for changes + // TODO move this into render? + + var el = $(e.target).closest('li'); + + this.selected = el.data('name'); + this.openConfig(this.selected); + + this.ui.menu.find('li.active').removeClass('active'); + el.addClass('active'); + e.preventDefault(); + }, + + choosePlugin: function(e) { + var self = this; + _.requireOnce(['views/settings/pluginChooserModal'], function(Modal) { + if (self.modal === null) + self.modal = new Modal(); + + self.modal.show(); + }); + }, + + deleteConfig: function(e) { + e.stopPropagation(); + var el = $(e.target).parent().parent(); + var name = el.data('name'); + var self = this; + $.ajax(App.apiRequest('deleteConfig', {plugin: name}, { success: function() { + self.refresh(); + }})); + return false; + } + + }); + });
\ No newline at end of file |