diff options
Diffstat (limited to 'pyload/web')
111 files changed, 7025 insertions, 0 deletions
diff --git a/pyload/web/.bowerrc b/pyload/web/.bowerrc new file mode 100644 index 000000000..f594df7a7 --- /dev/null +++ b/pyload/web/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "app/components" +} diff --git a/pyload/web/Gruntfile.js b/pyload/web/Gruntfile.js new file mode 100644 index 000000000..92bb33da9 --- /dev/null +++ b/pyload/web/Gruntfile.js @@ -0,0 +1,425 @@ +'use strict'; +var LIVERELOAD_PORT = 35729; +var lrSnippet = require('connect-livereload')({port: LIVERELOAD_PORT}); +var mountFolder = function(connect, dir) { + return connect.static(require('path').resolve(dir)); +}; +var fs = require('fs'); +var path = require('path'); + +// # Globbing +// for performance reasons we're only matching one level down: +// 'test/spec/{,*/}*.js' +// use this if you want to recursively match all subfolders: +// 'test/spec/**/*.js' + +module.exports = function(grunt) { + // load all grunt tasks + require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); + + // configurable paths + var yeomanConfig = { + app: 'app', + dist: 'dist', + banner: '/* Copyright(c) 2008-2013 pyLoad Team */\n' + }; + + grunt.initConfig({ + yeoman: yeomanConfig, + watch: { + options: { + nospawn: true + }, + less: { + files: ['<%= yeoman.app %>/styles/**/*.less'], + tasks: ['less'] + }, + livereload: { + options: { + livereload: LIVERELOAD_PORT + }, + files: [ + '<%= yeoman.app %>/**/*.html', + '{<%= yeoman.app %>}/styles/**/*.css', + '{.tmp,<%= yeoman.app %>}/scripts/**/*.js', + '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' + ] + } + }, + connect: { + options: { + port: 9000, + // change this to '0.0.0.0' to access the server from outside + hostname: 'localhost' + }, + livereload: { + options: { + middleware: function(connect) { + return [ + lrSnippet, + mountFolder(connect, '.tmp'), + mountFolder(connect, yeomanConfig.app) + ]; + } + } + }, + test: { + options: { + middleware: function(connect) { + return [ + mountFolder(connect, '.tmp'), + mountFolder(connect, 'test') + ]; + } + } + }, + dist: { + options: { + middleware: function(connect) { + return [ + mountFolder(connect, yeomanConfig.dist) + ]; + } + } + } + }, + open: { // Opens the webbrowser + server: { + path: 'http://localhost:<%= connect.options.port %>' + } + }, + clean: { + dist: { + files: [ + { + dot: true, + src: [ + '.tmp', + '<%= yeoman.dist %>/*', + '!<%= yeoman.dist %>/.git*' + ] + } + ] + }, + server: '.tmp' + }, + jshint: { + options: { + jshintrc: '<%= yeoman.app %>/components/pyload-common/.jshintrc' + }, + all: [ + 'Gruntfile.js', + '<%= yeoman.app %>/scripts/**/*.js', + '!<%= yeoman.app %>/scripts/vendor/*', + 'test/spec/{,*/}*.js' + ] + }, + mocha: { + all: { + options: { + run: true, + urls: ['http://localhost:<%= connect.options.port %>/index.html'] + } + } + }, + less: { + options: { + paths: [yeomanConfig.app + '/components', yeomanConfig.app + '/components/pyload-common/styles', + yeomanConfig.app + '/styles/default'] + //dumpLineNumbers: true + }, + dist: { + files: [ + { + expand: true, // Enable dynamic expansion. + cwd: '<%= yeoman.app %>/styles/', // Src matches are relative to this path. + src: ['**/main.less'], // Actual pattern(s) to match. + dest: '.tmp/styles', // Destination path prefix. + ext: '.css' // Dest filepaths will have this extension. + } + ] + } + }, + // not used since Uglify task does concat, + // but still available if needed + /*concat: { + dist: {} + },*/ + requirejs: { + dist: { + // Options: https://github.com/jrburke/r.js/blob/master/build/example.build.js + options: { + // `name` and `out` is set by grunt-usemin + baseUrl: yeomanConfig.app + '/scripts', + optimize: 'none', + // TODO: Figure out how to make sourcemaps work with grunt-usemin + // https://github.com/yeoman/grunt-usemin/issues/30 + //generateSourceMaps: true, + // required to support SourceMaps + // http://requirejs.org/docs/errors.html#sourcemapcomments + preserveLicenseComments: false, + useStrict: true, + wrap: true, + + // Delete already included files from dist + // TODO: For multiple modules it would delete to much files + done: function(done, output) { + var root = path.join(path.resolve('.'), yeomanConfig.app); + var parse = require('rjs-build-analysis').parse(output); + parse.bundles.forEach(function(bundle) { + var parent = path.relative(path.resolve('.'), bundle.parent); + bundle.children.forEach(function(f) { + // Skip templates + if (f.indexOf('hbs!') > -1) return; + + var rel = path.relative(root, f); + var target = path.join(yeomanConfig.dist, rel); + + if (target === parent) + return; + + if (fs.existsSync(target)) { + console.log('Removing', target); + fs.unlinkSync(target); + + // Remove the empty directories + var files = fs.readdirSync(path.dirname(target)); + if (files.length === 0) { + fs.rmdirSync(path.dirname(target)); + console.log('Removing dir', path.dirname(target)); + } + + } + }); + }); + done(); + } + //uglify2: {} // https://github.com/mishoo/UglifyJS2 + } + } + }, + rev: { + dist: { + files: { + src: [ + // TODO only main script needs a rev + '<%= yeoman.dist %>/scripts/default.js', + '<%= yeoman.dist %>/styles/{,*/}*.css' + ] + } + } + }, + useminPrepare: { + options: { + dest: '<%= yeoman.dist %>' + }, + html: '<%= yeoman.app %>/index.html' + }, + usemin: { + options: { + dirs: ['<%= yeoman.dist %>'] + }, + html: ['<%= yeoman.dist %>/*.html'], + css: ['<%= yeoman.dist %>/styles/**/*.css'] + }, + imagemin: { + dist: { + files: [ + { + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '**/*.{png,jpg,jpeg}', + dest: '<%= yeoman.dist %>/images' + } + ] + } + }, + svgmin: { + dist: { + files: [ + { + expand: true, + cwd: '<%= yeoman.app %>/images', + src: '**/*.svg', + dest: '<%= yeoman.dist %>/images' + } + ] + } + }, + htmlmin: { + dist: { + options: { + /*removeCommentsFromCDATA: true, + // https://github.com/yeoman/grunt-usemin/issues/44 + //collapseWhitespace: true, + collapseBooleanAttributes: true, + removeAttributeQuotes: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeOptionalTags: true*/ + }, + files: [ + { + expand: true, + cwd: '<%= yeoman.app %>', + src: ['*.html'], + dest: '<%= yeoman.dist %>' + } + ] + } + }, + cssmin: { + options: { + banner: yeomanConfig.banner + }, + dist: { + expand: true, + cwd: '<%= yeoman.dist %>', + src: ['**/*.css', '!*.min.css'], + dest: '<%= yeoman.dist %>', + ext: '.css' + } + }, + uglify: { // JS min + options: { + mangle: true, + report: 'min', + preserveComments: false, + banner: yeomanConfig.banner + }, + dist: { + expand: true, + cwd: '<%= yeoman.dist %>', + dest: '<%= yeoman.dist %>', + src: ['**/*.js', '!*.min.js'] + } + }, + // Put files not handled in other tasks here + copy: { + // Copy files from third party libraries + stageComponents: { + files: [ + { + expand: true, + flatten: true, + cwd: '<%= yeoman.app %>', + dest: '.tmp/fonts', + src: [ + '**/font-awesome/font/*' + ] + }, + { + expand: true, + flatten: true, + cwd: '<%= yeoman.app %>', + dest: '.tmp/vendor', + src: [ + '**/select2/select2.{png,css}', + '**/select2/select2-spinner.gif', + '**/select2/select2x2.png' + ] + }, + { + expand: true, + cwd: '<%= yeoman.app %>/components/pyload-common', + dest: '.tmp', + src: [ + 'favicon.ico', + 'images/*', + 'fonts/*' + ] + } + ] + }, + + dist: { + files: [ + { + expand: true, + dot: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>', + src: [ + '*.{ico,txt}', + 'images/{,*/}*.{webp,gif}', + 'templates/**/*.html', + 'scripts/**/*.js', + 'styles/**/*.css', + 'fonts/*' + ] + } + ] + }, + + tmp: { + files: [ + { + expand: true, + cwd: '.tmp/', + dest: '<%= yeoman.dist %>', + src: [ + 'fonts/*', + 'images/*', + '**/*.{css,gif,png,js,html,ico}' + ] + } + ] + } + }, + concurrent: { + server: [ + 'copy:stageComponents', + 'less' + ], + test: [ + 'less' + ], + dist: [ + 'imagemin', + 'svgmin', + 'htmlmin', + 'cssmin' + ] + } + }); + + grunt.registerTask('server', function(target) { + if (target === 'dist') { + return grunt.task.run(['build', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'clean:server', + 'concurrent:server', + 'connect:livereload', + 'watch' + ]); + }); + + grunt.registerTask('test', [ + 'clean:server', + 'concurrent:test', + 'connect:test', + 'mocha' + ]); + + grunt.registerTask('build', [ + 'clean:dist', + 'useminPrepare', + 'less', + 'copy', // Copy .tmp, components, app to dist + 'requirejs', // build the main script and remove included scripts + 'concat', + 'concurrent:dist', // Run minimisation + 'uglify', // minify js + 'rev', + 'usemin' + ]); + + grunt.registerTask('default', [ + 'jshint', +// 'test', + 'build' + ]); +}; diff --git a/pyload/web/ServerThread.py b/pyload/web/ServerThread.py new file mode 100644 index 000000000..c55ddef0f --- /dev/null +++ b/pyload/web/ServerThread.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +from __future__ import with_statement +from time import time, sleep + +import threading +import logging + +from pyload.utils.fs import exists + +core = None +setup = None +log = logging.getLogger("log") + + +class WebServer(threading.Thread): + def __init__(self, pycore=None, pysetup=None): + global core, setup + threading.Thread.__init__(self) + + if pycore: + core = pycore + config = pycore.config + elif pysetup: + setup = pysetup + config = pysetup.config + else: + raise Exception("No config context provided") + + self.server = config['webinterface']['server'] + self.https = config['webinterface']['https'] + self.cert = config["ssl"]["cert"] + self.key = config["ssl"]["key"] + self.host = config['webinterface']['host'] + self.port = config['webinterface']['port'] + self.debug = config['general']['debug_mode'] + self.force_server = config['webinterface']['force_server'] + self.error = None + + self.setDaemon(True) + + def run(self): + self.running = True + import webinterface + + global webinterface + + if self.https: + if not exists(self.cert) or not exists(self.key): + log.warning(_("SSL certificates not found.")) + self.https = False + + if webinterface.UNAVAILALBE: + log.warning(_("WebUI built is not available")) + elif webinterface.APP_PATH == "app": + log.info(_("Running webUI in development mode")) + + prefer = None + + # These cases covers all settings + if self.server == "threaded": + prefer = "threaded" + elif self.server == "fastcgi": + prefer = "flup" + elif self.server == "fallback": + prefer = "wsgiref" + + server = self.select_server(prefer) + + try: + self.start_server(server) + + except Exception, e: + log.error(_("Failed starting webserver: " + e.message)) + self.error = e + if core: + core.print_exc() + + def select_server(self, prefer=None): + """ find a working server """ + from servers import all_server + + unavailable = [] + server = None + for server in all_server: + + if self.force_server and self.force_server == server.NAME: + break # Found server + # When force_server is set, no further checks have to be made + elif self.force_server: + continue + + if prefer and prefer == server.NAME: + break # found prefered server + elif prefer: # prefer is similar to force, but force has precedence + continue + + # Filter for server that offer ssl if needed + if self.https and not server.SSL: + continue + + try: + if server.find(): + break # Found a server + else: + unavailable.append(server.NAME) + except Exception, e: + log.error(_("Failed importing webserver: " + e.message)) + + if unavailable: # Just log whats not available to have some debug information + log.debug("Unavailable webserver: " + ",".join(unavailable)) + + if not server and self.force_server: + server = self.force_server # just return the name + + return server + + + def start_server(self, server): + + from servers import ServerAdapter + + if issubclass(server, ServerAdapter): + + if self.https and not server.SSL: + log.warning(_("This server offers no SSL, please consider using threaded instead")) + elif not self.https: + self.cert = self.key = None # This implicitly disables SSL + # there is no extra argument for the server adapter + # TODO: check for openSSL ? + + # Now instantiate the serverAdapter + server = server(self.host, self.port, self.key, self.cert, 6, self.debug) # todo, num_connections + name = server.NAME + + else: # server is just a string + name = server + + log.info( + _("Starting %(name)s webserver: %(host)s:%(port)d") % {"name": name, "host": self.host, "port": self.port}) + webinterface.run_server(host=self.host, port=self.port, server=server) + + + # check if an error was raised for n seconds + def check_error(self, n=1): + t = time() + n + while time() < t: + if self.error: + return self.error + sleep(0.1) + diff --git a/pyload/web/__init__.py b/pyload/web/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/pyload/web/__init__.py diff --git a/pyload/web/api_app.py b/pyload/web/api_app.py new file mode 100644 index 000000000..3ffc507aa --- /dev/null +++ b/pyload/web/api_app.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from urllib import unquote +from itertools import chain +from traceback import format_exc, print_exc + +from bottle import route, request, response, HTTPError, parse_auth + +from utils import set_session, get_user_api +from webinterface import PYLOAD + +from pyload.Api import ExceptionObject +from pyload.remote.json_converter import loads, dumps +from pyload.utils import remove_chars + +def add_header(r): + r.headers.replace("Content-type", "application/json") + r.headers.append("Cache-Control", "no-cache, must-revalidate") + r.headers.append("Access-Control-Allow-Origin", request.get_header('Origin', '*')) + r.headers.append("Access-Control-Allow-Credentials", "true") + +# accepting positional arguments, as well as kwargs via post and get +# only forbidden path symbol are "?", which is used to separate GET data and # +@route("/api/<func><args:re:[^#?]*>") +@route("/api/<func><args:re:[^#?]*>", method="POST") +def call_api(func, args=""): + add_header(response) + + s = request.environ.get('beaker.session') + # Accepts standard http auth + auth = parse_auth(request.get_header('Authorization', '')) + if 'session' in request.POST or 'session' in request.GET: + # removes "' so it works on json strings + s = s.get_by_id(remove_chars(request.params.get('session'), "'\"")) + elif auth: + user = PYLOAD.checkAuth(auth[0], auth[1], request.environ.get('REMOTE_ADDR', None)) + # if auth is correct create a pseudo session + if user: s = {'uid': user.uid} + + api = get_user_api(s) + if not api: + return HTTPError(401, dumps("Unauthorized"), **response.headers) + + if not PYLOAD.isAuthorized(func, api.user): + return HTTPError(403, dumps("Forbidden"), **response.headers) + + if not hasattr(PYLOAD.EXTERNAL, func) or func.startswith("_"): + print "Invalid API call", func + return HTTPError(404, dumps("Not Found"), **response.headers) + + # TODO: possible encoding + # TODO Better error codes on invalid input + + args = [loads(unquote(arg)) for arg in args.split("/")[1:]] + kwargs = {} + + # accepts body as json dict + if request.json: + kwargs = request.json + + # convert arguments from json to obj separately + for x, y in chain(request.GET.iteritems(), request.POST.iteritems()): + if not x or not y or x == "session": continue + kwargs[x] = loads(unquote(y)) + + try: + result = getattr(api, func)(*args, **kwargs) + # null is invalid json response + if result is None: result = True + return dumps(result) + + except ExceptionObject, e: + return HTTPError(400, dumps(e), **response.headers) + except Exception, e: + print_exc() + return HTTPError(500, dumps({"error": e.message, "traceback": format_exc()}), **response.headers) + + +@route("/api/login") +@route("/api/login", method="POST") +def login(): + add_header(response) + + username = request.params.get("username") + password = request.params.get("password") + + user = PYLOAD.checkAuth(username, password, request.environ.get('REMOTE_ADDR', None)) + + if not user: + return dumps(False) + + s = set_session(request, user) + + # get the session id by dirty way, documentations seems wrong + try: + sid = s._headers["cookie_out"].split("=")[1].split(";")[0] + return dumps(sid) + except: + print "Could not get session" + return dumps(True) + + +@route("/api/logout") +@route("/api/logout", method="POST") +def logout(): + add_header(response) + + s = request.environ.get('beaker.session') + s.delete() + + return dumps(True) diff --git a/pyload/web/app/fonts/Abel-Regular.ttf b/pyload/web/app/fonts/Abel-Regular.ttf Binary files differnew file mode 100755 index 000000000..e37beb972 --- /dev/null +++ b/pyload/web/app/fonts/Abel-Regular.ttf diff --git a/pyload/web/app/fonts/Abel-Regular.woff b/pyload/web/app/fonts/Abel-Regular.woff Binary files differnew file mode 100644 index 000000000..ab8954389 --- /dev/null +++ b/pyload/web/app/fonts/Abel-Regular.woff diff --git a/pyload/web/app/images/default/checks_sheet.png b/pyload/web/app/images/default/checks_sheet.png Binary files differnew file mode 100644 index 000000000..9662b8070 --- /dev/null +++ b/pyload/web/app/images/default/checks_sheet.png diff --git a/pyload/web/app/images/icon.png b/pyload/web/app/images/icon.png Binary files differnew file mode 100644 index 000000000..1ab4ca081 --- /dev/null +++ b/pyload/web/app/images/icon.png diff --git a/pyload/web/app/index.html b/pyload/web/app/index.html new file mode 100644 index 000000000..03e8535d7 --- /dev/null +++ b/pyload/web/app/index.html @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> + <!-- TODO: dynamic title --> + <title>pyLoad WebUI</title> + <meta name="description" content="pyLoad WebUI"> + <meta name="viewport" content="width=device-width"> + + <!-- TODO: basepath and templates --> + <link href="styles/font.css" rel="stylesheet" type="text/css"/> + <link href="styles/default/main.css" rel="stylesheet" type="text/css"> + <link href="vendor/select2.css" rel="stylesheet" type="text/css"/> + + + <!-- build:js scripts/config.js --> + <script data-main="scripts/config" src="components/requirejs/require.js"></script> + <!-- endbuild --> + + <script type="text/javascript"> + + // Use value set by templateEngine or default val + function configValue(string, defaultValue) { + if (string.indexOf('{{') > -1) + return defaultValue; + return string; + } + + window.dates = { + weeks: ['week', 'weeks'], + days: ['day', 'days'], + hours: ['hour', 'hours'], + minutes: ['minute', 'minutes'], + seconds: ['second', 'seconds'] + }; // TODO carefully when translating + + window.hostProtocol = window.location.protocol + '//'; + window.hostAddress = window.location.hostname; + window.hostPort = configValue('{{web}}', '8001'); + window.pathPrefix = '/'; + window.wsAddress = configValue('{{ws}}', 'ws://%s:7227'); + + require(['config'], function(Config) { + require(['default'], function(App) { + }); + }) + </script> + +</head> +<body> +<div id="wrap"> + <header> + <div class="container-fluid"> + <div class="row-fluid" id="header"> + <div class="span3"> + <div class="logo"></div> + <span class="title visible-large-screen">pyLoad</span> + </div> + </div> + </div> + <div id="notification-area"></div> + <div id="selection-area"></div> + </header> + <div id="content-container" class="container-fluid"> + <div class="row-fluid" id="actionbar"> + </div> + <div class="row-fluid" id="content"> + </div> + </div> +</div> +<footer> + <div class="container-fluid"> + <div class="row-fluid"> + <div class="span2 offset1"> + <div class="copyright"> + © 2008-2013<br> + <a href="http://pyload.org/" target="_blank">The pyLoad Team</a><br> + </div> + </div> + <div class="span2"> + <h2 class="block-title">Powered by</h2> + <hr> + Bootstrap <br> + </div> + + <div class="span2"> + <h2 class="block-title">pyLoad</h2> + <hr> + dsfdsf <br> + </div> + + <div class="span2"> + <h2 class="block-title">Community</h2> + <hr> + asd <br> + </div> + + <div class="span2"> + <h2 class="block-title">Development</h2> + <hr> + asd <br> + </div> + </div> + </div> +</footer> +<div id="modal-overlay" class="hide"></div> +</body> +</html> 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..873f4c0e3 --- /dev/null +++ b/pyload/web/app/scripts/collections/FileList.js @@ -0,0 +1,18 @@ +define(['jquery', 'backbone', 'underscore', 'models/File'], function($, Backbone, _, File) { + 'use strict'; + + return Backbone.Collection.extend({ + + model: File, + + comparator: function(file) { + return file.get('fileorder'); + }, + + 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..cbbc9064f --- /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: {'Multiple': 10, 'Int': 2, 'NA': 0, 'List': 11, 'Bool': 7, 'File': 3, 'Text': 1, 'Table': 12, 'Folder': 4, 'Password': 6, 'Click': 8, 'Select': 9, '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..a7779230b --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/dashboardView.js @@ -0,0 +1,168 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'models/TreeCollection', + './packageView', './fileView', 'hbs!tpl/dashboard/layout', 'select2'], + function($, Backbone, _, App, TreeCollection, 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); + else // fetch from server + file.fetch(); + } + }); + });
\ 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..79257547c --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/filterView.js @@ -0,0 +1,146 @@ +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.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..991f0f32e --- /dev/null +++ b/pyload/web/app/scripts/views/headerView.js @@ -0,0 +1,239 @@ +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' , 'flot'], + function($, _, Backbone, App, ServerStatus, ProgressList, ProgressView, NotificationView, formatSize, + template, templateStatus, templateHeader) { + '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' + }, + + ui: { + progress: '.progress-list', + speedgraph: '#speedgraph' + }, + + // todo: maybe combine these + template: template, + templateStatus: templateStatus, + templateHeader: templateHeader, + + // 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, + + 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; + }, + + 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( + this.templateStatus(status) + ); + + var data = {tasks: 0, downloads: 0, speed: 0, single: false}; + this.progressList.each(function(progress) { + if (progress.isDownload()) { + data.downloads += 1; + data.speed += progress.get('download').speed; + } else + data.tasks++; + }); + + // Show progress of one task + if (data.tasks + data.downloads === 1) { + var progress = this.progressList.at(0); + data.single = true; + data.eta = progress.get('eta'); + data.percent = progress.getPercent(); + data.name = progress.get('name'); + data.statusmsg = progress.get('statusmsg'); + } + // TODO: better progressbar rendering + + data.etaqueue = status.eta; + data.linksqueue = status.linksqueue; + data.sizequeue = status.sizequeue; + + this.$('#progress-info').html( + this.templateHeader(data) + ); + return this; + }, + + toggle_taskList: function() { + this.$('.popover').animate({opacity: 'toggle'}); + }, + + open_grabber: function() { + var self = this; + _.requireOnce(['views/linkGrabberModal'], function(ModalView) { + if (self.grabber === null) + self.grabber = new ModalView(); + + self.grabber.show(); + }); + }, + + onData: function(evt) { + var data = JSON.parse(evt.data); + if (data === null) return; + + if (data['@class'] === 'ServerStatus') { + 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') + }, {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..11665abb4 --- /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, value, default_value, description) { + 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..1fbe5042d --- /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, + default_value: null, + description: null, + + // enables tooltips + tooltip: true, + + initialize: function(options) { + this.input = options.input; + this.value = options.value; + this.default_value = options.default_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..3a4bb2825 --- /dev/null +++ b/pyload/web/app/scripts/views/progressView.js @@ -0,0 +1,33 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'views/abstract/itemView', + 'hbs!tpl/header/progress', 'helpers/pluginIcon'], + function($, Backbone, _, App, Api, ItemView, template, pluginIcon) { + 'use strict'; + + // Renders single file item + return ItemView.extend({ + + idAttribute: 'pid', + tagName: 'li', + template: template, + events: { + }, + + initialize: function() { + this.listenTo(this.model, 'change', this.render); + this.listenTo(this.model, 'remove', this.unrender); + }, + + onDestroy: function() { + }, + + render: function() { + // TODO: icon + // TODO: other states + // TODO: non download progress + // TODO: better progressbar rendering + this.$el.css('background-image', 'url('+ 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..c748e1657 --- /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); + // 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..91e9f11b3 --- /dev/null +++ b/pyload/web/app/scripts/views/settings/pluginChooserModal.js @@ -0,0 +1,69 @@ +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) { + 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 diff --git a/pyload/web/app/styles/default/accounts.less b/pyload/web/app/styles/default/accounts.less new file mode 100644 index 000000000..9b45b64b3 --- /dev/null +++ b/pyload/web/app/styles/default/accounts.less @@ -0,0 +1,6 @@ +@import "common"; + +.logo-select { + width: 20px; + height: 20px; +}
\ No newline at end of file diff --git a/pyload/web/app/styles/default/admin.less b/pyload/web/app/styles/default/admin.less new file mode 100644 index 000000000..92524c153 --- /dev/null +++ b/pyload/web/app/styles/default/admin.less @@ -0,0 +1,17 @@ +@import "common"; + +/* + Admin +*/ + +#btn_newuser { + float: right; +} + +#user_permissions { + float: right; +} + +.userperm { + width: 115px; +}
\ No newline at end of file diff --git a/pyload/web/app/styles/default/dashboard.less b/pyload/web/app/styles/default/dashboard.less new file mode 100644 index 000000000..cd240f323 --- /dev/null +++ b/pyload/web/app/styles/default/dashboard.less @@ -0,0 +1,330 @@ +@import "common"; + +/* + Dashboard +*/ + +#dashboard ul { + margin: 0; + list-style: none; +} + +.sidebar-header { + font-size: 25px; + line-height: 25px; + margin: 4px 0; + border-bottom: 1px dashed @grey; +} + +/* + Packages +*/ +.package-list { + list-style: none; + margin-left: 0; +} + +@frame-top: 20px; +@frame-bottom: 18px; + +.package-frame { + position: absolute; + top: -@frame-top; + left: -@frame-top / 2; + right: -@frame-top / 2; + bottom: -@frame-bottom + 2px; // + size of visible bar + z-index: -1; // lies under package + border: 1px solid @grey; + border-radius: 5px; + box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.75); +} + +.package-view { + padding-bottom: 4px; + margin: 8px 0; + position: relative; + overflow: hidden; + + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; + + i { + cursor: pointer; + } + + & > i { + vertical-align: middle; + } + + .progress { + position: absolute; + height: @frame-bottom; + line-height: @frame-bottom; + font-size: 12px; + text-align: center; + border-radius: 0; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + bottom: 0; + left: 0; + right: 0; + margin-bottom: 0; + background-image: none; + color: @light; + background-color: @yellow; + } + + .bar-info { + background-image: none; + background-color: @blue; + } + + &:hover { + overflow: visible; + z-index: 10; + + .package-frame { + background-color: @light; + } + } + + &.ui-selected:hover { + color: @light; + + .package-frame { + background-color: @dark; + } + + } +} + +.package-name { + cursor: pointer; +} + +.package-indicator { + position: absolute; + top: 0; + right: 0; + float: right; + color: @blue; + text-shadow: @yellowDark 1px 1px; + height: @frame-top; + line-height: @frame-top; + + & > i:hover { + color: @green; + } + + .dropdown-menu { + text-shadow: none; + } + + .tooltip { + text-shadow: none; + width: 100%; + } + + .btn-move { + color: @green; + display: none; + } + +} + +.ui-files-selected .btn-move { + display: inline; +} + +// Tag area with different effect on hover +.tag-area { + position: absolute; + top: -2px; + left: 0; + + .badge { + font-size: 11px; + line-height: 11px; + } + + .badge i { + cursor: pointer; + &:hover:before { + content: "\f024"; // show Remove icon + } + } + + .badge-ghost { + visibility: hidden; + cursor: pointer; + opacity: 0.5; + } + + &:hover .badge-ghost { + visibility: visible; + } + +} + +/* + File View +*/ + +.file-list { + list-style: none; + margin: 0; +} + +@file-height: 22px; + +.file-view { + position: relative; + padding: 0 4px; + border-top: 1px solid #dddddd; + line-height: @file-height; + + &:first-child { + border-top: none; + } + + &:hover, &.ui-selected:hover { + border-radius: 5px; + .gradient(top, @blue, @blueLight); + color: @light; + } + + &.ui-selected { + .gradient(top, @yellow, @yellowDark); + color: @dark; + border-color: @greenDark; + + .file-row.downloading .bar { + .gradient(top, @green, @greenLight); + } + + } + + img { // plugin logo + margin-top: -2px; + padding: 0 2px; + height: @file-height; + width: @file-height; + } + + .icon-chevron-down:hover { + cursor: pointer; + color: @yellow; + } + +} + +.file-row { + min-height: 0 !important; +// padding-left: 5px; + padding-top: 4px; + padding-bottom: 4px; + + // TODO: better styling for filestatus + &.second { +// border-radius: 4px; +// background: @light; + font-size: small; + font-weight: bold; +// box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.75); +// .default-shadow; + } + + &.third { + margin-left: 0; + position: relative; + font-size: small; + } + + .dropdown-menu { + font-size: medium; + } +} + +/* + TODO: more colorful states + better fileView design +*/ + +.file-row.finished { +// .gradient(top, @green, @greenLight); +// color: @light; + color: @green; +} + +.file-row.failed { +// .gradient(top, @red, @redLight); +// color: @light; + color: @red; +} + +.file-row.downloading { + + .progress { + height: @file-height; + background: @light; + margin: 0; + } + + .bar { + text-align: left; + .gradient(top, @yellow, @yellowDark); + color: @dark; + } + +} + +/* +FANCY CHECKBOXES +*/ +.file-view .checkbox { + width: 20px; + height: 21px; + background: url(../../images/default/checks_sheet.png) left top no-repeat; + cursor: pointer; +} + +.file-view.ui-selected .checkbox { + background: url(../../images/default/checks_sheet.png) -21px top no-repeat; +} + +/* + Actionbar +*/ + +.form-search { + position: relative; + + .dropdown-menu { + min-width: 100%; + position: absolute; + right: 0; + left: auto; + } + +} + +li.finished > a, li.finished:hover > a { + background-color: @green; + color: @light; + + .caret, .caret:hover { + border-bottom-color: @light !important; + border-top-color: @light !important; + } +} + +li.failed > a, li.failed:hover > a { + background-color: @red; + color: @light; + + .caret, .caret:hover { + border-bottom-color: @light !important; + border-top-color: @light !important; + } +}
\ No newline at end of file diff --git a/pyload/web/app/styles/default/main.less b/pyload/web/app/styles/default/main.less new file mode 100644 index 000000000..0bfa4fe2f --- /dev/null +++ b/pyload/web/app/styles/default/main.less @@ -0,0 +1,21 @@ +@import "bootstrap/less/bootstrap"; +@import "bootstrap/less/responsive"; +@import "font-awesome/less/font-awesome"; + +@FontAwesomePath: "../../fonts"; + +@import "pyload-common/styles/base"; +@import "pyload-common/styles/basic-layout"; + +@import "style"; +@import "dashboard"; +@import "settings"; +@import "accounts"; +@import "admin"; + +@ResourcePath: "../.."; +@DefaultFont: 'Abel', sans-serif; + +// Changed dimensions +@header-height: 70px; +@footer-height: 66px;
\ No newline at end of file diff --git a/pyload/web/app/styles/default/settings.less b/pyload/web/app/styles/default/settings.less new file mode 100644 index 000000000..34bfcb92a --- /dev/null +++ b/pyload/web/app/styles/default/settings.less @@ -0,0 +1,121 @@ +@import "common"; + +/* + Settings +*/ +.settings-menu { + background-color: #FFF; + box-shadow: 0 0 5px #000; // border: 10px solid #EEE; + + .nav-header { + background: @blueDark; + color: @light; + } + + li > a, .nav-header { + margin-left: -16px; + margin-right: -16px; + text-shadow: none; + } + + i { + margin-top: 0; + } + + .plugin, .addon { + a { + padding-left: 28px; + background-position: 4px 2px; + background-repeat: no-repeat; + background-size: 20px 20px; + } + + .icon-remove { + display: none; + } + + &:hover { + i { + display: block; + } + } + + } + + .addon { + div { + font-size: small; + } + .addon-on { + color: @green; + } + + .addon-off { + color: @red; + } + + } + + border-top-left-radius: 0; + border-top-right-radius: 0; + + .nav > li > a:hover { + color: @blueDark; + } +} + +.setting-box { + border: 10px solid @blueDark; + box-shadow: 0 0 5px @dark; // .gradient(bottom, @yellowLightest, @light); + overflow: hidden; + + .page-header { + margin: 0; + + .btn { + float: right; + margin-top: 5px; + } + + .popover { + font-size: medium; + } + + } + + // Bit wider control labels + .control-label { + width: 180px; + } + .controls { + margin-left: 200px; + } + .form-actions { + padding-left: 200px; + } + +} + +/* + Plugin select +*/ + +.plugin-select { + background-position: left 2px; + background-repeat: no-repeat; + background-size: 20px 20px; + padding-left: 24px; + + font-weight: bold; + span { + line-height: 14px; + font-size: small; + font-weight: normal; + } + +} + +.logo-select { + width: 20px; + height: 20px; +}
\ No newline at end of file diff --git a/pyload/web/app/styles/default/style.less b/pyload/web/app/styles/default/style.less new file mode 100644 index 000000000..2202217f6 --- /dev/null +++ b/pyload/web/app/styles/default/style.less @@ -0,0 +1,298 @@ +@import "common"; + +/* + Header +*/ +header { // background-color: @greyDark; + .gradient(to bottom, #222222, #111111); + height: @header-height; + position: fixed; + top: 0; + vertical-align: top; + width: 100%; + z-index: 10; + color: #ffffff; + + a { + color: #ffffff; + } + .container-fluid, .row-fluid { + height: @header-height; + } + + span.title { + color: white; + float: left; + font-family: SansationRegular, sans-serif; + font-size: 40px; + line-height: @header-height; + cursor: default; + } + + .logo { + margin-right: 10px; + margin-top: 10px; + width: 105px; + height: 107px; + background-size: auto; + } + +} + +@header-inner-height: @header-height - 16px; + +// centered header element +.centered { + height: @header-inner-height; + margin: 8px 0; +} + +.header-block { + .centered; + float: left; + line-height: @header-inner-height / 3; // 3 rows + font-size: small; +} + +.status-block { + min-width: 15%; +} + +.header-btn { + float: right; + position: relative; + .centered; + + .lower { + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin-left: 0; + + button { + width: 100% / 3; // 3 buttons + } + + } +} + +#progress-area { + .centered; + position: relative; + margin-top: 8px; + line-height: 16px; + + #progress-info { + padding-left: 2px; + } + + .sub { + font-size: small; + padding: 0 2px; + } + + .popover { // display: block; + max-width: none; + width: 120%; + left: -60%; // Half of width + margin-left: 50%; + top: 100%; + } + + .popover-title, .popover-content { + color: @greyDark; + } + + .icon-list { + cursor: pointer; + margin-right: 2px; // same as globalprogress margin + + &:hover { + color: @yellow; + } + } + .close { + line-height: 14px; + } +} + +.progress-list { + list-style: none; + margin: 0; + font-size: small; + + li { + background-repeat: no-repeat; + background-size: 32px 32px; + background-position: 0px 8px; + padding-left: 40px; + + &:not(:last-child) { + margin-bottom: 5px; + padding-bottom: 5px; + border-bottom: 1px dashed @greyLight; + } + + .progress { + height: 8px; + margin-bottom: 0; + + .bar { + .gradient(bottom, @blue, @blueLight); + } + } + } +} + +#globalprogress { + background-color: @greyDark; + background-image: none; + height: 8px; + margin: 4px 0; + border-radius: 8px; + border: 2px solid @grey; + + .bar { + color: @dark; + background-image: none; + background-color: @yellow; + + &.running { + width: 100%; + .stripes(@yellowLighter, @yellowDark); + } + } +} + +.speedgraph-container { + // Allows speedgraph to take up remaining space + display: block; + overflow: hidden; + padding: 0 8px; + + #speedgraph { + float: right; + width: 100%; + .centered; +// height: @header-height - 16px; +// margin: 8px 0; + font-family: sans-serif; + } +} + +.header-area { + display: none; // hidden by default + position: absolute; + bottom: -28px; + line-height: 18px; + top: @header-height; + padding: 4px 10px 6px 10px; + text-align: center; + border-radius: 0 0 6px 6px; + color: @light; + background-color: @greyDark; + .default-shadow; +} + +#notification-area { + .header-area; + left: 140px; + + .badge { + vertical-align: top; + } + + .btn-query, .btn-notification { + cursor: pointer; + } +} + +#selection-area { + .header-area; + left: 50%; + min-width: 15%; + + i { + cursor: pointer; + + &:hover { + color: @yellow; + } + } + +} + +/* + Actionbar +*/ + +.nav > li > a:hover { + color: @blue; +} + +.actionbar { + padding-bottom: 3px; + margin-bottom: 0; + border-bottom: 1px dashed @grey; + + height: @actionbar-height; + + padding-top: 2px; + margin-bottom: 5px; + +} + +.actionbar > li > a { + margin-top: 4px; +} + +.actionbar .breadcrumb { + margin: 0; + padding-top: 10px; + padding-bottom: 0; + + .active { + color: @grey; + } + +} + +.actionbar form { + margin-top: 6px; + margin-bottom: 0; +} + +.actionbar input, .actionbar button { + padding-top: 2px; + padding-bottom: 2px; +} + +.actionbar .dropdown-menu i { + margin-top: 4px; + padding-right: 5px; +} + +/* + Login +*/ +.login { + vertical-align: middle; + border: 2px solid @dark; + padding: 15px 50px; + font-size: 17px; + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; +} + +/* + Footer +*/ +footer .copyright { + background-size: 40px 40px; + background-position: 12px center; + height: 40px; + padding-left: 40px; + padding-top: 10px; +} diff --git a/pyload/web/app/styles/font.css b/pyload/web/app/styles/font.css new file mode 100644 index 000000000..088b6f14c --- /dev/null +++ b/pyload/web/app/styles/font.css @@ -0,0 +1,13 @@ +/** + * @file + * Font styling + */ + +@font-face { + font-family: 'Abel'; + font-style: normal; + font-weight: 400; + src: local('Abel'), local('Abel-Regular'); + src: url(../fonts/Abel-Regular.woff) format('woff'); + url(../fonts/Abel-Regular.ttf) format('truetype'); +} diff --git a/pyload/web/app/templates/default/accounts/account.html b/pyload/web/app/templates/default/accounts/account.html new file mode 100644 index 000000000..90bd632c8 --- /dev/null +++ b/pyload/web/app/templates/default/accounts/account.html @@ -0,0 +1,10 @@ +<td>{{ plugin }}</td> +<td>{{ loginname }}</td> +<td>{{ valid }}</td> +<td>{{ premium }}</td> +<td>{{ trafficleft }}</td> +<td>{{ shared }}</td> +<td>{{ activated }}</td> +<td> + <button type="button" class="btn btn-danger">Delete</button> +</td>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/accounts/actionbar.html b/pyload/web/app/templates/default/accounts/actionbar.html new file mode 100644 index 000000000..f4652ec42 --- /dev/null +++ b/pyload/web/app/templates/default/accounts/actionbar.html @@ -0,0 +1,5 @@ +<div class="span2 offset1"> +</div> +<span class="span9"> + <button class="btn btn-small btn-blue btn-add">Add Account</button> +</span>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/accounts/layout.html b/pyload/web/app/templates/default/accounts/layout.html new file mode 100644 index 000000000..e6627500d --- /dev/null +++ b/pyload/web/app/templates/default/accounts/layout.html @@ -0,0 +1,19 @@ +<!--{# TODO: responsive layout instead of table #}--> +<div class="span10 offset2"> + <table class="table table-striped"> + <thead> + <tr> + <th>Plugin</th> + <th>Name</th> + <th>Valid</th> + <th>Premium</th> + <th>Traffic</th> + <th>Shared</th> + <th>Activated</th> + <th>Delete</th> + </tr> + </thead> + <tbody class="account-list"> + </tbody> + </table> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/admin.html b/pyload/web/app/templates/default/admin.html new file mode 100644 index 000000000..2eb90d7e0 --- /dev/null +++ b/pyload/web/app/templates/default/admin.html @@ -0,0 +1,223 @@ +{% extends 'default/base.html' %} + +{% block title %}{{ _("Admin") }} - {{ super() }} {% endblock %} +{% block subtitle %}{{ _("Admin") }} +{% endblock %} + +{% block css %} + <link href="static/css/default/admin.less" rel="stylesheet/less" type="text/css" media="screen"/> + <link rel="stylesheet" type="text/css" href="static/css/fontawesome.css" /> +{% endblock %} + +{% block require %} +{% endblock %} + +{% block content %} + <div class="container-fluid"> + <div class="row-fluid"> + <div id="userlist" class="span10"> + <div class="page-header"> + <h1>Admin Bereich + <small>Userverwaltung, Systeminfos</small> + <a id="btn_newuser" class="btn btn-warning btn-large" type="button"><i class="iconf-plus-sign iconf-large "></i></a> + </h1> + + + + </div> + + <div class="dropdown"> + <span class="label name">User</span> + <a class="dropdown-toggle" data-toggle="dropdown" href="#"><i class="iconf-user iconf-8x"></i></a> + <ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu"> + <li><a tabindex="-1" id="useredit" href="#" role="button" data-backdrop="true" data-controls-modal="event-modal" data-keyboard="true"><i class="icon-pencil"></i>Edit</a></li> + <li><a tabindex="-1" href="#"><i class="icon-tasks"></i>Statistik</a></li> + <li class="divider"></li> + <li><a tabindex="-1" href="#"><i class="icon-remove-sign"></i>Delete</a></li> + </ul> + </div> + + <div id="event-modal" class="modal hide fade"> + <div class="modal-header"> + <a class="close" id="useredit_close" href="#">x</a> + <h3>User Settings</h3> + </div> + <div class="modal-body"> + <p>Set password and permissions</p> + <table style="width:100%;" class="table "> + <td> + <div class="input-prepend"> + <span class="add-on"><i class="iconf-key"></i></span> + <input class="span2" style="min-width:120px;" id="prependedInput" type="text" placeholder="New Password"> + </div> + <div class="input-prepend"> + <span class="add-on"><i class="icon-repeat"></i></span> + <input class="span2" style="min-width:120px;" id="prependedInput" type="text" placeholder="Repeat"> + </div> + <br> + <br> + <br> + <form class="form-horizontal"> + <div class="control-group"> + <label class="control-label" for="onoff">Administrator</label> + + <div class="controls"> + <div class="btn-group" id="onoff" data-toggle="buttons-radio"> + <button type="button" class="btn btn-primary" >On</button> + <button type="button" class="btn btn-primary active">Off</button> + </div> + </div> + </div> + </form> + </td> + <td> + <div id="user_permissions"> + <h3>Permissions</h3> + <div class="btn-group btn-group-vertical" data-toggle="buttons-checkbox"> + <button type="button" class="btn btn-inverse userperm">Accounts</button> + <button type="button" class="btn btn-inverse userperm active">Add</button> + <button type="button" class="btn btn-inverse userperm">Delete</button> + <button type="button" class="btn btn-inverse userperm active">Download</button> + <button type="button" class="btn btn-inverse userperm active">List</button> + <button type="button" class="btn btn-inverse userperm">Logs</button> + <button type="button" class="btn btn-inverse userperm">Modify</button> + <button type="button" class="btn btn-inverse userperm">Settings</button> + <button type="button" class="btn btn-inverse userperm active">Status</button> + </div> + </div> + </td> + </table> + </div> + <div class="modal-footer"> + <a class="btn btn-primary" id="useredit_save"href="#">Save</a> + + </div> + </div> + + + + </div> + + <div class="span2"> + <br> + <h2>Support</h2> + <table> + <tr> + <td> + <i class="icon-globe"></i> + </td> + <td> + <a href="#">Wiki |</a> + <a href="#">Forum |</a> + <a href="#">Chat</a> + </td> + </tr> + <tr> + <td> + <i class="icon-book"></i> + </td> + <td> + <a href="#">Documentation</a> + </td> + </tr> + <tr> + <td> + <i class="icon-fire"></i> + </td> + <td> + <a href="#">Development</a> + </td> + </tr> + <tr> + <td> + <i class="icon-bullhorn"></i> + </td> + <td> + <a href="#">Issue Tracker</a> + </td> + </tr> + </table> + <br> + <a href="#" class="btn btn-inverse" id="info" rel="popover" data-content="<table class='table table-striped'> + <tbody> + <tr> + <td>Python:</td> + <td>2.6.4 </td> + </tr> + <tr> + <td>Betriebssystem:</td> + <td>nt win32</td> + </tr> + <tr> + <td>pyLoad Version:</td> + <td>0.4.9</td> + </tr> + <tr> + <td>Installationsordner:</td> + <td>C:\pyLoad</td> + </tr> + <tr> + <td>Konfigurationsordner:</td> + <td>C:\Users\Marvin\pyload</td> + </tr> + <tr> + <td>Downloadordner:</td> + <td>C:\Users\Marvin\new</td> + </tr> + <tr> + <td>HDD:</td> + <td>1.67 TiB <div class='progress progress-striped active'> + <div class='bar' style='width: 40%;'></div> +</div></td> + </tr> + <tr> + <td>Sprache:</td> + <td>de</td> + </tr> + <tr> + <td>Webinterface Port:</td> + <td>8000</td> + </tr> + <tr> + <td>Remote Interface Port:</td> + <td>7227</td> + </tr> + </tbody> + </table>" title="Systeminformationen">System</a> + + </div> + </div> + </div> + + <script src="static/js/libs/jquery-1.9.0.js"></script> + {##} + <script src="static/js/libs/bootstrap-2.2.2.js"></script> + <script type="text/javascript"> + $('#info').popover({ + placement: 'left', + trigger: 'click', + html:'true', + }); + + $('.dropdown-toggle').dropdown(); + + $("#btn_newuser").click(function() { + + str = "<div class='dropdown1'><span class='label name'>User</span><a class='dropdown-toggle' data-toggle='dropdown1' href='#'><i class='iconf-user iconf-8x'></i></a><ul class='dropdown-menu' role='menu' aria-labelledby='dropdownMenu'><li><a tabindex='-1' href='#'>Action</a></li><li><a tabindex='-1' href='#'>Another action</a></li><li><a tabindex='-1' href='#'>Something else here</a></li><li class='divider'></li><li><a tabindex='-1' href='#'>Separated link</a></li></ul></div>"; + + $("#userlist").append(str); + + }); + + $("#useredit").click(function() { + $('#event-modal').modal(); + }); + $("#useredit_close").click(function() { + $('#event-modal').modal('hide'); + }); + $("#useredit_save").click(function() { + $('#event-modal').modal('hide'); + }); + + </script> +{% endblock %}
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dashboard/actionbar.html b/pyload/web/app/templates/default/dashboard/actionbar.html new file mode 100644 index 000000000..a8b2ebecd --- /dev/null +++ b/pyload/web/app/templates/default/dashboard/actionbar.html @@ -0,0 +1,54 @@ +<div class="span2 offset1"> +</div> +<ul class="actionbar nav nav-pills span9"> + <li> + <ul class="breadcrumb"> + <li><a href="#">Local</a> <span class="divider">/</span></li> + <li class="active"></li> + </ul> + </li> + + <li style="float: right;"> + <form class="form-search" action="#"> + <div class="input-append"> + <input type="text" class="search-query" style="width: 120px"> + <button type="submit" class="btn">Search</button> + </div> + </form> + </li> + <li style="float: right" class="li-check"> + <a href="#"><i class="icon-check-empty btn-check"></i></a> + </li> + <li class="dropdown" style="float: right;"> + <a class="dropdown-toggle type" + data-toggle="dropdown" + href="#"> + Type + <b class="caret"></b> + </a> + <ul class="dropdown-menu"> + <li><a class="filter-type" data-type="2" href="#"><i class="icon-ok"></i> Audio</a></li> + <li><a class="filter-type" data-type="4" href="#"><i class="icon-ok"></i> Image</a></li> + <li><a class="filter-type" data-type="8" href="#"><i class="icon-ok"></i> Video</a></li> + <li><a class="filter-type" data-type="16" href="#"><i class="icon-ok"></i> Document</a></li> + <li><a class="filter-type" data-type="32" href="#"><i class="icon-remove"></i> Archive</a></li> + <li><a class="filter-type" data-type="1" href="#"><i class="icon-remove"></i> Other</a></li> + </ul> + </li> + <li class="dropdown" style="float: right;"> + <a class="dropdown-toggle" + data-toggle="dropdown" + href="#"> + <span class="state"> + All + </span> + <b class="caret"></b> + </a> + <ul class="dropdown-menu"> + <li><a class="filter-state" data-state="0" href="#">All</a></li> + <li><a class="filter-state" data-state="1" href="#">Finished</a></li> + <li><a class="filter-state" data-state="2" href="#">Unfinished</a></li> + <li><a class="filter-state" data-state="3" href="#">Failed</a></li> + </ul> + </li> +</ul>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dashboard/file.html b/pyload/web/app/templates/default/dashboard/file.html new file mode 100644 index 000000000..4bf3c7a97 --- /dev/null +++ b/pyload/web/app/templates/default/dashboard/file.html @@ -0,0 +1,34 @@ +<div class="file-row first span6"> + <i class="checkbox"></i> + <span class="name"> + {{ name }} + </span> +</div> +<div class="file-row second span3 {{ fileClass this }}"> + {{ fileStatus this }} +</div> + +<div class="file-row third span3 pull-right"> + <i class="{{ fileIcon media }}"></i> + {{ formatSize size }} + <span class="pull-right"> + <img src="{{ pluginIcon download.plugin }}"/> + {{ download.plugin }} + <i class="icon-chevron-down" data-toggle="dropdown"></i> + <ul class="dropdown-menu" role="menu"> + <li><a href="#" class="btn-delete"><i class="icon-trash"></i> Delete</a></li> + <li><a href="#" class="btn-restart"><i class="icon-refresh"></i> Restart</a></li> + <!--{# TODO: only show when finished #}--> + <li><a href="download/{{ fid }}" target="_blank" class="btn-dowload"><i class="icon-download"></i> + Download</a></li> + <li><a href="#" class="btn-share"><i class="icon-share"></i> Share</a></li> + <li class="divider"></li> + <li class="dropdown-submenu pull-left"> + <a>Addons</a> + <ul class="dropdown-menu"> + <li><a>Test</a></li> + </ul> + </li> + </ul> + </span> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dashboard/layout.html b/pyload/web/app/templates/default/dashboard/layout.html new file mode 100644 index 000000000..945d11762 --- /dev/null +++ b/pyload/web/app/templates/default/dashboard/layout.html @@ -0,0 +1,35 @@ +<div class="span3"> + <div class="sidebar-header"> + <i class="icon-hdd"></i> Local + <div class="pull-right" style="font-size: medium; line-height: normal"> + <i class="icon-chevron-down" style="font-size: 20px"></i> + </div> + <div class="clearfix"></div> + </div> + <ul class="package-list"> + + </ul> + <div class="sidebar-header"> + <i class="icon-group"></i> Shared + </div> + <ul class="package-list"> + <li>Content from</li> + <li>Other user</li> + <li>which they shared</li> + </ul> + <div class="sidebar-header"> + <i class="icon-sitemap"></i> Remote + </div> + <ul> + <li>Content from</li> + <li>remote sites</li> + <li>mega</li> + <li>dropbox</li> + <li>other pyloads</li> + </ul> +</div> +<div class="span9"> + <ul class="file-list"> + + </ul> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dashboard/package.html b/pyload/web/app/templates/default/dashboard/package.html new file mode 100644 index 000000000..0f2496046 --- /dev/null +++ b/pyload/web/app/templates/default/dashboard/package.html @@ -0,0 +1,50 @@ +{{#if selected }} + <i class="icon-check select"></i> + {{ else }} + <i class="icon-check-empty select"></i> + {{/if}} + <span class="package-name"> + {{ name }} + </span> + + <div class="package-frame"> + <div class="tag-area"> + <span class="badge badge-success"><i class="icon-tag"></i>video</span> + <span class="badge badge-success badge-ghost"><i class="icon-tag"></i> Add Tag</span> + </div> + <div class="package-indicator"> + <i class="icon-plus-sign btn-move" data-toggle="tooltip" title="Move files here"></i> + <i class="icon-pause" data-toggle="tooltip" title="Pause Package"></i> + <i class="icon-refresh" data-toggle="tooltip" title="Restart Package"></i> + {{#if shared }} + <i class="icon-eye-open" data-toggle="tooltip" title="Package is public"></i> + {{ else }} + <i class="icon-eye-close" data-toggle="tooltip" title="Package is private"></i> + {{/if}} + <i class="icon-chevron-down" data-toggle="dropdown"> + </i> + <ul class="dropdown-menu" role="menu"> + <li><a href="#" class="btn-open"><i class="icon-folder-open-alt"></i> Open</a></li> + <li><a href="#"><i class="icon-plus-sign"></i> Add links</a></li> + <li><a href="#"><i class="icon-edit"></i> Details</a></li> + <li><a href="#" class="btn-delete"><i class="icon-trash"></i> Delete</a></li> + <li><a href="#" class="btn-recheck"><i class="icon-refresh"></i> Recheck</a></li> + <li class="divider"></li> + <li class="dropdown-submenu"> + <a>Addons</a> + <ul class="dropdown-menu"> + <li><a>Test</a></li> + </ul> + </li> + </ul> + </div> + <div class="progress"> + <span style="position: absolute; left: 5px"> + {{ stats.linksdone }} / {{ stats.linkstotal }} + </span> + <div class="bar bar-info" style="width: {{ percent }}%"></div> + <span style="position: absolute; right: 5px"> + {{ formatSize stats.sizedone }} / {{ formatSize stats.sizetotal }} + </span> + </div> + </div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dashboard/select.html b/pyload/web/app/templates/default/dashboard/select.html new file mode 100644 index 000000000..f4c696d11 --- /dev/null +++ b/pyload/web/app/templates/default/dashboard/select.html @@ -0,0 +1,11 @@ +<i class="icon-check" data-toggle="tooltip" title="Deselect"></i> +{{#if packs }}{{ ngettext "1 package" "%d packages" packs }}{{/if}} +{{#if files}} +{{#if packs}}, {{/if}} +{{ ngettext "1 file" "%d files" files }} +{{/if }} +selected + | +<i class="icon-pause" data-toggle="tooltip" title="Pause"></i> +<i class="icon-trash" data-toggle="tooltip" title="Delete"></i> +<i class="icon-refresh" data-toggle="tooltip" title="Restart"></i>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dialogs/addAccount.html b/pyload/web/app/templates/default/dialogs/addAccount.html new file mode 100755 index 000000000..bdc8a609a --- /dev/null +++ b/pyload/web/app/templates/default/dialogs/addAccount.html @@ -0,0 +1,42 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Add an account</h3> +</div> +<div class="modal-body"> + <form class="form-horizontal" autocomplete="off"> + <legend> + Please enter your account data + </legend> + <div class="control-group"> + <label class="control-label" for="pluginSelect"> + Plugin + </label> + + <div class="controls"> + <input type="hidden" id="pluginSelect"> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="login"> + Loginname + </label> + + <div class="controls"> + <input type="text" id="login"> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="password"> + Password + </label> + + <div class="controls"> + <input type="password" id="password"> + </div> + </div> + </form> +</div> +<div class="modal-footer"> + <a class="btn btn-success btn-add">Add</a> + <a class="btn btn-close">Close</a> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dialogs/addPluginConfig.html b/pyload/web/app/templates/default/dialogs/addPluginConfig.html new file mode 100755 index 000000000..e7a42a208 --- /dev/null +++ b/pyload/web/app/templates/default/dialogs/addPluginConfig.html @@ -0,0 +1,26 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3> + Choose a plugin + </h3> +</div> +<div class="modal-body"> + <form class="form-horizontal"> + <legend> + Please choose a plugin, which you want to configure + </legend> + <div class="control-group"> + <label class="control-label" for="pluginSelect"> + Plugin + </label> + + <div class="controls"> + <input type="hidden" id="pluginSelect"> + </div> + </div> + </form> +</div> +<div class="modal-footer"> + <a class="btn btn-success btn-add">Add</a> + <a class="btn btn-close">Close</a> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dialogs/confirmDelete.html b/pyload/web/app/templates/default/dialogs/confirmDelete.html new file mode 100644 index 000000000..65ae1cb21 --- /dev/null +++ b/pyload/web/app/templates/default/dialogs/confirmDelete.html @@ -0,0 +1,11 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Please confirm</h3> +</div> +<div class="modal-body"> + Do you want to delete the selected items? +</div> +<div class="modal-footer"> + <a class="btn btn-danger btn-confirm"><i class="icon-trash icon-white"></i> Delete</a> + <a class="btn btn-close">Cancel</a> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dialogs/interactionTask.html b/pyload/web/app/templates/default/dialogs/interactionTask.html new file mode 100755 index 000000000..a152a5046 --- /dev/null +++ b/pyload/web/app/templates/default/dialogs/interactionTask.html @@ -0,0 +1,37 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3> + {{ title }} + <small style="background: url('{{ pluginIcon plugin }}') no-repeat right 0; background-size: 20px; padding-right: 22px"> + {{ plugin }} + </small> + </h3> +</div> +<div class="modal-body"> + <form class="form-horizontal" action="#"> + <legend>{{ description }}</legend> + {{#if captcha }} + <div class="control-group"> + <label class="control-label" for="captchaImage"> + Captcha Image + </label> + + <div class="controls"> + <img id="captchaImage" src="data:image/{{ type }};base64,{{ captcha }}"> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="inputField">Captcha Text</label> + + <div class="controls" id="inputField"> + </div> + </div> + {{ else }} + {{ content }} + {{/if}} + </form> +</div> +<div class="modal-footer"> + <a class="btn btn-success">Submit</a> + <a class="btn btn-close">Close</a> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dialogs/linkgrabber.html b/pyload/web/app/templates/default/dialogs/linkgrabber.html new file mode 100755 index 000000000..08418cf03 --- /dev/null +++ b/pyload/web/app/templates/default/dialogs/linkgrabber.html @@ -0,0 +1,49 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3> + AddPackage + <small>paste&add links to pyLoad</small> + </h3> +</div> + +<div class="modal-body"> + <div class="alert alert-error hidden"> + Upload files container failed. Please try again. + </div> + <form class="form-horizontal"> + <div class="control-group"> + <label class="control-label" for="inputPackageName">Package name</label> + + <div class="controls"> + <input type="text" class="span4" id="inputPackageName" placeholder="Name of your package"> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="inputLinks">Links</label> + + <div class="controls"> + <textarea id="inputLinks" class="span4" rows="10" placeholder="Paste your links here..."></textarea> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="inputPassword">Password</label> + + <div class="controls"> + <input type="text" id="inputPassword" class="span4" placeholder="Password for .rar files"> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="inputContainer">Upload links container</label> + + <div class="controls controls-row"> + <input type="text" id="inputContainer" class="span3" placeholder="Path to your container"> + <button id="inputContainer-btn" class="btn span1" type="button">Browse…</button> + </div> + </div> + </form> +</div> + +<div class="modal-footer"> + <a class="btn btn-success"><i class="icon-plus icon-white"></i> Add</a> + <a class="btn btn-close">Close</a> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/dialogs/modal.html b/pyload/web/app/templates/default/dialogs/modal.html new file mode 100755 index 000000000..1e44cc99c --- /dev/null +++ b/pyload/web/app/templates/default/dialogs/modal.html @@ -0,0 +1,10 @@ +<div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3>Dialog</h3> +</div> +<div class="modal-body"> +</div> +<div class="modal-footer"> + <a class="btn btn-close">Close</a> + <a class="btn btn-primary">Save</a> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/header/layout.html b/pyload/web/app/templates/default/header/layout.html new file mode 100644 index 000000000..890a5b018 --- /dev/null +++ b/pyload/web/app/templates/default/header/layout.html @@ -0,0 +1,62 @@ +<div class="span3"> + <div class="logo"></div> + <span class="title visible-large-screen">pyLoad</span> +</div> +<div class="span4 offset1"> + <div id="progress-area"> + <span id="progress-info"> + </span> + + <div class="popover bottom"> + <div class="arrow"></div> + <div class="popover-inner"> + <h3 class="popover-title"> + Running... + <button type="button" class="close" aria-hidden="true">×</button> + </h3> + <div class="popover-content"> + <ul class="progress-list"></ul> + </div> + </div> + </div> + </div> +</div> +<div class="span4"> + <div class="header-block"> + <i class="icon-download-alt icon-white"></i> Max. Speed:<br> + <i class="icon-off icon-white"></i> Running:<br> + <i class="icon-refresh icon-white"></i> Reconnect:<br> + </div> + + <div class="header-block status-block"></div> + + <div class="header-btn"> + <div class="btn-group"> + <a class="btn btn-blue btn-small" href="#"><i class="icon-user icon-white"></i> User</a> + <a class="btn btn-blue btn-small dropdown-toggle" data-toggle="dropdown" href="#"><span + class="caret"></span></a> + <ul class="dropdown-menu" style="right: 0; left: -100%"> + <li><a data-nav href="/"><i class="icon-list-alt"></i> Dashboard</a></li> + <li><a data-nav href="/settings"><i class="icon-wrench"></i> Settings</a></li> + <li><a data-nav href="/accounts"><i class="icon-key"></i> Accounts</a></li> + <li><a data-nav href="/admin"><i class="icon-cogs"></i> Admin</a></li> + <li class="divider"></li> + <li><a data-nav href="/logout"><i class="icon-signout"></i> Logout</a></li> + </ul> + </div> + <div class="btn-group lower"> + <button class="btn btn-success btn-grabber btn-mini" href="#"> + <i class="icon-plus icon-white"></i> + </button> + <button class="btn btn-blue btn-play btn-mini" href="#"> + <i class="icon-play icon-white"></i> + </button> + <button class="btn btn-danger btn-delete btn-mini" href="#"> + <i class="icon-remove icon-white"></i> + </button> + </div> + </div> +<span class="visible-desktop speedgraph-container"> + <div id="speedgraph"></div> +</span> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/header/progress.html b/pyload/web/app/templates/default/header/progress.html new file mode 100644 index 000000000..4ef5a1cd6 --- /dev/null +++ b/pyload/web/app/templates/default/header/progress.html @@ -0,0 +1,14 @@ +{{ name }} +<span class="pull-right">{{ plugin }}</span> + +<div class="progress"> + <div class="bar" style="width: {{ percent }}%"></div> +</div> +{{#if downloading }} +{{ formatSize done }} of {{ formatSize total }} ({{ formatSize download.speed }}/s) +{{ else }} +{{ statusmsg }} +{{/if}} +<span class="pull-right"> + {{ formatTime eta }} +</span>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/header/progressbar.html b/pyload/web/app/templates/default/header/progressbar.html new file mode 100644 index 000000000..a36fb5684 --- /dev/null +++ b/pyload/web/app/templates/default/header/progressbar.html @@ -0,0 +1,27 @@ +{{#if single }} + {{ truncate name 32}} ({{ statusmsg }}) +{{ else }} + {{#if downloads }} + {{ downloads }} downloads running {{#if speed }}({{ formatSize speed }}/s){{/if}} + {{ else }} + No running tasks + {{/if}} +{{/if}} +<i class="icon-list pull-right"></i> +<!-- TODO active animation --> +<div class="progress" id="globalprogress"> + {{#if single }} + <div class="bar" style="width: {{ percent }}%"> + {{ else }} + <div class="bar {{#if downloads }}running{{/if}}"> + {{/if}} + </div> + </div> + <div class="sub"> + {{#if linksqueue }} + {{ linksqueue }} downloads left ({{ formatSize sizequeue }}) + {{/if}} + <span class="pull-right"> + {{ formatTime etaqueue }} + </span> + </div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/header/status.html b/pyload/web/app/templates/default/header/status.html new file mode 100644 index 000000000..f840b6e33 --- /dev/null +++ b/pyload/web/app/templates/default/header/status.html @@ -0,0 +1,3 @@ +<span class="pull-right maxspeed">{{ formatSize maxspeed }}/s</span><br> +<span class="pull-right running">{{ paused }}</span><br> +<span class="pull-right reconnect">{{#if reconnect }}true{{ else }}false{{/if}}</span>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/login.html b/pyload/web/app/templates/default/login.html new file mode 100644 index 000000000..9e8d9eeb6 --- /dev/null +++ b/pyload/web/app/templates/default/login.html @@ -0,0 +1,28 @@ +<br> +<div class="login"> + <form method="post" class="form-horizontal"> + <legend>Login</legend> + <div class="control-group"> + <label class="control-label" for="inputUser">Username</label> + <div class="controls"> + <input type="text" id="inputUser" placeholder="Username" name="username"> + </div> + </div> + <div class="control-group"> + <label class="control-label" for="inputPassword">Password</label> + <div class="controls"> + <input type="password" id="inputPassword" placeholder="Password" name="password"> + </div> + </div> + <div class="control-group"> + <div class="controls"> + <label class="checkbox"> + <input type="checkbox"> Remember me + </label> + <button type="submit" class="btn">Login</button> + </div> + </div> + </form> +</div> +<br> +<!-- TODO: Errors --> diff --git a/pyload/web/app/templates/default/notification.html b/pyload/web/app/templates/default/notification.html new file mode 100644 index 000000000..1b6d21e27 --- /dev/null +++ b/pyload/web/app/templates/default/notification.html @@ -0,0 +1,10 @@ +{{#if queries }} + <span class="btn-query"> + Queries <span class="badge badge-info">{{ queries }}</span> + </span> +{{/if}} +{{#if notifications }} + <span class="btn-notification"> + Notifications <span class="badge badge-success">{{ notifications }}</span> + </span> +{{/if}}
\ No newline at end of file diff --git a/pyload/web/app/templates/default/settings/actionbar.html b/pyload/web/app/templates/default/settings/actionbar.html new file mode 100644 index 000000000..25b10d463 --- /dev/null +++ b/pyload/web/app/templates/default/settings/actionbar.html @@ -0,0 +1,5 @@ +<div class="span2 offset1"> +</div> +<span class="span9"> + <button class="btn btn-small btn-blue btn-add">Add Plugin</button> +</span>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/settings/config.html b/pyload/web/app/templates/default/settings/config.html new file mode 100644 index 000000000..47ff45f0b --- /dev/null +++ b/pyload/web/app/templates/default/settings/config.html @@ -0,0 +1,17 @@ +<legend> + <div class="page-header"> + <h1>{{ label }} + <small>{{ description }}</small> + {{#if long_description }} + <a class="btn btn-small" data-title="Help" data-content="{{ long_description }}"><i + class="icon-question-sign"></i></a> + {{/if}} + </h1> + </div> +</legend> +<div class="control-content"> +</div> +<div class="form-actions"> + <button type="button" class="btn btn-primary">Save changes</button> + <button type="button" class="btn btn-reset">Reset</button> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/settings/configItem.html b/pyload/web/app/templates/default/settings/configItem.html new file mode 100644 index 000000000..5b583b8df --- /dev/null +++ b/pyload/web/app/templates/default/settings/configItem.html @@ -0,0 +1,7 @@ + <div class="control-group"> + <label class="control-label">{{ label }}</label> + + <div class="controls"> + <!--{# <span class="help-inline">{{ description }}</span>#}--> + </div> + </div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/settings/layout.html b/pyload/web/app/templates/default/settings/layout.html new file mode 100644 index 000000000..39f1a2ec9 --- /dev/null +++ b/pyload/web/app/templates/default/settings/layout.html @@ -0,0 +1,11 @@ +<div class="span2"> + <ul class="nav nav-list well settings-menu"> + </ul> +</div> +<div class="span10"> + <div class="well setting-box"> + <form class="form-horizontal" action="#"> + <h1>Please choose a config section</h1> + </form> + </div> +</div>
\ No newline at end of file diff --git a/pyload/web/app/templates/default/settings/menu.html b/pyload/web/app/templates/default/settings/menu.html new file mode 100644 index 000000000..893fd7b5b --- /dev/null +++ b/pyload/web/app/templates/default/settings/menu.html @@ -0,0 +1,40 @@ +{{#if core}} +<li class="nav-header"><i class="icon-globe icon-white"></i> General</li> +{{#each core}} +<li data-name="{{ name }}"><a href="#">{{ label }}</a></li> +{{/each}} +{{/if}} +<li class="divider"></li> +<li class="nav-header"><i class="icon-th-large icon-white"></i> Addons</li> +{{#each addon }} +<li class="addon" data-name="{{ name }}"> + <a href="#" style="background-image: url({{ pluginIcon name }});"> + {{ label }} + <i class="icon-remove pull-right"></i> + {{#if activated }} + <div class="addon-on"> + active + {{else}} + <div class="addon-off"> + inactive + {{/if}} + {{#if user_context }} + <!--{# TODO: tooltip #}--> + <i class="icon-user pull-right"></i> + {{else}} + <i class="icon-globe pull-right"></i> + {{/if}} + </div> + </a> +</li> +{{/each}} +<li class="divider"></li> +<li class="nav-header"><i class="icon-th-list icon-white"></i> Plugin Configs</li> +{{#each plugin }} +<li class="plugin" data-name="{{ name }}"> + <a style="background-image: url({{ pluginIcon name }});"> + {{ label }} + <i class="icon-remove pull-right"></i> + </a> +</li> +{{/each}}
\ No newline at end of file diff --git a/pyload/web/app/templates/default/setup.html b/pyload/web/app/templates/default/setup.html new file mode 100644 index 000000000..e5c9f4b8c --- /dev/null +++ b/pyload/web/app/templates/default/setup.html @@ -0,0 +1,16 @@ +{% extends 'default/base.html' %} +{% block title %} + {{_("Setup")}} - {{ super()}} +{% endblock %} + +{% block content %} + <div class="hero-unit"> + <h1>You did it!</h1> + <p>pyLoad is running and ready for configuration.</p> + <p> + <a class="btn btn-primary btn-large"> + Go on + </a> + </p> + </div> +{% endblock %}
\ No newline at end of file diff --git a/pyload/web/app/unavailable.html b/pyload/web/app/unavailable.html new file mode 100644 index 000000000..6706a693c --- /dev/null +++ b/pyload/web/app/unavailable.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <title>WebUI not available</title> +</head> +<body> + +<h1>WebUI not available</h1> +You are running a pyLoad version without prebuilt webUI. You can download a build from our website or deactivate the dev mode. +If desired you can build it yourself by running: +<ul> + <li>npm install</li> + <li>bower install</li> + <li>grunt build</li> +</ul> + +</body> +</html>
\ No newline at end of file diff --git a/pyload/web/bower.json b/pyload/web/bower.json new file mode 100644 index 000000000..dfabc05d6 --- /dev/null +++ b/pyload/web/bower.json @@ -0,0 +1,22 @@ +{ + "name": "pyload", + "version": "0.1.0", + "dependencies": { + "pyload-common": "https://github.com/pyload/pyload-common.git", + "requirejs": "~2.1.6", + "requirejs-text": "*", + "require-handlebars-plugin": "*", + "jquery": "~1.9.1", + "jquery.transit": "~0.9.9", + "jquery.cookie": "~1.3.1", + "jquery.animate-enhanced": "*", + "flot": "~0.8.1", + "underscore": "~1.4.4", + "backbone": "~1.0.0", + "backbone.marionette": "~1.0.3", + "handlebars.js": "1.0.0-rc.3", + "jed": "~0.5.4", + "select2": "~3.4.0" + }, + "devDependencies": {} +} diff --git a/pyload/web/cnl_app.py b/pyload/web/cnl_app.py new file mode 100644 index 000000000..90aa76d72 --- /dev/null +++ b/pyload/web/cnl_app.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from os.path import join +import re +from urllib import unquote +from base64 import standard_b64decode +from binascii import unhexlify + +from pyload.utils.fs import save_filename + +from bottle import route, request, HTTPError +from webinterface import PYLOAD, DL_ROOT, JS + +try: + from Crypto.Cipher import AES +except: + pass + + +def local_check(function): + def _view(*args, **kwargs): + if request.environ.get('REMOTE_ADDR', "0") in ('127.0.0.1', 'localhost') \ + or request.environ.get('HTTP_HOST','0') in ('127.0.0.1:9666', 'localhost:9666'): + return function(*args, **kwargs) + else: + return HTTPError(403, "Forbidden") + + return _view + + +@route("/flash") +@route("/flash/:id") +@route("/flash", method="POST") +@local_check +def flash(id="0"): + return "JDownloader\r\n" + +@route("/flash/add", method="POST") +@local_check +def add(request): + package = request.POST.get('referer', None) + urls = filter(lambda x: x != "", request.POST['urls'].split("\n")) + + if package: + PYLOAD.addPackage(package, urls, 0) + else: + PYLOAD.generateAndAddPackages(urls, 0) + + return "" + +@route("/flash/addcrypted", method="POST") +@local_check +def addcrypted(): + + package = request.forms.get('referer', 'ClickAndLoad Package') + dlc = request.forms['crypted'].replace(" ", "+") + + dlc_path = join(DL_ROOT, save_filename(package) + ".dlc") + dlc_file = open(dlc_path, "wb") + dlc_file.write(dlc) + dlc_file.close() + + try: + PYLOAD.addPackage(package, [dlc_path], 0) + except: + return HTTPError() + else: + return "success\r\n" + +@route("/flash/addcrypted2", method="POST") +@local_check +def addcrypted2(): + + package = request.forms.get("source", None) + crypted = request.forms["crypted"] + jk = request.forms["jk"] + + crypted = standard_b64decode(unquote(crypted.replace(" ", "+"))) + if JS: + jk = "%s f()" % jk + jk = JS.eval(jk) + + else: + try: + jk = re.findall(r"return ('|\")(.+)('|\")", jk)[0][1] + except: + ## Test for some known js functions to decode + if jk.find("dec") > -1 and jk.find("org") > -1: + org = re.findall(r"var org = ('|\")([^\"']+)", jk)[0][1] + jk = list(org) + jk.reverse() + jk = "".join(jk) + else: + print "Could not decrypt key, please install py-spidermonkey or ossp-js" + + try: + Key = unhexlify(jk) + except: + print "Could not decrypt key, please install py-spidermonkey or ossp-js" + return "failed" + + IV = Key + + obj = AES.new(Key, AES.MODE_CBC, IV) + result = obj.decrypt(crypted).replace("\x00", "").replace("\r","").split("\n") + + result = filter(lambda x: x != "", result) + + try: + if package: + PYLOAD.addPackage(package, result, 0) + else: + PYLOAD.generateAndAddPackages(result, 0) + except: + return "failed can't add" + else: + return "success\r\n" + +@route("/flashgot_pyload") +@route("/flashgot_pyload", method="POST") +@route("/flashgot") +@route("/flashgot", method="POST") +@local_check +def flashgot(): + if request.environ['HTTP_REFERER'] != "http://localhost:9666/flashgot" and request.environ['HTTP_REFERER'] != "http://127.0.0.1:9666/flashgot": + return HTTPError() + + autostart = int(request.forms.get('autostart', 0)) + package = request.forms.get('package', None) + urls = filter(lambda x: x != "", request.forms['urls'].split("\n")) + folder = request.forms.get('dir', None) + + if package: + PYLOAD.addPackage(package, urls, autostart) + else: + PYLOAD.generateAndAddPackages(urls, autostart) + + return "" + +@route("/crossdomain.xml") +@local_check +def crossdomain(): + rep = "<?xml version=\"1.0\"?>\n" + rep += "<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n" + rep += "<cross-domain-policy>\n" + rep += "<allow-access-from domain=\"*\" />\n" + rep += "</cross-domain-policy>" + return rep + + +@route("/flash/checkSupportForUrl") +@local_check +def checksupport(): + + url = request.GET.get("url") + res = PYLOAD.checkURLs([url]) + supported = (not res[0][1] is None) + + return str(supported).lower() + +@route("/jdcheck.js") +@local_check +def jdcheck(): + rep = "jdownloader=true;\n" + rep += "var version='9.581;'" + return rep diff --git a/pyload/web/middlewares.py b/pyload/web/middlewares.py new file mode 100644 index 000000000..ae0911cc3 --- /dev/null +++ b/pyload/web/middlewares.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# gzip is optional on some platform +try: + import gzip +except ImportError: + gzip = None + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +class StripPathMiddleware(object): + def __init__(self, app): + self.app = app + + def __call__(self, e, h): + e['PATH_INFO'] = e['PATH_INFO'].rstrip('/') + return self.app(e, h) + + +class PrefixMiddleware(object): + def __init__(self, app, prefix="/pyload"): + self.app = app + self.prefix = prefix + + def __call__(self, e, h): + path = e["PATH_INFO"] + if path.startswith(self.prefix): + e['PATH_INFO'] = path.replace(self.prefix, "", 1) + return self.app(e, h) + +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + +# WSGI middleware +# Gzip-encodes the response. + +class GZipMiddleWare(object): + + def __init__(self, application, compress_level=6): + self.application = application + self.compress_level = int(compress_level) + + def __call__(self, environ, start_response): + if 'gzip' not in environ.get('HTTP_ACCEPT_ENCODING', ''): + # nothing for us to do, so this middleware will + # be a no-op: + return self.application(environ, start_response) + response = GzipResponse(start_response, self.compress_level) + app_iter = self.application(environ, + response.gzip_start_response) + if app_iter is not None: + response.finish_response(app_iter) + + return response.write() + +def header_value(headers, key): + for header, value in headers: + if key.lower() == header.lower(): + return value + +def update_header(headers, key, value): + remove_header(headers, key) + headers.append((key, value)) + +def remove_header(headers, key): + for header, value in headers: + if key.lower() == header.lower(): + headers.remove((header, value)) + break + +class GzipResponse(object): + + def __init__(self, start_response, compress_level): + self.start_response = start_response + self.compress_level = compress_level + self.buffer = StringIO() + self.compressible = False + self.content_length = None + self.headers = () + + def gzip_start_response(self, status, headers, exc_info=None): + self.headers = headers + ct = header_value(headers,'content-type') + ce = header_value(headers,'content-encoding') + cl = header_value(headers, 'content-length') + + # don't compress on unknown size, it may be too huge + cl = int(cl) if cl else 0 + + if ce: + self.compressible = False + elif gzip is not None and ct and (ct.startswith('text/') or ct.startswith('application/')) \ + and 'zip' not in ct and 200 < cl < 1024*1024: + self.compressible = True + headers.append(('content-encoding', 'gzip')) + headers.append(('vary', 'Accept-Encoding')) + + remove_header(headers, 'content-length') + self.headers = headers + self.status = status + return self.buffer.write + + def write(self): + out = self.buffer + out.seek(0) + s = out.getvalue() + out.close() + return [s] + + def finish_response(self, app_iter): + if self.compressible: + output = gzip.GzipFile(mode='wb', compresslevel=self.compress_level, + fileobj=self.buffer) + else: + output = self.buffer + try: + for s in app_iter: + output.write(s) + if self.compressible: + output.close() + finally: + if hasattr(app_iter, 'close'): + try: + app_iter.close() + except : + pass + + content_length = self.buffer.tell() + update_header(self.headers, "Content-Length" , str(content_length)) + self.start_response(self.status, self.headers)
\ No newline at end of file diff --git a/pyload/web/package.json b/pyload/web/package.json new file mode 100644 index 000000000..fdd7b62c4 --- /dev/null +++ b/pyload/web/package.json @@ -0,0 +1,36 @@ +{ + "name": "pyload", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "git://github.com/pyload/pyload.git" + }, + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-concat": "~0.1.3", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-jshint": "~0.4.1", + "grunt-contrib-less": "~0.5.2", + "grunt-contrib-cssmin": "~0.6.0", + "grunt-contrib-connect": "~0.2.0", + "grunt-contrib-clean": "~0.4.0", + "grunt-contrib-htmlmin": "~0.1.3", + "grunt-contrib-requirejs": "~0.4.0", + "grunt-contrib-imagemin": "~0.1.3", + "grunt-contrib-watch": "~0.4.0", + "grunt-rev": "~0.1.0", + "grunt-usemin": "~0.1.10", + "grunt-mocha": "~0.3.0", + "grunt-open": "~0.2.0", + "grunt-svgmin": "~0.1.0", + "grunt-concurrent": "~0.1.0", + "matchdep": "~0.1.1", + "rjs-build-analysis": "0.0.3", + "connect-livereload": "~0.2.0" + }, + "engines": { + "node": ">=0.8.0" + } +} diff --git a/pyload/web/pyload_app.py b/pyload/web/pyload_app.py new file mode 100644 index 000000000..2f6211621 --- /dev/null +++ b/pyload/web/pyload_app.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, + or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, see <http://www.gnu.org/licenses/>. + + @author: RaNaN +""" +import time +from os.path import join + +from bottle import route, static_file, response, redirect, template + +from webinterface import PYLOAD, PROJECT_DIR, SETUP, APP_PATH, UNAVAILALBE + +from utils import login_required + + +@route('/icons/<path:path>') +def serve_icon(path): + # TODO + return redirect('/images/icon.png') + # return static_file(path, root=join("tmp", "icons")) + + +@route("/download/:fid") +@login_required('Download') +def download(fid, api): + path, name = api.getFilePath(fid) + return static_file(name, path, download=True) + + +@route('/') +def index(): + if UNAVAILALBE: + return server_static("unavailable.html") + + if SETUP: + # TODO show different page + pass + + f = server_static('index.html') + content = f.body.read() + f.body = template(content, ws=PYLOAD.getWSAddress(), web=PYLOAD.getConfigValue('webinterface', 'port')) + + return f + +# Very last route that is registered, could match all uris +@route('/<path:path>') +def server_static(path): + response.headers['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", + time.gmtime(time.time() + 60 * 60 * 24 * 7)) + response.headers['Cache-control'] = "public" + resp = static_file(path, root=join(PROJECT_DIR, APP_PATH)) + # Also serve from .tmp folder in dev mode + if resp.status_code == 404 and APP_PATH == "app": + return static_file(path, root=join(PROJECT_DIR, '.tmp')) + + return resp
\ No newline at end of file diff --git a/pyload/web/servers.py b/pyload/web/servers.py new file mode 100644 index 000000000..a3c51e36b --- /dev/null +++ b/pyload/web/servers.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from bottle import ServerAdapter as BaseAdapter + +class ServerAdapter(BaseAdapter): + SSL = False + NAME = "" + + def __init__(self, host, port, key, cert, connections, debug, **kwargs): + BaseAdapter.__init__(self, host, port, **kwargs) + self.key = key + self.cert = cert + self.connection = connections + self.debug = debug + + @classmethod + def find(cls): + """ Check if server is available by trying to import it + + :raises Exception: importing C dependant library could also fail with other reasons + :return: True on success + """ + try: + __import__(cls.NAME) + return True + except ImportError: + return False + + def run(self, handler): + raise NotImplementedError + + +class CherryPyWSGI(ServerAdapter): + SSL = True + NAME = "threaded" + + @classmethod + def find(cls): + return True + + def run(self, handler): + from wsgiserver import CherryPyWSGIServer + + if self.cert and self.key: + CherryPyWSGIServer.ssl_certificate = self.cert + CherryPyWSGIServer.ssl_private_key = self.key + + server = CherryPyWSGIServer((self.host, self.port), handler, numthreads=self.connection) + server.start() + + +class FapwsServer(ServerAdapter): + """ Does not work very good currently """ + + NAME = "fapws" + + def run(self, handler): # pragma: no cover + import fapws._evwsgi as evwsgi + from fapws import base, config + + port = self.port + if float(config.SERVER_IDENT[-2:]) > 0.4: + # fapws3 silently changed its API in 0.5 + port = str(port) + evwsgi.start(self.host, port) + evwsgi.set_base_module(base) + + def app(environ, start_response): + environ['wsgi.multiprocess'] = False + return handler(environ, start_response) + + evwsgi.wsgi_cb(('', app)) + evwsgi.run() + + +# TODO: ssl +class MeinheldServer(ServerAdapter): + SSL = True + NAME = "meinheld" + + def run(self, handler): + from meinheld import server + + if self.quiet: + server.set_access_logger(None) + server.set_error_logger(None) + + server.listen((self.host, self.port)) + server.run(handler) + +# todo:ssl +class TornadoServer(ServerAdapter): + """ The super hyped asynchronous server by facebook. Untested. """ + + SSL = True + NAME = "tornado" + + def run(self, handler): # pragma: no cover + import tornado.wsgi, tornado.httpserver, tornado.ioloop + + container = tornado.wsgi.WSGIContainer(handler) + server = tornado.httpserver.HTTPServer(container) + server.listen(port=self.port) + tornado.ioloop.IOLoop.instance().start() + + +class BjoernServer(ServerAdapter): + """ Fast server written in C: https://github.com/jonashaag/bjoern """ + + NAME = "bjoern" + + def run(self, handler): + from bjoern import run + + run(handler, self.host, self.port) + + +# todo: ssl +class EventletServer(ServerAdapter): + + SSL = True + NAME = "eventlet" + + def run(self, handler): + from eventlet import wsgi, listen + + try: + wsgi.server(listen((self.host, self.port)), handler, + log_output=(not self.quiet)) + except TypeError: + # Needed to ignore the log + class NoopLog: + def write(self, *args): + pass + + # Fallback, if we have old version of eventlet + wsgi.server(listen((self.host, self.port)), handler, log=NoopLog()) + + +class FlupFCGIServer(ServerAdapter): + + SSL = False + NAME = "flup" + + def run(self, handler): # pragma: no cover + import flup.server.fcgi + from flup.server.threadedserver import ThreadedServer + + def noop(*args, **kwargs): + pass + + # Monkey patch signal handler, it does not work from threads + ThreadedServer._installSignalHandlers = noop + + self.options.setdefault('bindAddress', (self.host, self.port)) + flup.server.fcgi.WSGIServer(handler, **self.options).run() + +# Order is important and gives every server precedence over others! +all_server = [BjoernServer, TornadoServer, EventletServer, CherryPyWSGI] +# Some are deactivated because they have some flaws +##all_server = [FapwsServer, MeinheldServer, BjoernServer, TornadoServer, EventletServer, CherryPyWSGI]
\ No newline at end of file diff --git a/pyload/web/setup_app.py b/pyload/web/setup_app.py new file mode 100644 index 000000000..cd44ad08e --- /dev/null +++ b/pyload/web/setup_app.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from bottle import route, request, response, HTTPError, redirect + +from webinterface import PROJECT_DIR, SETUP + +def setup_required(func): + def _view(*args, **kwargs): + # setup needs to be running + if SETUP is None: + redirect("/nopermission") + + return func(*args, **kwargs) + return _view + + +@route("/setup") +@setup_required +def setup(): + pass # TODO diff --git a/pyload/web/utils.py b/pyload/web/utils.py new file mode 100644 index 000000000..dae987f84 --- /dev/null +++ b/pyload/web/utils.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import re +from bottle import request, HTTPError, redirect + +from webinterface import PYLOAD, SETUP + + +def set_session(request, user): + s = request.environ.get('beaker.session') + s["uid"] = user.uid + s.save() + return s + + +def get_user_api(s): + if s: + uid = s.get("uid", None) + if (uid is not None) and (PYLOAD is not None): + return PYLOAD.withUserContext(uid) + return None + + +def is_mobile(): + if request.get_cookie("mobile"): + if request.get_cookie("mobile") == "True": + return True + else: + return False + mobile_ua = request.headers.get('User-Agent', '').lower() + if mobile_ua.find('opera mini') > 0: + return True + if mobile_ua.find('windows') > 0: + return False + if request.headers.get('Accept', '').lower().find('application/vnd.wap.xhtml+xml') > 0: + return True + if re.search('(up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone|android)', mobile_ua) is not None: + return True + mobile_ua = mobile_ua[:4] + mobile_agents = ['w3c ', 'acs-', 'alav', 'alca', 'amoi', 'audi', 'avan', 'benq', 'bird', 'blac', 'blaz', 'brew', + 'cell', 'cldc', 'cmd-', + 'dang', 'doco', 'eric', 'hipt', 'inno', 'ipaq', 'java', 'jigs', 'kddi', 'keji', 'leno', 'lg-c', + 'lg-d', 'lg-g', 'lge-', + 'maui', 'maxo', 'midp', 'mits', 'mmef', 'mobi', 'mot-', 'moto', 'mwbp', 'nec-', 'newt', 'noki', + 'palm', 'pana', 'pant', + 'phil', 'play', 'port', 'prox', 'qwap', 'sage', 'sams', 'sany', 'sch-', 'sec-', 'send', 'seri', + 'sgh-', 'shar', 'sie-', + 'siem', 'smal', 'smar', 'sony', 'sph-', 'symb', 't-mo', 'teli', 'tim-', 'tosh', 'tsm-', 'upg1', + 'upsi', 'vk-v', 'voda', + 'wap-', 'wapa', 'wapi', 'wapp', 'wapr', 'webc', 'winw', 'winw', 'xda ', 'xda-'] + if mobile_ua in mobile_agents: + return True + return False + + +def login_required(perm=None): + def _dec(func): + def _view(*args, **kwargs): + + # In case of setup, no login methods can be accessed + if SETUP is not None: + redirect("/setup") + + s = request.environ.get('beaker.session') + api = get_user_api(s) + if api is not None: + if perm: + if api.user.hasPermission(perm): + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return HTTPError(403, "Forbidden") + else: + return redirect("/nopermission") + + kwargs["api"] = api + return func(*args, **kwargs) + else: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return HTTPError(403, "Forbidden") + else: + return redirect("/login") + + return _view + + return _dec diff --git a/pyload/web/webinterface.py b/pyload/web/webinterface.py new file mode 100644 index 000000000..206603f27 --- /dev/null +++ b/pyload/web/webinterface.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +############################################################################### +# Copyright(c) 2008-2013 pyLoad Team +# http://www.pyload.org +# +# This file is part of pyLoad. +# pyLoad is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Subjected to the terms and conditions in LICENSE +# +# @author: RaNaN +############################################################################### + +import sys + +from os.path import join, abspath, dirname, exists + +PROJECT_DIR = abspath(dirname(__file__)) +PYLOAD_DIR = abspath(join(PROJECT_DIR, "..", "..")) + +import bottle +from bottle import run, app + +from middlewares import StripPathMiddleware, GZipMiddleWare, PrefixMiddleware + +SETUP = None +PYLOAD = None + +import ServerThread + +if not ServerThread.core: + if ServerThread.setup: + SETUP = ServerThread.setup + config = SETUP.config + else: + raise Exception("Could not access pyLoad Core") +else: + PYLOAD = ServerThread.core.api + config = ServerThread.core.config + +from pyload.utils.JsEngine import JsEngine +JS = JsEngine() + +TEMPLATE = config.get('webinterface', 'template') +DL_ROOT = config.get('general', 'download_folder') +PREFIX = config.get('webinterface', 'prefix') + +if PREFIX: + PREFIX = PREFIX.rstrip("/") + if PREFIX and not PREFIX.startswith("/"): + PREFIX = "/" + PREFIX + +APP_PATH = "dist" +UNAVAILALBE = False + +# webUI build is available +if exists(join(PROJECT_DIR, "app", "components")) and exists(join(PROJECT_DIR, ".tmp")) and config.get('webinterface', 'develop'): + APP_PATH = "app" +elif not exists(join(PROJECT_DIR, "dist", "index.html")): + UNAVAILALBE = True + +DEBUG = config.get("general", "debug_mode") or "-d" in sys.argv or "--debug" in sys.argv +bottle.debug(DEBUG) + + +# Middlewares +from beaker.middleware import SessionMiddleware + +session_opts = { + 'session.type': 'file', + 'session.cookie_expires': False, + 'session.data_dir': './tmp', + 'session.auto': False +} + +session = SessionMiddleware(app(), session_opts) +web = StripPathMiddleware(session) +web = GZipMiddleWare(web) + +if PREFIX: + web = PrefixMiddleware(web, prefix=PREFIX) + +import api_app +import cnl_app +import setup_app +# Last routes to register, +import pyload_app + +# Server Adapter +def run_server(host, port, server): + run(app=web, host=host, port=port, quiet=True, server=server) + + +if __name__ == "__main__": + run(app=web, port=8001) |