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 | 
