diff options
author | RaNaN <Mast3rRaNaN@hotmail.de> | 2013-06-09 18:10:22 +0200 |
---|---|---|
committer | RaNaN <Mast3rRaNaN@hotmail.de> | 2013-06-09 18:10:23 +0200 |
commit | 16af85004c84d0d6c626b4f8424ce9647669a0c1 (patch) | |
tree | 025d479862d376dbc17e934f4ed20031c8cd97d1 /pyload/web | |
parent | adapted to jshint config (diff) | |
download | pyload-16af85004c84d0d6c626b4f8424ce9647669a0c1.tar.xz |
moved everything from module to pyload
Diffstat (limited to 'pyload/web')
126 files changed, 11475 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/.jshintrc b/pyload/web/.jshintrc new file mode 100644 index 000000000..0cff430d9 --- /dev/null +++ b/pyload/web/.jshintrc @@ -0,0 +1,29 @@ +{ + "node": true, + "browser": true, + "esnext": true, + "bitwise": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 4, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "unused": false, + "camelcase": false, + "-W030": false, + "-W015": false, + "-W116": false, + "predef": [ + "require", + "define", + "alert" + ] +} diff --git a/pyload/web/Gruntfile.js b/pyload/web/Gruntfile.js new file mode 100644 index 000000000..4799afb01 --- /dev/null +++ b/pyload/web/Gruntfile.js @@ -0,0 +1,361 @@ +// Generated on 2013-06-06 using generator-webapp 0.2.2 +'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)); +}; + +// # 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' + }; + + 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', + '{.tmp,<%= 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 [ + mountFolder(connect, '.tmp'), + mountFolder(connect, yeomanConfig.app), + lrSnippet + ]; + } + } + }, + test: { + options: { + middleware: function(connect) { + return [ + mountFolder(connect, '.tmp'), + mountFolder(connect, 'test') + ]; + } + } + }, + dist: { + options: { + middleware: function(connect) { + return [ + mountFolder(connect, yeomanConfig.dist) + ]; + } + } + } + }, + open: { + server: { + path: 'http://localhost:<%= connect.options.port %>' + } + }, + clean: { + dist: { + files: [ + { + dot: true, + src: [ + '.tmp', + '<%= yeoman.dist %>/*', + '!<%= yeoman.dist %>/.git*' + ] + } + ] + }, + server: '.tmp' + }, + jshint: { + options: { + jshintrc: '.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 + '/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 + //uglify2: {} // https://github.com/mishoo/UglifyJS2 + } + } + }, + rev: { + dist: { + files: { + src: [ + '<%= yeoman.dist %>/scripts/{,*/}*.js', + '<%= yeoman.dist %>/styles/{,*/}*.css', + '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp}', + '<%= yeoman.dist %>/fonts/*' + ] + } + } + }, + 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' + } + ] + } + }, + cssmin: { + dist: { + expand: true, + cwd: '<%= yeoman.app %>/styles', + src: ['{,*/}*.css', '!*.min.css'], + dest: '<%= yeoman.dist %>/styles', + ext: '.min.css' + } + }, + 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 %>' + } + ] + } + }, + // Put files not handled in other tasks here + copy: { + // Copy files from third party libraries + // TODO: copy also to dist folder + libs: { + files: [ + { + expand: true, + flatten: true, + cwd: '<% yeoman.app %>', + dest: '<% yeoman.app %>/images,', + src: [ + ] + }, + { + expand: true, + flatten: true, + cwd: '<% yeoman.app %>', + dest: '.tmp/fonts', + src: [ + '**/font-awesome/font/*' + ] + } + ] + }, + + dist: { + files: [ + { + expand: true, + dot: true, + cwd: '<%= yeoman.app %>', + dest: '<%= yeoman.dist %>', + src: [ + '*.{ico,txt}', + '.htaccess', + 'images/{,*/}*.{webp,gif}', + 'fonts/*' + ] + }, + { + expand: true, + cwd: '.tmp/images', + dest: '<%= yeoman.dist %>/images', + src: [ + 'generated/*' + ] + } + ] + } + }, + concurrent: { + server: [ + 'copy:libs', + 'less:dist' + ], + test: [ + 'less' + ], + dist: [ + 'less', + 'imagemin', + 'svgmin', + 'htmlmin' + ] + }, + bower: { + options: { + exclude: ['modernizr'] + }, + all: { + rjsConfig: '<%= yeoman.app %>/scripts/config.js' + } + } + }); + + grunt.registerTask('server', function(target) { + if (target === 'dist') { + return grunt.task.run(['build', 'open', 'connect:dist:keepalive']); + } + + grunt.task.run([ + 'clean:server', + 'concurrent:server', + 'connect:livereload', + 'open', + 'watch' + ]); + }); + + grunt.registerTask('test', [ + 'clean:server', + 'concurrent:test', + 'connect:test', + 'mocha' + ]); + + grunt.registerTask('build', [ + 'clean:dist', + 'copy:libs', + 'useminPrepare', + 'concurrent:dist', + 'requirejs', + 'cssmin', + 'concat', + 'uglify', + 'copy', + '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..95a09bd80 --- /dev/null +++ b/pyload/web/ServerThread.py @@ -0,0 +1,143 @@ +#!/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 + + 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/favicon.ico b/pyload/web/app/favicon.ico Binary files differnew file mode 100644 index 000000000..d7f9f1857 --- /dev/null +++ b/pyload/web/app/favicon.ico diff --git a/pyload/web/app/fonts/Sansation_Bold-webfont.eot b/pyload/web/app/fonts/Sansation_Bold-webfont.eot Binary files differnew file mode 100644 index 000000000..43ed2ee31 --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Bold-webfont.eot diff --git a/pyload/web/app/fonts/Sansation_Bold-webfont.ttf b/pyload/web/app/fonts/Sansation_Bold-webfont.ttf Binary files differnew file mode 100644 index 000000000..d2e7c4c2a --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Bold-webfont.ttf diff --git a/pyload/web/app/fonts/Sansation_Bold-webfont.woff b/pyload/web/app/fonts/Sansation_Bold-webfont.woff Binary files differnew file mode 100644 index 000000000..9ee938d55 --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Bold-webfont.woff diff --git a/pyload/web/app/fonts/Sansation_Light-webfont.eot b/pyload/web/app/fonts/Sansation_Light-webfont.eot Binary files differnew file mode 100644 index 000000000..d83fa9cf6 --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Light-webfont.eot diff --git a/pyload/web/app/fonts/Sansation_Light-webfont.ttf b/pyload/web/app/fonts/Sansation_Light-webfont.ttf Binary files differnew file mode 100644 index 000000000..64d734bec --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Light-webfont.ttf diff --git a/pyload/web/app/fonts/Sansation_Light-webfont.woff b/pyload/web/app/fonts/Sansation_Light-webfont.woff Binary files differnew file mode 100644 index 000000000..5f3dce493 --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Light-webfont.woff diff --git a/pyload/web/app/fonts/Sansation_Regular-webfont.eot b/pyload/web/app/fonts/Sansation_Regular-webfont.eot Binary files differnew file mode 100644 index 000000000..46219c9ff --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Regular-webfont.eot diff --git a/pyload/web/app/fonts/Sansation_Regular-webfont.ttf b/pyload/web/app/fonts/Sansation_Regular-webfont.ttf Binary files differnew file mode 100644 index 000000000..92f686359 --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Regular-webfont.ttf diff --git a/pyload/web/app/fonts/Sansation_Regular-webfont.woff b/pyload/web/app/fonts/Sansation_Regular-webfont.woff Binary files differnew file mode 100644 index 000000000..524b67992 --- /dev/null +++ b/pyload/web/app/fonts/Sansation_Regular-webfont.woff diff --git a/pyload/web/app/images/default/bgpattern.png b/pyload/web/app/images/default/bgpattern.png Binary files differnew file mode 100644 index 000000000..5111e6bdf --- /dev/null +++ b/pyload/web/app/images/default/bgpattern.png 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/default/fancy_deboss.png b/pyload/web/app/images/default/fancy_deboss.png Binary files differnew file mode 100644 index 000000000..926a762db --- /dev/null +++ b/pyload/web/app/images/default/fancy_deboss.png diff --git a/pyload/web/app/images/default/logo.png b/pyload/web/app/images/default/logo.png Binary files differnew file mode 100644 index 000000000..bb9c13679 --- /dev/null +++ b/pyload/web/app/images/default/logo.png diff --git a/pyload/web/app/images/default/logo_grey.png b/pyload/web/app/images/default/logo_grey.png Binary files differnew file mode 100644 index 000000000..a4114d832 --- /dev/null +++ b/pyload/web/app/images/default/logo_grey.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..87fd6c612 --- /dev/null +++ b/pyload/web/app/index.html @@ -0,0 +1,107 @@ +<!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 Include this font --> + <link href="http://fonts.googleapis.com/css?family=Abel" rel="stylesheet" type="text/css"/> + + <!-- 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"> + + <!-- build:css styles/vendor.css --> + <link href="components/select2/select2.css" rel="stylesheet" type="text/css"/> + <!-- endbuild --> + + <!-- build:js scripts/default.js --> + <script data-main="scripts/config" src="components/requirejs/require.js"></script> + <!-- endbuild --> + + <script type="text/javascript"> + window.dates = { + weeks: ['week', 'weeks'], + days: ['day', 'days'], + hours: ['hour', 'hours'], + minutes: ['minute', 'minutes'], + seconds: ['second', 'seconds'] + }; // TODO carefully when translating + + window.hostProtocol = 'http://'; + window.hostAddress = 'localhost'; + window.hostPort = '8001'; + window.pathPrefix = "/"; + + window.wsAddress = "ws://%s:7227"; // TODO + + 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..427cb1bc8 --- /dev/null +++ b/pyload/web/app/scripts/app.js @@ -0,0 +1,105 @@ +/* + * Global Application Object + * Contains all necessary logic shared across views + */ +define([ + + // Libraries. + 'jquery', + 'underscore', + 'backbone', + 'utils/initHB', + '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) { + // TODO + 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..398d97e11 --- /dev/null +++ b/pyload/web/app/scripts/config.js @@ -0,0 +1,73 @@ +// 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: 'vendor/bootstrap-2.3.2', + underscore: '../components/underscore/underscore', + backbone: '../components/backbone/backbone', + marionette: '../components/backbone.marionette/lib/backbone.marionette', +// handlebars: '../components/handlebars.js/dist/handlebars', + handlebars: 'vendor/Handlebars-1.0rc1', + 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) { + // 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..05237914d --- /dev/null +++ b/pyload/web/app/scripts/controller.js @@ -0,0 +1,67 @@ +define([ + 'app', + 'backbone', + + // 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 completly visible after reattaching + 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..a337cee21 --- /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/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/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..3beb7f270 --- /dev/null +++ b/pyload/web/app/scripts/models/File.js @@ -0,0 +1,92 @@ +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) { + + }, + + 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..4933b7ed2 --- /dev/null +++ b/pyload/web/app/scripts/utils/dialogs.js @@ -0,0 +1,16 @@ +// 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 = 'text!tpl/' + template; + _.requireOnce([template], function(html) { + var template = _.compile(html); + var dialog = new Modal(template, _.bind(func, context)); + dialog.show(); + }); + + }; +});
\ No newline at end of file diff --git a/pyload/web/app/scripts/utils/initHB.js b/pyload/web/app/scripts/utils/initHB.js new file mode 100644 index 000000000..d7f582521 --- /dev/null +++ b/pyload/web/app/scripts/utils/initHB.js @@ -0,0 +1,11 @@ +// Loads all helper and set own handlebars rules +define(['underscore', 'handlebars', + 'helpers/formatSize', 'helpers/fileHelper', 'helpers/formatTime'], + function(_, Handlebars) { + 'use strict'; + // Replace with own lexer rules compiled from handlebars.l + Handlebars.Parser.lexer.rules = [/^(?:[^\x00]*?(?=(<%)))/, /^(?:[^\x00]+)/, /^(?:[^\x00]{2,}?(?=(\{\{|$)))/, /^(?:\{\{>)/, /^(?:<%=)/, /^(?:<%\/)/, /^(?:\{\{\^)/, /^(?:<%\s*else\b)/, /^(?:\{<%%)/, /^(?:\{\{&)/, /^(?:<%![\s\S]*?%>)/, /^(?:<%)/, /^(?:=)/, /^(?:\.(?=[%} ]))/, /^(?:\.\.)/, /^(?:[\/.])/, /^(?:\s+)/, /^(?:%%>)/, /^(?:%>)/, /^(?:"(\\["]|[^"])*")/, /^(?:'(\\[']|[^'])*')/, /^(?:@[a-zA-Z]+)/, /^(?:true(?=[%}\s]))/, /^(?:false(?=[%}\s]))/, /^(?:[0-9]+(?=[%}\s]))/, /^(?:[a-zA-Z0-9_$-]+(?=[=%}\s\/.]))/, /^(?:\[[^\]]*\])/, /^(?:.)/, /^(?:$)/]; + _.compile = Handlebars.compile; + + return Handlebars; + });
\ 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/Handlebars-1.0rc1.js b/pyload/web/app/scripts/vendor/Handlebars-1.0rc1.js new file mode 100644 index 000000000..991242461 --- /dev/null +++ b/pyload/web/app/scripts/vendor/Handlebars-1.0rc1.js @@ -0,0 +1,1927 @@ +// lib/handlebars/base.js +(function () { +/*jshint eqnull:true*/ +this.Handlebars = {}; + +(function(Handlebars) { + +Handlebars.VERSION = "1.0.rc.1"; + +Handlebars.helpers = {}; +Handlebars.partials = {}; + +Handlebars.registerHelper = function(name, fn, inverse) { + if(inverse) { fn.not = inverse; } + this.helpers[name] = fn; +}; + +Handlebars.registerPartial = function(name, str) { + this.partials[name] = str; +}; + +Handlebars.registerHelper('helperMissing', function(arg) { + if(arguments.length === 2) { + return undefined; + } else { + throw new Error("Could not find property '" + arg + "'"); + } +}); + +var toString = Object.prototype.toString, functionType = "[object Function]"; + +Handlebars.registerHelper('blockHelperMissing', function(context, options) { + var inverse = options.inverse || function() {}, fn = options.fn; + + + var ret = ""; + var type = toString.call(context); + + if(type === functionType) { context = context.call(this); } + + if(context === true) { + return fn(this); + } else if(context === false || context == null) { + return inverse(this); + } else if(type === "[object Array]") { + if(context.length > 0) { + return Handlebars.helpers.each(context, options); + } else { + return inverse(this); + } + } else { + return fn(context); + } +}); + +Handlebars.K = function() {}; + +Handlebars.createFrame = Object.create || function(object) { + Handlebars.K.prototype = object; + var obj = new Handlebars.K(); + Handlebars.K.prototype = null; + return obj; +}; + +Handlebars.registerHelper('each', function(context, options) { + var fn = options.fn, inverse = options.inverse; + var ret = "", data; + + if (options.data) { + data = Handlebars.createFrame(options.data); + } + + if(context && context.length > 0) { + for(var i=0, j=context.length; i<j; i++) { + if (data) { data.index = i; } + ret = ret + fn(context[i], { data: data }); + } + } else { + ret = inverse(this); + } + return ret; +}); + +Handlebars.registerHelper('if', function(context, options) { + var type = toString.call(context); + if(type === functionType) { context = context.call(this); } + + if(!context || Handlebars.Utils.isEmpty(context)) { + return options.inverse(this); + } else { + return options.fn(this); + } +}); + +Handlebars.registerHelper('unless', function(context, options) { + var fn = options.fn, inverse = options.inverse; + options.fn = inverse; + options.inverse = fn; + + return Handlebars.helpers['if'].call(this, context, options); +}); + +Handlebars.registerHelper('with', function(context, options) { + return options.fn(context); +}); + +Handlebars.registerHelper('log', function(context) { + Handlebars.log(context); +}); + +}(this.Handlebars)); +; +// lib/handlebars/compiler/parser.js +/* Jison generated parser */ +var handlebars = (function(){ +var parser = {trace: function trace() { }, +yy: {}, +symbols_: {"error":2,"root":3,"program":4,"EOF":5,"statements":6,"simpleInverse":7,"statement":8,"openInverse":9,"closeBlock":10,"openBlock":11,"mustache":12,"partial":13,"CONTENT":14,"COMMENT":15,"OPEN_BLOCK":16,"inMustache":17,"CLOSE":18,"OPEN_INVERSE":19,"OPEN_ENDBLOCK":20,"path":21,"OPEN":22,"OPEN_UNESCAPED":23,"OPEN_PARTIAL":24,"params":25,"hash":26,"DATA":27,"param":28,"STRING":29,"INTEGER":30,"BOOLEAN":31,"hashSegments":32,"hashSegment":33,"ID":34,"EQUALS":35,"pathSegments":36,"SEP":37,"$accept":0,"$end":1}, +terminals_: {2:"error",5:"EOF",14:"CONTENT",15:"COMMENT",16:"OPEN_BLOCK",18:"CLOSE",19:"OPEN_INVERSE",20:"OPEN_ENDBLOCK",22:"OPEN",23:"OPEN_UNESCAPED",24:"OPEN_PARTIAL",27:"DATA",29:"STRING",30:"INTEGER",31:"BOOLEAN",34:"ID",35:"EQUALS",37:"SEP"}, +productions_: [0,[3,2],[4,3],[4,1],[4,0],[6,1],[6,2],[8,3],[8,3],[8,1],[8,1],[8,1],[8,1],[11,3],[9,3],[10,3],[12,3],[12,3],[13,3],[13,4],[7,2],[17,3],[17,2],[17,2],[17,1],[17,1],[25,2],[25,1],[28,1],[28,1],[28,1],[28,1],[28,1],[26,1],[32,2],[32,1],[33,3],[33,3],[33,3],[33,3],[33,3],[21,1],[36,3],[36,1]], +performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) { + +var $0 = $$.length - 1; +switch (yystate) { +case 1: return $$[$0-1]; +break; +case 2: this.$ = new yy.ProgramNode($$[$0-2], $$[$0]); +break; +case 3: this.$ = new yy.ProgramNode($$[$0]); +break; +case 4: this.$ = new yy.ProgramNode([]); +break; +case 5: this.$ = [$$[$0]]; +break; +case 6: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; +break; +case 7: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1].inverse, $$[$0-1], $$[$0]); +break; +case 8: this.$ = new yy.BlockNode($$[$0-2], $$[$0-1], $$[$0-1].inverse, $$[$0]); +break; +case 9: this.$ = $$[$0]; +break; +case 10: this.$ = $$[$0]; +break; +case 11: this.$ = new yy.ContentNode($$[$0]); +break; +case 12: this.$ = new yy.CommentNode($$[$0]); +break; +case 13: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); +break; +case 14: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); +break; +case 15: this.$ = $$[$0-1]; +break; +case 16: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1]); +break; +case 17: this.$ = new yy.MustacheNode($$[$0-1][0], $$[$0-1][1], true); +break; +case 18: this.$ = new yy.PartialNode($$[$0-1]); +break; +case 19: this.$ = new yy.PartialNode($$[$0-2], $$[$0-1]); +break; +case 20: +break; +case 21: this.$ = [[$$[$0-2]].concat($$[$0-1]), $$[$0]]; +break; +case 22: this.$ = [[$$[$0-1]].concat($$[$0]), null]; +break; +case 23: this.$ = [[$$[$0-1]], $$[$0]]; +break; +case 24: this.$ = [[$$[$0]], null]; +break; +case 25: this.$ = [[new yy.DataNode($$[$0])], null]; +break; +case 26: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; +break; +case 27: this.$ = [$$[$0]]; +break; +case 28: this.$ = $$[$0]; +break; +case 29: this.$ = new yy.StringNode($$[$0]); +break; +case 30: this.$ = new yy.IntegerNode($$[$0]); +break; +case 31: this.$ = new yy.BooleanNode($$[$0]); +break; +case 32: this.$ = new yy.DataNode($$[$0]); +break; +case 33: this.$ = new yy.HashNode($$[$0]); +break; +case 34: $$[$0-1].push($$[$0]); this.$ = $$[$0-1]; +break; +case 35: this.$ = [$$[$0]]; +break; +case 36: this.$ = [$$[$0-2], $$[$0]]; +break; +case 37: this.$ = [$$[$0-2], new yy.StringNode($$[$0])]; +break; +case 38: this.$ = [$$[$0-2], new yy.IntegerNode($$[$0])]; +break; +case 39: this.$ = [$$[$0-2], new yy.BooleanNode($$[$0])]; +break; +case 40: this.$ = [$$[$0-2], new yy.DataNode($$[$0])]; +break; +case 41: this.$ = new yy.IdNode($$[$0]); +break; +case 42: $$[$0-2].push($$[$0]); this.$ = $$[$0-2]; +break; +case 43: this.$ = [$$[$0]]; +break; +} +}, +table: [{3:1,4:2,5:[2,4],6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{1:[3]},{5:[1,16]},{5:[2,3],7:17,8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,19],20:[2,3],22:[1,13],23:[1,14],24:[1,15]},{5:[2,5],14:[2,5],15:[2,5],16:[2,5],19:[2,5],20:[2,5],22:[2,5],23:[2,5],24:[2,5]},{4:20,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{4:21,6:3,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,4],22:[1,13],23:[1,14],24:[1,15]},{5:[2,9],14:[2,9],15:[2,9],16:[2,9],19:[2,9],20:[2,9],22:[2,9],23:[2,9],24:[2,9]},{5:[2,10],14:[2,10],15:[2,10],16:[2,10],19:[2,10],20:[2,10],22:[2,10],23:[2,10],24:[2,10]},{5:[2,11],14:[2,11],15:[2,11],16:[2,11],19:[2,11],20:[2,11],22:[2,11],23:[2,11],24:[2,11]},{5:[2,12],14:[2,12],15:[2,12],16:[2,12],19:[2,12],20:[2,12],22:[2,12],23:[2,12],24:[2,12]},{17:22,21:23,27:[1,24],34:[1,26],36:25},{17:27,21:23,27:[1,24],34:[1,26],36:25},{17:28,21:23,27:[1,24],34:[1,26],36:25},{17:29,21:23,27:[1,24],34:[1,26],36:25},{21:30,34:[1,26],36:25},{1:[2,1]},{6:31,8:4,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],22:[1,13],23:[1,14],24:[1,15]},{5:[2,6],14:[2,6],15:[2,6],16:[2,6],19:[2,6],20:[2,6],22:[2,6],23:[2,6],24:[2,6]},{17:22,18:[1,32],21:23,27:[1,24],34:[1,26],36:25},{10:33,20:[1,34]},{10:35,20:[1,34]},{18:[1,36]},{18:[2,24],21:41,25:37,26:38,27:[1,45],28:39,29:[1,42],30:[1,43],31:[1,44],32:40,33:46,34:[1,47],36:25},{18:[2,25]},{18:[2,41],27:[2,41],29:[2,41],30:[2,41],31:[2,41],34:[2,41],37:[1,48]},{18:[2,43],27:[2,43],29:[2,43],30:[2,43],31:[2,43],34:[2,43],37:[2,43]},{18:[1,49]},{18:[1,50]},{18:[1,51]},{18:[1,52],21:53,34:[1,26],36:25},{5:[2,2],8:18,9:5,11:6,12:7,13:8,14:[1,9],15:[1,10],16:[1,12],19:[1,11],20:[2,2],22:[1,13],23:[1,14],24:[1,15]},{14:[2,20],15:[2,20],16:[2,20],19:[2,20],22:[2,20],23:[2,20],24:[2,20]},{5:[2,7],14:[2,7],15:[2,7],16:[2,7],19:[2,7],20:[2,7],22:[2,7],23:[2,7],24:[2,7]},{21:54,34:[1,26],36:25},{5:[2,8],14:[2,8],15:[2,8],16:[2,8],19:[2,8],20:[2,8],22:[2,8],23:[2,8],24:[2,8]},{14:[2,14],15:[2,14],16:[2,14],19:[2,14],20:[2,14],22:[2,14],23:[2,14],24:[2,14]},{18:[2,22],21:41,26:55,27:[1,45],28:56,29:[1,42],30:[1,43],31:[1,44],32:40,33:46,34:[1,47],36:25},{18:[2,23]},{18:[2,27],27:[2,27],29:[2,27],30:[2,27],31:[2,27],34:[2,27]},{18:[2,33],33:57,34:[1,58]},{18:[2,28],27:[2,28],29:[2,28],30:[2,28],31:[2,28],34:[2,28]},{18:[2,29],27:[2,29],29:[2,29],30:[2,29],31:[2,29],34:[2,29]},{18:[2,30],27:[2,30],29:[2,30],30:[2,30],31:[2,30],34:[2,30]},{18:[2,31],27:[2,31],29:[2,31],30:[2,31],31:[2,31],34:[2,31]},{18:[2,32],27:[2,32],29:[2,32],30:[2,32],31:[2,32],34:[2,32]},{18:[2,35],34:[2,35]},{18:[2,43],27:[2,43],29:[2,43],30:[2,43],31:[2,43],34:[2,43],35:[1,59],37:[2,43]},{34:[1,60]},{14:[2,13],15:[2,13],16:[2,13],19:[2,13],20:[2,13],22:[2,13],23:[2,13],24:[2,13]},{5:[2,16],14:[2,16],15:[2,16],16:[2,16],19:[2,16],20:[2,16],22:[2,16],23:[2,16],24:[2,16]},{5:[2,17],14:[2,17],15:[2,17],16:[2,17],19:[2,17],20:[2,17],22:[2,17],23:[2,17],24:[2,17]},{5:[2,18],14:[2,18],15:[2,18],16:[2,18],19:[2,18],20:[2,18],22:[2,18],23:[2,18],24:[2,18]},{18:[1,61]},{18:[1,62]},{18:[2,21]},{18:[2,26],27:[2,26],29:[2,26],30:[2,26],31:[2,26],34:[2,26]},{18:[2,34],34:[2,34]},{35:[1,59]},{21:63,27:[1,67],29:[1,64],30:[1,65],31:[1,66],34:[1,26],36:25},{18:[2,42],27:[2,42],29:[2,42],30:[2,42],31:[2,42],34:[2,42],37:[2,42]},{5:[2,19],14:[2,19],15:[2,19],16:[2,19],19:[2,19],20:[2,19],22:[2,19],23:[2,19],24:[2,19]},{5:[2,15],14:[2,15],15:[2,15],16:[2,15],19:[2,15],20:[2,15],22:[2,15],23:[2,15],24:[2,15]},{18:[2,36],34:[2,36]},{18:[2,37],34:[2,37]},{18:[2,38],34:[2,38]},{18:[2,39],34:[2,39]},{18:[2,40],34:[2,40]}], +defaultActions: {16:[2,1],24:[2,25],38:[2,23],55:[2,21]}, +parseError: function parseError(str, hash) { + throw new Error(str); +}, +parse: function parse(input) { + var self = this, stack = [0], vstack = [null], lstack = [], table = this.table, yytext = "", yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; + this.lexer.setInput(input); + this.lexer.yy = this.yy; + this.yy.lexer = this.lexer; + this.yy.parser = this; + if (typeof this.lexer.yylloc == "undefined") + this.lexer.yylloc = {}; + var yyloc = this.lexer.yylloc; + lstack.push(yyloc); + var ranges = this.lexer.options && this.lexer.options.ranges; + if (typeof this.yy.parseError === "function") + this.parseError = this.yy.parseError; + function popStack(n) { + stack.length = stack.length - 2 * n; + vstack.length = vstack.length - n; + lstack.length = lstack.length - n; + } + function lex() { + var token; + token = self.lexer.lex() || 1; + if (typeof token !== "number") { + token = self.symbols_[token] || token; + } + return token; + } + var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; + while (true) { + state = stack[stack.length - 1]; + if (this.defaultActions[state]) { + action = this.defaultActions[state]; + } else { + if (symbol === null || typeof symbol == "undefined") { + symbol = lex(); + } + action = table[state] && table[state][symbol]; + } + if (typeof action === "undefined" || !action.length || !action[0]) { + var errStr = ""; + if (!recovering) { + expected = []; + for (p in table[state]) + if (this.terminals_[p] && p > 2) { + expected.push("'" + this.terminals_[p] + "'"); + } + if (this.lexer.showPosition) { + errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; + } else { + errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); + } + this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); + } + } + if (action[0] instanceof Array && action.length > 1) { + throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); + } + switch (action[0]) { + case 1: + stack.push(symbol); + vstack.push(this.lexer.yytext); + lstack.push(this.lexer.yylloc); + stack.push(action[1]); + symbol = null; + if (!preErrorSymbol) { + yyleng = this.lexer.yyleng; + yytext = this.lexer.yytext; + yylineno = this.lexer.yylineno; + yyloc = this.lexer.yylloc; + if (recovering > 0) + recovering--; + } else { + symbol = preErrorSymbol; + preErrorSymbol = null; + } + break; + case 2: + len = this.productions_[action[1]][1]; + yyval.$ = vstack[vstack.length - len]; + yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; + if (ranges) { + yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; + } + r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); + if (typeof r !== "undefined") { + return r; + } + if (len) { + stack = stack.slice(0, -1 * len * 2); + vstack = vstack.slice(0, -1 * len); + lstack = lstack.slice(0, -1 * len); + } + stack.push(this.productions_[action[1]][0]); + vstack.push(yyval.$); + lstack.push(yyval._$); + newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; + stack.push(newState); + break; + case 3: + return true; + } + } + return true; +} +}; +/* Jison generated lexer */ +var lexer = (function(){ +var lexer = ({EOF:1, +parseError:function parseError(str, hash) { + if (this.yy.parser) { + this.yy.parser.parseError(str, hash); + } else { + throw new Error(str); + } + }, +setInput:function (input) { + this._input = input; + this._more = this._less = this.done = false; + this.yylineno = this.yyleng = 0; + this.yytext = this.matched = this.match = ''; + this.conditionStack = ['INITIAL']; + this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; + if (this.options.ranges) this.yylloc.range = [0,0]; + this.offset = 0; + return this; + }, +input:function () { + var ch = this._input[0]; + this.yytext += ch; + this.yyleng++; + this.offset++; + this.match += ch; + this.matched += ch; + var lines = ch.match(/(?:\r\n?|\n).*/g); + if (lines) { + this.yylineno++; + this.yylloc.last_line++; + } else { + this.yylloc.last_column++; + } + if (this.options.ranges) this.yylloc.range[1]++; + + this._input = this._input.slice(1); + return ch; + }, +unput:function (ch) { + var len = ch.length; + var lines = ch.split(/(?:\r\n?|\n)/g); + + this._input = ch + this._input; + this.yytext = this.yytext.substr(0, this.yytext.length-len-1); + //this.yyleng -= len; + this.offset -= len; + var oldLines = this.match.split(/(?:\r\n?|\n)/g); + this.match = this.match.substr(0, this.match.length-1); + this.matched = this.matched.substr(0, this.matched.length-1); + + if (lines.length-1) this.yylineno -= lines.length-1; + var r = this.yylloc.range; + + this.yylloc = {first_line: this.yylloc.first_line, + last_line: this.yylineno+1, + first_column: this.yylloc.first_column, + last_column: lines ? + (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length: + this.yylloc.first_column - len + }; + + if (this.options.ranges) { + this.yylloc.range = [r[0], r[0] + this.yyleng - len]; + } + return this; + }, +more:function () { + this._more = true; + return this; + }, +less:function (n) { + this.unput(this.match.slice(n)); + }, +pastInput:function () { + var past = this.matched.substr(0, this.matched.length - this.match.length); + return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); + }, +upcomingInput:function () { + var next = this.match; + if (next.length < 20) { + next += this._input.substr(0, 20-next.length); + } + return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); + }, +showPosition:function () { + var pre = this.pastInput(); + var c = new Array(pre.length + 1).join("-"); + return pre + this.upcomingInput() + "\n" + c+"^"; + }, +next:function () { + if (this.done) { + return this.EOF; + } + if (!this._input) this.done = true; + + var token, + match, + tempMatch, + index, + col, + lines; + if (!this._more) { + this.yytext = ''; + this.match = ''; + } + var rules = this._currentRules(); + for (var i=0;i < rules.length; i++) { + tempMatch = this._input.match(this.rules[rules[i]]); + if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { + match = tempMatch; + index = i; + if (!this.options.flex) break; + } + } + if (match) { + lines = match[0].match(/(?:\r\n?|\n).*/g); + if (lines) this.yylineno += lines.length; + this.yylloc = {first_line: this.yylloc.last_line, + last_line: this.yylineno+1, + first_column: this.yylloc.last_column, + last_column: lines ? lines[lines.length-1].length-lines[lines.length-1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length}; + this.yytext += match[0]; + this.match += match[0]; + this.matches = match; + this.yyleng = this.yytext.length; + if (this.options.ranges) { + this.yylloc.range = [this.offset, this.offset += this.yyleng]; + } + this._more = false; + this._input = this._input.slice(match[0].length); + this.matched += match[0]; + token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]); + if (this.done && this._input) this.done = false; + if (token) return token; + else return; + } + if (this._input === "") { + return this.EOF; + } else { + return this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), + {text: "", token: null, line: this.yylineno}); + } + }, +lex:function lex() { + var r = this.next(); + if (typeof r !== 'undefined') { + return r; + } else { + return this.lex(); + } + }, +begin:function begin(condition) { + this.conditionStack.push(condition); + }, +popState:function popState() { + return this.conditionStack.pop(); + }, +_currentRules:function _currentRules() { + return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; + }, +topState:function () { + return this.conditionStack[this.conditionStack.length-2]; + }, +pushState:function begin(condition) { + this.begin(condition); + }}); +lexer.options = {}; +lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { + +var YYSTATE=YY_START +switch($avoiding_name_collisions) { +case 0: + if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); + if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); + if(yy_.yytext) return 14; + +break; +case 1: return 14; +break; +case 2: + if(yy_.yytext.slice(-1) !== "\\") this.popState(); + if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1); + return 14; + +break; +case 3: return 24; +break; +case 4: return 16; +break; +case 5: return 20; +break; +case 6: return 19; +break; +case 7: return 19; +break; +case 8: return 23; +break; +case 9: return 23; +break; +case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; +break; +case 11: return 22; +break; +case 12: return 35; +break; +case 13: return 34; +break; +case 14: return 34; +break; +case 15: return 37; +break; +case 16: /*ignore whitespace*/ +break; +case 17: this.popState(); return 18; +break; +case 18: this.popState(); return 18; +break; +case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29; +break; +case 20: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 29; +break; +case 21: yy_.yytext = yy_.yytext.substr(1); return 27; +break; +case 22: return 31; +break; +case 23: return 31; +break; +case 24: return 30; +break; +case 25: return 34; +break; +case 26: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 34; +break; +case 27: return 'INVALID'; +break; +case 28: return 5; +break; +} +}; +lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/,/^(?:[^\x00]+)/,/^(?:[^\x00]{2,}?(?=(\{\{|$)))/,/^(?:\{\{>)/,/^(?:\{\{#)/,/^(?:\{\{\/)/,/^(?:\{\{\^)/,/^(?:\{\{\s*else\b)/,/^(?:\{\{\{)/,/^(?:\{\{&)/,/^(?:\{\{![\s\S]*?\}\})/,/^(?:\{\{)/,/^(?:=)/,/^(?:\.(?=[} ]))/,/^(?:\.\.)/,/^(?:[\/.])/,/^(?:\s+)/,/^(?:\}\}\})/,/^(?:\}\})/,/^(?:"(\\["]|[^"])*")/,/^(?:'(\\[']|[^'])*')/,/^(?:@[a-zA-Z]+)/,/^(?:true(?=[}\s]))/,/^(?:false(?=[}\s]))/,/^(?:[0-9]+(?=[}\s]))/,/^(?:[a-zA-Z0-9_$-]+(?=[=}\s\/.]))/,/^(?:\[[^\]]*\])/,/^(?:.)/,/^(?:$)/]; +lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,28],"inclusive":true}}; +return lexer;})() +parser.lexer = lexer; +function Parser () { this.yy = {}; }Parser.prototype = parser;parser.Parser = Parser; +return new Parser; +})(); +if (typeof require !== 'undefined' && typeof exports !== 'undefined') { +exports.parser = handlebars; +exports.Parser = handlebars.Parser; +exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); } +exports.main = function commonjsMain(args) { + if (!args[1]) + throw new Error('Usage: '+args[0]+' FILE'); + var source, cwd; + if (typeof process !== 'undefined') { + source = require('fs').readFileSync(require('path').resolve(args[1]), "utf8"); + } else { + source = require("file").path(require("file").cwd()).join(args[1]).read({charset: "utf-8"}); + } + return exports.parser.parse(source); +} +if (typeof module !== 'undefined' && require.main === module) { + exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args); +} +}; +; +// lib/handlebars/compiler/base.js +Handlebars.Parser = handlebars; + +Handlebars.parse = function(string) { + Handlebars.Parser.yy = Handlebars.AST; + return Handlebars.Parser.parse(string); +}; + +Handlebars.print = function(ast) { + return new Handlebars.PrintVisitor().accept(ast); +}; + +Handlebars.logger = { + DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, + + // override in the host environment + log: function(level, str) {} +}; + +Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); }; +; +// lib/handlebars/compiler/ast.js +(function() { + + Handlebars.AST = {}; + + Handlebars.AST.ProgramNode = function(statements, inverse) { + this.type = "program"; + this.statements = statements; + if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } + }; + + Handlebars.AST.MustacheNode = function(rawParams, hash, unescaped) { + this.type = "mustache"; + this.escaped = !unescaped; + this.hash = hash; + + var id = this.id = rawParams[0]; + var params = this.params = rawParams.slice(1); + + // a mustache is an eligible helper if: + // * its id is simple (a single part, not `this` or `..`) + var eligibleHelper = this.eligibleHelper = id.isSimple; + + // a mustache is definitely a helper if: + // * it is an eligible helper, and + // * it has at least one parameter or hash segment + this.isHelper = eligibleHelper && (params.length || hash); + + // if a mustache is an eligible helper but not a definite + // helper, it is ambiguous, and will be resolved in a later + // pass or at runtime. + }; + + Handlebars.AST.PartialNode = function(id, context) { + this.type = "partial"; + + // TODO: disallow complex IDs + + this.id = id; + this.context = context; + }; + + var verifyMatch = function(open, close) { + if(open.original !== close.original) { + throw new Handlebars.Exception(open.original + " doesn't match " + close.original); + } + }; + + Handlebars.AST.BlockNode = function(mustache, program, inverse, close) { + verifyMatch(mustache.id, close); + this.type = "block"; + this.mustache = mustache; + this.program = program; + this.inverse = inverse; + + if (this.inverse && !this.program) { + this.isInverse = true; + } + }; + + Handlebars.AST.ContentNode = function(string) { + this.type = "content"; + this.string = string; + }; + + Handlebars.AST.HashNode = function(pairs) { + this.type = "hash"; + this.pairs = pairs; + }; + + Handlebars.AST.IdNode = function(parts) { + this.type = "ID"; + this.original = parts.join("."); + + var dig = [], depth = 0; + + for(var i=0,l=parts.length; i<l; i++) { + var part = parts[i]; + + if(part === "..") { depth++; } + else if(part === "." || part === "this") { this.isScoped = true; } + else { dig.push(part); } + } + + this.parts = dig; + this.string = dig.join('.'); + this.depth = depth; + + // an ID is simple if it only has one part, and that part is not + // `..` or `this`. + this.isSimple = parts.length === 1 && !this.isScoped && depth === 0; + }; + + Handlebars.AST.DataNode = function(id) { + this.type = "DATA"; + this.id = id; + }; + + Handlebars.AST.StringNode = function(string) { + this.type = "STRING"; + this.string = string; + }; + + Handlebars.AST.IntegerNode = function(integer) { + this.type = "INTEGER"; + this.integer = integer; + }; + + Handlebars.AST.BooleanNode = function(bool) { + this.type = "BOOLEAN"; + this.bool = bool; + }; + + Handlebars.AST.CommentNode = function(comment) { + this.type = "comment"; + this.comment = comment; + }; + +})();; +// lib/handlebars/utils.js +Handlebars.Exception = function(message) { + var tmp = Error.prototype.constructor.apply(this, arguments); + + for (var p in tmp) { + if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; } + } + + this.message = tmp.message; +}; +Handlebars.Exception.prototype = new Error(); + +// Build out our basic SafeString type +Handlebars.SafeString = function(string) { + this.string = string; +}; +Handlebars.SafeString.prototype.toString = function() { + return this.string.toString(); +}; + +(function() { + var escape = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "`": "`" + }; + + var badChars = /[&<>"'`]/g; + var possible = /[&<>"'`]/; + + var escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + Handlebars.Utils = { + escapeExpression: function(string) { + // don't escape SafeStrings, since they're already safe + if (string instanceof Handlebars.SafeString) { + return string.toString(); + } else if (string == null || string === false) { + return ""; + } + + if(!possible.test(string)) { return string; } + return string.replace(badChars, escapeChar); + }, + + isEmpty: function(value) { + if (typeof value === "undefined") { + return true; + } else if (value === null) { + return true; + } else if (value === false) { + return true; + } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { + return true; + } else { + return false; + } + } + }; +})();; +// lib/handlebars/compiler/compiler.js + +/*jshint eqnull:true*/ +Handlebars.Compiler = function() {}; +Handlebars.JavaScriptCompiler = function() {}; + +(function(Compiler, JavaScriptCompiler) { + // the foundHelper register will disambiguate helper lookup from finding a + // function in a context. This is necessary for mustache compatibility, which + // requires that context functions in blocks are evaluated by blockHelperMissing, + // and then proceed as if the resulting value was provided to blockHelperMissing. + + Compiler.prototype = { + compiler: Compiler, + + disassemble: function() { + var opcodes = this.opcodes, opcode, out = [], params, param; + + for (var i=0, l=opcodes.length; i<l; i++) { + opcode = opcodes[i]; + + if (opcode.opcode === 'DECLARE') { + out.push("DECLARE " + opcode.name + "=" + opcode.value); + } else { + params = []; + for (var j=0; j<opcode.args.length; j++) { + param = opcode.args[j]; + if (typeof param === "string") { + param = "\"" + param.replace("\n", "\\n") + "\""; + } + params.push(param); + } + out.push(opcode.opcode + " " + params.join(" ")); + } + } + + return out.join("\n"); + }, + + guid: 0, + + compile: function(program, options) { + this.children = []; + this.depths = {list: []}; + this.options = options; + + // These changes will propagate to the other compiler components + var knownHelpers = this.options.knownHelpers; + this.options.knownHelpers = { + 'helperMissing': true, + 'blockHelperMissing': true, + 'each': true, + 'if': true, + 'unless': true, + 'with': true, + 'log': true + }; + if (knownHelpers) { + for (var name in knownHelpers) { + this.options.knownHelpers[name] = knownHelpers[name]; + } + } + + return this.program(program); + }, + + accept: function(node) { + return this[node.type](node); + }, + + program: function(program) { + var statements = program.statements, statement; + this.opcodes = []; + + for(var i=0, l=statements.length; i<l; i++) { + statement = statements[i]; + this[statement.type](statement); + } + this.isSimple = l === 1; + + this.depths.list = this.depths.list.sort(function(a, b) { + return a - b; + }); + + return this; + }, + + compileProgram: function(program) { + var result = new this.compiler().compile(program, this.options); + var guid = this.guid++, depth; + + this.usePartial = this.usePartial || result.usePartial; + + this.children[guid] = result; + + for(var i=0, l=result.depths.list.length; i<l; i++) { + depth = result.depths.list[i]; + + if(depth < 2) { continue; } + else { this.addDepth(depth - 1); } + } + + return guid; + }, + + block: function(block) { + var mustache = block.mustache, + program = block.program, + inverse = block.inverse; + + if (program) { + program = this.compileProgram(program); + } + + if (inverse) { + inverse = this.compileProgram(inverse); + } + + var type = this.classifyMustache(mustache); + + if (type === "helper") { + this.helperMustache(mustache, program, inverse); + } else if (type === "simple") { + this.simpleMustache(mustache); + + // now that the simple mustache is resolved, we need to + // evaluate it by executing `blockHelperMissing` + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); + this.opcode('pushLiteral', '{}'); + this.opcode('blockValue'); + } else { + this.ambiguousMustache(mustache, program, inverse); + + // now that the simple mustache is resolved, we need to + // evaluate it by executing `blockHelperMissing` + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); + this.opcode('pushLiteral', '{}'); + this.opcode('ambiguousBlockValue'); + } + + this.opcode('append'); + }, + + hash: function(hash) { + var pairs = hash.pairs, pair, val; + + this.opcode('push', '{}'); + + for(var i=0, l=pairs.length; i<l; i++) { + pair = pairs[i]; + val = pair[1]; + + this.accept(val); + this.opcode('assignToHash', pair[0]); + } + }, + + partial: function(partial) { + var id = partial.id; + this.usePartial = true; + + if(partial.context) { + this.ID(partial.context); + } else { + this.opcode('push', 'depth0'); + } + + this.opcode('invokePartial', id.original); + this.opcode('append'); + }, + + content: function(content) { + this.opcode('appendContent', content.string); + }, + + mustache: function(mustache) { + var options = this.options; + var type = this.classifyMustache(mustache); + + if (type === "simple") { + this.simpleMustache(mustache); + } else if (type === "helper") { + this.helperMustache(mustache); + } else { + this.ambiguousMustache(mustache); + } + + if(mustache.escaped && !options.noEscape) { + this.opcode('appendEscaped'); + } else { + this.opcode('append'); + } + }, + + ambiguousMustache: function(mustache, program, inverse) { + var id = mustache.id, name = id.parts[0]; + + this.opcode('getContext', id.depth); + + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); + + this.opcode('invokeAmbiguous', name); + }, + + simpleMustache: function(mustache, program, inverse) { + var id = mustache.id; + + if (id.type === 'DATA') { + this.DATA(id); + } else if (id.parts.length) { + this.ID(id); + } else { + // Simplified ID for `this` + this.addDepth(id.depth); + this.opcode('getContext', id.depth); + this.opcode('pushContext'); + } + + this.opcode('resolvePossibleLambda'); + }, + + helperMustache: function(mustache, program, inverse) { + var params = this.setupFullMustacheParams(mustache, program, inverse), + name = mustache.id.parts[0]; + + if (this.options.knownHelpers[name]) { + this.opcode('invokeKnownHelper', params.length, name); + } else if (this.knownHelpersOnly) { + throw new Error("You specified knownHelpersOnly, but used the unknown helper " + name); + } else { + this.opcode('invokeHelper', params.length, name); + } + }, + + ID: function(id) { + this.addDepth(id.depth); + this.opcode('getContext', id.depth); + + var name = id.parts[0]; + if (!name) { + this.opcode('pushContext'); + } else { + this.opcode('lookupOnContext', id.parts[0]); + } + + for(var i=1, l=id.parts.length; i<l; i++) { + this.opcode('lookup', id.parts[i]); + } + }, + + DATA: function(data) { + this.options.data = true; + this.opcode('lookupData', data.id); + }, + + STRING: function(string) { + this.opcode('pushString', string.string); + }, + + INTEGER: function(integer) { + this.opcode('pushLiteral', integer.integer); + }, + + BOOLEAN: function(bool) { + this.opcode('pushLiteral', bool.bool); + }, + + comment: function() {}, + + // HELPERS + opcode: function(name) { + this.opcodes.push({ opcode: name, args: [].slice.call(arguments, 1) }); + }, + + declare: function(name, value) { + this.opcodes.push({ opcode: 'DECLARE', name: name, value: value }); + }, + + addDepth: function(depth) { + if(isNaN(depth)) { throw new Error("EWOT"); } + if(depth === 0) { return; } + + if(!this.depths[depth]) { + this.depths[depth] = true; + this.depths.list.push(depth); + } + }, + + classifyMustache: function(mustache) { + var isHelper = mustache.isHelper; + var isEligible = mustache.eligibleHelper; + var options = this.options; + + // if ambiguous, we can possibly resolve the ambiguity now + if (isEligible && !isHelper) { + var name = mustache.id.parts[0]; + + if (options.knownHelpers[name]) { + isHelper = true; + } else if (options.knownHelpersOnly) { + isEligible = false; + } + } + + if (isHelper) { return "helper"; } + else if (isEligible) { return "ambiguous"; } + else { return "simple"; } + }, + + pushParams: function(params) { + var i = params.length, param; + + while(i--) { + param = params[i]; + + if(this.options.stringParams) { + if(param.depth) { + this.addDepth(param.depth); + } + + this.opcode('getContext', param.depth || 0); + this.opcode('pushStringParam', param.string); + } else { + this[param.type](param); + } + } + }, + + setupMustacheParams: function(mustache) { + var params = mustache.params; + this.pushParams(params); + + if(mustache.hash) { + this.hash(mustache.hash); + } else { + this.opcode('pushLiteral', '{}'); + } + + return params; + }, + + // this will replace setupMustacheParams when we're done + setupFullMustacheParams: function(mustache, program, inverse) { + var params = mustache.params; + this.pushParams(params); + + this.opcode('pushProgram', program); + this.opcode('pushProgram', inverse); + + if(mustache.hash) { + this.hash(mustache.hash); + } else { + this.opcode('pushLiteral', '{}'); + } + + return params; + } + }; + + var Literal = function(value) { + this.value = value; + }; + + JavaScriptCompiler.prototype = { + // PUBLIC API: You can override these methods in a subclass to provide + // alternative compiled forms for name lookup and buffering semantics + nameLookup: function(parent, name, type) { + if (/^[0-9]+$/.test(name)) { + return parent + "[" + name + "]"; + } else if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { + return parent + "." + name; + } + else { + return parent + "['" + name + "']"; + } + }, + + appendToBuffer: function(string) { + if (this.environment.isSimple) { + return "return " + string + ";"; + } else { + return "buffer += " + string + ";"; + } + }, + + initializeBuffer: function() { + return this.quotedString(""); + }, + + namespace: "Handlebars", + // END PUBLIC API + + compile: function(environment, options, context, asObject) { + this.environment = environment; + this.options = options || {}; + + Handlebars.log(Handlebars.logger.DEBUG, this.environment.disassemble() + "\n\n"); + + this.name = this.environment.name; + this.isChild = !!context; + this.context = context || { + programs: [], + aliases: { } + }; + + this.preamble(); + + this.stackSlot = 0; + this.stackVars = []; + this.registers = { list: [] }; + this.compileStack = []; + + this.compileChildren(environment, options); + + var opcodes = environment.opcodes, opcode; + + this.i = 0; + + for(l=opcodes.length; this.i<l; this.i++) { + opcode = opcodes[this.i]; + + if(opcode.opcode === 'DECLARE') { + this[opcode.name] = opcode.value; + } else { + this[opcode.opcode].apply(this, opcode.args); + } + } + + return this.createFunctionContext(asObject); + }, + + nextOpcode: function() { + var opcodes = this.environment.opcodes, opcode = opcodes[this.i + 1]; + return opcodes[this.i + 1]; + }, + + eat: function(opcode) { + this.i = this.i + 1; + }, + + preamble: function() { + var out = []; + + if (!this.isChild) { + var namespace = this.namespace; + var copies = "helpers = helpers || " + namespace + ".helpers;"; + if (this.environment.usePartial) { copies = copies + " partials = partials || " + namespace + ".partials;"; } + if (this.options.data) { copies = copies + " data = data || {};"; } + out.push(copies); + } else { + out.push(''); + } + + if (!this.environment.isSimple) { + out.push(", buffer = " + this.initializeBuffer()); + } else { + out.push(""); + } + + // track the last context pushed into place to allow skipping the + // getContext opcode when it would be a noop + this.lastContext = 0; + this.source = out; + }, + + createFunctionContext: function(asObject) { + var locals = this.stackVars.concat(this.registers.list); + + if(locals.length > 0) { + this.source[1] = this.source[1] + ", " + locals.join(", "); + } + + // Generate minimizer alias mappings + if (!this.isChild) { + var aliases = []; + for (var alias in this.context.aliases) { + this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; + } + } + + if (this.source[1]) { + this.source[1] = "var " + this.source[1].substring(2) + ";"; + } + + // Merge children + if (!this.isChild) { + this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; + } + + if (!this.environment.isSimple) { + this.source.push("return buffer;"); + } + + var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; + + for(var i=0, l=this.environment.depths.list.length; i<l; i++) { + params.push("depth" + this.environment.depths.list[i]); + } + + if (asObject) { + params.push(this.source.join("\n ")); + + return Function.apply(this, params); + } else { + var functionSource = 'function ' + (this.name || '') + '(' + params.join(',') + ') {\n ' + this.source.join("\n ") + '}'; + Handlebars.log(Handlebars.logger.DEBUG, functionSource + "\n\n"); + return functionSource; + } + }, + + // [blockValue] + // + // On stack, before: hash, inverse, program, value + // On stack, after: return value of blockHelperMissing + // + // The purpose of this opcode is to take a block of the form + // `{{#foo}}...{{/foo}}`, resolve the value of `foo`, and + // replace it on the stack with the result of properly + // invoking blockHelperMissing. + blockValue: function() { + this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; + + var params = ["depth0"]; + this.setupParams(0, params); + + this.replaceStack(function(current) { + params.splice(1, 0, current); + return current + " = blockHelperMissing.call(" + params.join(", ") + ")"; + }); + }, + + // [ambiguousBlockValue] + // + // On stack, before: hash, inverse, program, value + // Compiler value, before: lastHelper=value of last found helper, if any + // On stack, after, if no lastHelper: same as [blockValue] + // On stack, after, if lastHelper: value + ambiguousBlockValue: function() { + this.context.aliases.blockHelperMissing = 'helpers.blockHelperMissing'; + + var params = ["depth0"]; + this.setupParams(0, params); + + var current = this.topStack(); + params.splice(1, 0, current); + + this.source.push("if (!" + this.lastHelper + ") { " + current + " = blockHelperMissing.call(" + params.join(", ") + "); }"); + }, + + // [appendContent] + // + // On stack, before: ... + // On stack, after: ... + // + // Appends the string value of `content` to the current buffer + appendContent: function(content) { + this.source.push(this.appendToBuffer(this.quotedString(content))); + }, + + // [append] + // + // On stack, before: value, ... + // On stack, after: ... + // + // Coerces `value` to a String and appends it to the current buffer. + // + // If `value` is truthy, or 0, it is coerced into a string and appended + // Otherwise, the empty string is appended + append: function() { + var local = this.popStack(); + this.source.push("if(" + local + " || " + local + " === 0) { " + this.appendToBuffer(local) + " }"); + if (this.environment.isSimple) { + this.source.push("else { " + this.appendToBuffer("''") + " }"); + } + }, + + // [appendEscaped] + // + // On stack, before: value, ... + // On stack, after: ... + // + // Escape `value` and append it to the buffer + appendEscaped: function() { + var opcode = this.nextOpcode(), extra = ""; + this.context.aliases.escapeExpression = 'this.escapeExpression'; + + if(opcode && opcode.opcode === 'appendContent') { + extra = " + " + this.quotedString(opcode.args[0]); + this.eat(opcode); + } + + this.source.push(this.appendToBuffer("escapeExpression(" + this.popStack() + ")" + extra)); + }, + + // [getContext] + // + // On stack, before: ... + // On stack, after: ... + // Compiler value, after: lastContext=depth + // + // Set the value of the `lastContext` compiler value to the depth + getContext: function(depth) { + if(this.lastContext !== depth) { + this.lastContext = depth; + } + }, + + // [lookupOnContext] + // + // On stack, before: ... + // On stack, after: currentContext[name], ... + // + // Looks up the value of `name` on the current context and pushes + // it onto the stack. + lookupOnContext: function(name) { + this.pushStack(this.nameLookup('depth' + this.lastContext, name, 'context')); + }, + + // [pushContext] + // + // On stack, before: ... + // On stack, after: currentContext, ... + // + // Pushes the value of the current context onto the stack. + pushContext: function() { + this.pushStackLiteral('depth' + this.lastContext); + }, + + // [resolvePossibleLambda] + // + // On stack, before: value, ... + // On stack, after: resolved value, ... + // + // If the `value` is a lambda, replace it on the stack by + // the return value of the lambda + resolvePossibleLambda: function() { + this.context.aliases.functionType = '"function"'; + + this.replaceStack(function(current) { + return "typeof " + current + " === functionType ? " + current + "() : " + current; + }); + }, + + // [lookup] + // + // On stack, before: value, ... + // On stack, after: value[name], ... + // + // Replace the value on the stack with the result of looking + // up `name` on `value` + lookup: function(name) { + this.replaceStack(function(current) { + return current + " == null || " + current + " === false ? " + current + " : " + this.nameLookup(current, name, 'context'); + }); + }, + + // [lookupData] + // + // On stack, before: ... + // On stack, after: data[id], ... + // + // Push the result of looking up `id` on the current data + lookupData: function(id) { + this.pushStack(this.nameLookup('data', id, 'data')); + }, + + // [pushStringParam] + // + // On stack, before: ... + // On stack, after: string, currentContext, ... + // + // This opcode is designed for use in string mode, which + // provides the string value of a parameter along with its + // depth rather than resolving it immediately. + pushStringParam: function(string) { + this.pushStackLiteral('depth' + this.lastContext); + this.pushString(string); + }, + + // [pushString] + // + // On stack, before: ... + // On stack, after: quotedString(string), ... + // + // Push a quoted version of `string` onto the stack + pushString: function(string) { + this.pushStackLiteral(this.quotedString(string)); + }, + + // [push] + // + // On stack, before: ... + // On stack, after: expr, ... + // + // Push an expression onto the stack + push: function(expr) { + this.pushStack(expr); + }, + + // [pushLiteral] + // + // On stack, before: ... + // On stack, after: value, ... + // + // Pushes a value onto the stack. This operation prevents + // the compiler from creating a temporary variable to hold + // it. + pushLiteral: function(value) { + this.pushStackLiteral(value); + }, + + // [pushProgram] + // + // On stack, before: ... + // On stack, after: program(guid), ... + // + // Push a program expression onto the stack. This takes + // a compile-time guid and converts it into a runtime-accessible + // expression. + pushProgram: function(guid) { + if (guid != null) { + this.pushStackLiteral(this.programExpression(guid)); + } else { + this.pushStackLiteral(null); + } + }, + + // [invokeHelper] + // + // On stack, before: hash, inverse, program, params..., ... + // On stack, after: result of helper invocation + // + // Pops off the helper's parameters, invokes the helper, + // and pushes the helper's return value onto the stack. + // + // If the helper is not found, `helperMissing` is called. + invokeHelper: function(paramSize, name) { + this.context.aliases.helperMissing = 'helpers.helperMissing'; + + var helper = this.lastHelper = this.setupHelper(paramSize, name); + this.register('foundHelper', helper.name); + + this.pushStack("foundHelper ? foundHelper.call(" + + helper.callParams + ") " + ": helperMissing.call(" + + helper.helperMissingParams + ")"); + }, + + // [invokeKnownHelper] + // + // On stack, before: hash, inverse, program, params..., ... + // On stack, after: result of helper invocation + // + // This operation is used when the helper is known to exist, + // so a `helperMissing` fallback is not required. + invokeKnownHelper: function(paramSize, name) { + var helper = this.setupHelper(paramSize, name); + this.pushStack(helper.name + ".call(" + helper.callParams + ")"); + }, + + // [invokeAmbiguous] + // + // On stack, before: hash, inverse, program, params..., ... + // On stack, after: result of disambiguation + // + // This operation is used when an expression like `{{foo}}` + // is provided, but we don't know at compile-time whether it + // is a helper or a path. + // + // This operation emits more code than the other options, + // and can be avoided by passing the `knownHelpers` and + // `knownHelpersOnly` flags at compile-time. + invokeAmbiguous: function(name) { + this.context.aliases.functionType = '"function"'; + + this.pushStackLiteral('{}'); + var helper = this.setupHelper(0, name); + + var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); + this.register('foundHelper', helperName); + + var nonHelper = this.nameLookup('depth' + this.lastContext, name, 'context'); + var nextStack = this.nextStack(); + + this.source.push('if (foundHelper) { ' + nextStack + ' = foundHelper.call(' + helper.callParams + '); }'); + this.source.push('else { ' + nextStack + ' = ' + nonHelper + '; ' + nextStack + ' = typeof ' + nextStack + ' === functionType ? ' + nextStack + '() : ' + nextStack + '; }'); + }, + + // [invokePartial] + // + // On stack, before: context, ... + // On stack after: result of partial invocation + // + // This operation pops off a context, invokes a partial with that context, + // and pushes the result of the invocation back. + invokePartial: function(name) { + var params = [this.nameLookup('partials', name, 'partial'), "'" + name + "'", this.popStack(), "helpers", "partials"]; + + if (this.options.data) { + params.push("data"); + } + + this.context.aliases.self = "this"; + this.pushStack("self.invokePartial(" + params.join(", ") + ");"); + }, + + // [assignToHash] + // + // On stack, before: value, hash, ... + // On stack, after: hash, ... + // + // Pops a value and hash off the stack, assigns `hash[key] = value` + // and pushes the hash back onto the stack. + assignToHash: function(key) { + var value = this.popStack(); + var hash = this.topStack(); + + this.source.push(hash + "['" + key + "'] = " + value + ";"); + }, + + // HELPERS + + compiler: JavaScriptCompiler, + + compileChildren: function(environment, options) { + var children = environment.children, child, compiler; + + for(var i=0, l=children.length; i<l; i++) { + child = children[i]; + compiler = new this.compiler(); + + this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children + var index = this.context.programs.length; + child.index = index; + child.name = 'program' + index; + this.context.programs[index] = compiler.compile(child, options, this.context); + } + }, + + programExpression: function(guid) { + this.context.aliases.self = "this"; + + if(guid == null) { + return "self.noop"; + } + + var child = this.environment.children[guid], + depths = child.depths.list, depth; + + var programParams = [child.index, child.name, "data"]; + + for(var i=0, l = depths.length; i<l; i++) { + depth = depths[i]; + + if(depth === 1) { programParams.push("depth0"); } + else { programParams.push("depth" + (depth - 1)); } + } + + if(depths.length === 0) { + return "self.program(" + programParams.join(", ") + ")"; + } else { + programParams.shift(); + return "self.programWithDepth(" + programParams.join(", ") + ")"; + } + }, + + register: function(name, val) { + this.useRegister(name); + this.source.push(name + " = " + val + ";"); + }, + + useRegister: function(name) { + if(!this.registers[name]) { + this.registers[name] = true; + this.registers.list.push(name); + } + }, + + pushStackLiteral: function(item) { + this.compileStack.push(new Literal(item)); + return item; + }, + + pushStack: function(item) { + this.source.push(this.incrStack() + " = " + item + ";"); + this.compileStack.push("stack" + this.stackSlot); + return "stack" + this.stackSlot; + }, + + replaceStack: function(callback) { + var item = callback.call(this, this.topStack()); + + this.source.push(this.topStack() + " = " + item + ";"); + return "stack" + this.stackSlot; + }, + + nextStack: function(skipCompileStack) { + var name = this.incrStack(); + this.compileStack.push("stack" + this.stackSlot); + return name; + }, + + incrStack: function() { + this.stackSlot++; + if(this.stackSlot > this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } + return "stack" + this.stackSlot; + }, + + popStack: function() { + var item = this.compileStack.pop(); + + if (item instanceof Literal) { + return item.value; + } else { + this.stackSlot--; + return item; + } + }, + + topStack: function() { + var item = this.compileStack[this.compileStack.length - 1]; + + if (item instanceof Literal) { + return item.value; + } else { + return item; + } + }, + + quotedString: function(str) { + return '"' + str + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + '"'; + }, + + setupHelper: function(paramSize, name) { + var params = []; + this.setupParams(paramSize, params); + var foundHelper = this.nameLookup('helpers', name, 'helper'); + + return { + params: params, + name: foundHelper, + callParams: ["depth0"].concat(params).join(", "), + helperMissingParams: ["depth0", this.quotedString(name)].concat(params).join(", ") + }; + }, + + // the params and contexts arguments are passed in arrays + // to fill in + setupParams: function(paramSize, params) { + var options = [], contexts = [], param, inverse, program; + + options.push("hash:" + this.popStack()); + + inverse = this.popStack(); + program = this.popStack(); + + // Avoid setting fn and inverse if neither are set. This allows + // helpers to do a check for `if (options.fn)` + if (program || inverse) { + if (!program) { + this.context.aliases.self = "this"; + program = "self.noop"; + } + + if (!inverse) { + this.context.aliases.self = "this"; + inverse = "self.noop"; + } + + options.push("inverse:" + inverse); + options.push("fn:" + program); + } + + for(var i=0; i<paramSize; i++) { + param = this.popStack(); + params.push(param); + + if(this.options.stringParams) { + contexts.push(this.popStack()); + } + } + + if (this.options.stringParams) { + options.push("contexts:[" + contexts.join(",") + "]"); + } + + if(this.options.data) { + options.push("data:data"); + } + + params.push("{" + options.join(",") + "}"); + return params.join(", "); + } + }; + + var reservedWords = ( + "break else new var" + + " case finally return void" + + " catch for switch while" + + " continue function this with" + + " default if throw" + + " delete in try" + + " do instanceof typeof" + + " abstract enum int short" + + " boolean export interface static" + + " byte extends long super" + + " char final native synchronized" + + " class float package throws" + + " const goto private transient" + + " debugger implements protected volatile" + + " double import public let yield" + ).split(" "); + + var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; + + for(var i=0, l=reservedWords.length; i<l; i++) { + compilerWords[reservedWords[i]] = true; + } + + JavaScriptCompiler.isValidJavaScriptVariableName = function(name) { + if(!JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]+$/.test(name)) { + return true; + } + return false; + }; + +})(Handlebars.Compiler, Handlebars.JavaScriptCompiler); + +Handlebars.precompile = function(string, options) { + options = options || {}; + + var ast = Handlebars.parse(string); + var environment = new Handlebars.Compiler().compile(ast, options); + return new Handlebars.JavaScriptCompiler().compile(environment, options); +}; + +Handlebars.compile = function(string, options) { + options = options || {}; + + var compiled; + function compile() { + var ast = Handlebars.parse(string); + var environment = new Handlebars.Compiler().compile(ast, options); + var templateSpec = new Handlebars.JavaScriptCompiler().compile(environment, options, undefined, true); + return Handlebars.template(templateSpec); + } + + // Template is only compiled on first use and cached after that point. + return function(context, options) { + if (!compiled) { + compiled = compile(); + } + return compiled.call(this, context, options); + }; +}; +; +// lib/handlebars/runtime.js +Handlebars.VM = { + template: function(templateSpec) { + // Just add water + var container = { + escapeExpression: Handlebars.Utils.escapeExpression, + invokePartial: Handlebars.VM.invokePartial, + programs: [], + program: function(i, fn, data) { + var programWrapper = this.programs[i]; + if(data) { + return Handlebars.VM.program(fn, data); + } else if(programWrapper) { + return programWrapper; + } else { + programWrapper = this.programs[i] = Handlebars.VM.program(fn); + return programWrapper; + } + }, + programWithDepth: Handlebars.VM.programWithDepth, + noop: Handlebars.VM.noop + }; + + return function(context, options) { + options = options || {}; + return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data); + }; + }, + + programWithDepth: function(fn, data, $depth) { + var args = Array.prototype.slice.call(arguments, 2); + + return function(context, options) { + options = options || {}; + + return fn.apply(this, [context, options.data || data].concat(args)); + }; + }, + program: function(fn, data) { + return function(context, options) { + options = options || {}; + + return fn(context, options.data || data); + }; + }, + noop: function() { return ""; }, + invokePartial: function(partial, name, context, helpers, partials, data) { + var options = { helpers: helpers, partials: partials, data: data }; + + if(partial === undefined) { + throw new Handlebars.Exception("The partial " + name + " could not be found"); + } else if(partial instanceof Function) { + return partial(context, options); + } else if (!Handlebars.compile) { + throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); + } else { + partials[name] = Handlebars.compile(partial, {data: data !== undefined}); + return partials[name](context, options); + } + } +}; + +Handlebars.template = Handlebars.VM.template; +; + +// AMD Define +define(function(){ + return Handlebars; +}); + +})(); diff --git a/pyload/web/app/scripts/vendor/bootstrap-2.3.2.js b/pyload/web/app/scripts/vendor/bootstrap-2.3.2.js new file mode 100755 index 000000000..96fed1387 --- /dev/null +++ b/pyload/web/app/scripts/vendor/bootstrap-2.3.2.js @@ -0,0 +1,2291 @@ +/* =================================================== + * bootstrap-transition.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#transitions + * =================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CSS TRANSITION SUPPORT (http://www.modernizr.com/) + * ======================================================= */ + + $(function () { + + $.support.transition = (function () { + + var transitionEnd = (function () { + + var el = document.createElement('bootstrap') + , transEndEventNames = { + 'WebkitTransition' : 'webkitTransitionEnd' + , 'MozTransition' : 'transitionend' + , 'OTransition' : 'oTransitionEnd otransitionend' + , 'transition' : 'transitionend' + } + , name + + for (name in transEndEventNames){ + if (el.style[name] !== undefined) { + return transEndEventNames[name] + } + } + + }()) + + return transitionEnd && { + end: transitionEnd + } + + })() + + }) + +}(window.jQuery); +/* ========================================================= + * bootstrap-modal.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#modals + * ========================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================= */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* MODAL CLASS DEFINITION + * ====================== */ + + var Modal = function (element, options) { + this.options = options + this.$element = $(element) + .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this)) + this.options.remote && this.$element.find('.modal-body').load(this.options.remote) + } + + Modal.prototype = { + + constructor: Modal + + , toggle: function () { + return this[!this.isShown ? 'show' : 'hide']() + } + + , show: function () { + var that = this + , e = $.Event('show') + + this.$element.trigger(e) + + if (this.isShown || e.isDefaultPrevented()) return + + this.isShown = true + + this.escape() + + this.backdrop(function () { + var transition = $.support.transition && that.$element.hasClass('fade') + + if (!that.$element.parent().length) { + that.$element.appendTo(document.body) //don't move modals dom position + } + + that.$element.show() + + if (transition) { + that.$element[0].offsetWidth // force reflow + } + + that.$element + .addClass('in') + .attr('aria-hidden', false) + + that.enforceFocus() + + transition ? + that.$element.one($.support.transition.end, function () { that.$element.focus().trigger('shown') }) : + that.$element.focus().trigger('shown') + + }) + } + + , hide: function (e) { + e && e.preventDefault() + + var that = this + + e = $.Event('hide') + + this.$element.trigger(e) + + if (!this.isShown || e.isDefaultPrevented()) return + + this.isShown = false + + this.escape() + + $(document).off('focusin.modal') + + this.$element + .removeClass('in') + .attr('aria-hidden', true) + + $.support.transition && this.$element.hasClass('fade') ? + this.hideWithTransition() : + this.hideModal() + } + + , enforceFocus: function () { + var that = this + $(document).on('focusin.modal', function (e) { + if (that.$element[0] !== e.target && !that.$element.has(e.target).length) { + that.$element.focus() + } + }) + } + + , escape: function () { + var that = this + if (this.isShown && this.options.keyboard) { + this.$element.on('keyup.dismiss.modal', function ( e ) { + e.which == 27 && that.hide() + }) + } else if (!this.isShown) { + this.$element.off('keyup.dismiss.modal') + } + } + + , hideWithTransition: function () { + var that = this + , timeout = setTimeout(function () { + that.$element.off($.support.transition.end) + that.hideModal() + }, 500) + + this.$element.one($.support.transition.end, function () { + clearTimeout(timeout) + that.hideModal() + }) + } + + , hideModal: function () { + var that = this + this.$element.hide() + this.backdrop(function () { + that.removeBackdrop() + that.$element.trigger('hidden') + }) + } + + , removeBackdrop: function () { + this.$backdrop && this.$backdrop.remove() + this.$backdrop = null + } + + , backdrop: function (callback) { + var that = this + , animate = this.$element.hasClass('fade') ? 'fade' : '' + + if (this.isShown && this.options.backdrop) { + var doAnimate = $.support.transition && animate + + this.$backdrop = $('<div class="modal-backdrop ' + animate + '" />') + .appendTo(document.body) + + this.$backdrop.click( + this.options.backdrop == 'static' ? + $.proxy(this.$element[0].focus, this.$element[0]) + : $.proxy(this.hide, this) + ) + + if (doAnimate) this.$backdrop[0].offsetWidth // force reflow + + this.$backdrop.addClass('in') + + if (!callback) return + + doAnimate ? + this.$backdrop.one($.support.transition.end, callback) : + callback() + + } else if (!this.isShown && this.$backdrop) { + this.$backdrop.removeClass('in') + + $.support.transition && this.$element.hasClass('fade')? + this.$backdrop.one($.support.transition.end, callback) : + callback() + + } else if (callback) { + callback() + } + } + } + + + /* MODAL PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.modal + + $.fn.modal = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('modal') + , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('modal', (data = new Modal(this, options))) + if (typeof option == 'string') data[option]() + else if (options.show) data.show() + }) + } + + $.fn.modal.defaults = { + backdrop: true + , keyboard: true + , show: true + } + + $.fn.modal.Constructor = Modal + + + /* MODAL NO CONFLICT + * ================= */ + + $.fn.modal.noConflict = function () { + $.fn.modal = old + return this + } + + + /* MODAL DATA-API + * ============== */ + + $(document).on('click.modal.data-api', '[data-toggle="modal"]', function (e) { + var $this = $(this) + , href = $this.attr('href') + , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7 + , option = $target.data('modal') ? 'toggle' : $.extend({ remote:!/#/.test(href) && href }, $target.data(), $this.data()) + + e.preventDefault() + + $target + .modal(option) + .one('hide', function () { + $this.focus() + }) + }) + +}(window.jQuery); + +/* ============================================================ + * bootstrap-dropdown.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#dropdowns + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* DROPDOWN CLASS DEFINITION + * ========================= */ + + var toggle = '[data-toggle=dropdown]' + , Dropdown = function (element) { + var $el = $(element).on('click.dropdown.data-api', this.toggle) + $('html').on('click.dropdown.data-api', function () { + $el.parent().removeClass('open') + }) + } + + Dropdown.prototype = { + + constructor: Dropdown + + , toggle: function (e) { + var $this = $(this) + , $parent + , isActive + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement) { + // if mobile we we use a backdrop because click events don't delegate + $('<div class="dropdown-backdrop"/>').insertBefore($(this)).on('click', clearMenus) + } + $parent.toggleClass('open') + } + + $this.focus() + + return false + } + + , keydown: function (e) { + var $this + , $items + , $active + , $parent + , isActive + , index + + if (!/(38|40|27)/.test(e.keyCode)) return + + $this = $(this) + + e.preventDefault() + e.stopPropagation() + + if ($this.is('.disabled, :disabled')) return + + $parent = getParent($this) + + isActive = $parent.hasClass('open') + + if (!isActive || (isActive && e.keyCode == 27)) { + if (e.which == 27) $parent.find(toggle).focus() + return $this.click() + } + + $items = $('[role=menu] li:not(.divider):visible a', $parent) + + if (!$items.length) return + + index = $items.index($items.filter(':focus')) + + if (e.keyCode == 38 && index > 0) index-- // up + if (e.keyCode == 40 && index < $items.length - 1) index++ // down + if (!~index) index = 0 + + $items + .eq(index) + .focus() + } + + } + + function clearMenus() { + $('.dropdown-backdrop').remove() + $(toggle).each(function () { + getParent($(this)).removeClass('open') + }) + } + + function getParent($this) { + var selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = selector && $(selector) + + if (!$parent || !$parent.length) $parent = $this.parent() + + return $parent + } + + + /* DROPDOWN PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.dropdown + + $.fn.dropdown = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('dropdown') + if (!data) $this.data('dropdown', (data = new Dropdown(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.dropdown.Constructor = Dropdown + + + /* DROPDOWN NO CONFLICT + * ==================== */ + + $.fn.dropdown.noConflict = function () { + $.fn.dropdown = old + return this + } + + + /* APPLY TO STANDARD DROPDOWN ELEMENTS + * =================================== */ + + $(document) + .on('click.dropdown.data-api', clearMenus) + .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) + .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) + .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + +}(window.jQuery); + +/* ============================================================= + * bootstrap-scrollspy.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#scrollspy + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* SCROLLSPY CLASS DEFINITION + * ========================== */ + + function ScrollSpy(element, options) { + var process = $.proxy(this.process, this) + , $element = $(element).is('body') ? $(window) : $(element) + , href + this.options = $.extend({}, $.fn.scrollspy.defaults, options) + this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process) + this.selector = (this.options.target + || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + || '') + ' .nav li > a' + this.$body = $('body') + this.refresh() + this.process() + } + + ScrollSpy.prototype = { + + constructor: ScrollSpy + + , refresh: function () { + var self = this + , $targets + + this.offsets = $([]) + this.targets = $([]) + + $targets = this.$body + .find(this.selector) + .map(function () { + var $el = $(this) + , href = $el.data('target') || $el.attr('href') + , $href = /^#\w/.test(href) && $(href) + return ( $href + && $href.length + && [[ $href.position().top + (!$.isWindow(self.$scrollElement.get(0)) && self.$scrollElement.scrollTop()), href ]] ) || null + }) + .sort(function (a, b) { return a[0] - b[0] }) + .each(function () { + self.offsets.push(this[0]) + self.targets.push(this[1]) + }) + } + + , process: function () { + var scrollTop = this.$scrollElement.scrollTop() + this.options.offset + , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight + , maxScroll = scrollHeight - this.$scrollElement.height() + , offsets = this.offsets + , targets = this.targets + , activeTarget = this.activeTarget + , i + + if (scrollTop >= maxScroll) { + return activeTarget != (i = targets.last()[0]) + && this.activate ( i ) + } + + for (i = offsets.length; i--;) { + activeTarget != targets[i] + && scrollTop >= offsets[i] + && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) + && this.activate( targets[i] ) + } + } + + , activate: function (target) { + var active + , selector + + this.activeTarget = target + + $(this.selector) + .parent('.active') + .removeClass('active') + + selector = this.selector + + '[data-target="' + target + '"],' + + this.selector + '[href="' + target + '"]' + + active = $(selector) + .parent('li') + .addClass('active') + + if (active.parent('.dropdown-menu').length) { + active = active.closest('li.dropdown').addClass('active') + } + + active.trigger('activate') + } + + } + + + /* SCROLLSPY PLUGIN DEFINITION + * =========================== */ + + var old = $.fn.scrollspy + + $.fn.scrollspy = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('scrollspy') + , options = typeof option == 'object' && option + if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.scrollspy.Constructor = ScrollSpy + + $.fn.scrollspy.defaults = { + offset: 10 + } + + + /* SCROLLSPY NO CONFLICT + * ===================== */ + + $.fn.scrollspy.noConflict = function () { + $.fn.scrollspy = old + return this + } + + + /* SCROLLSPY DATA-API + * ================== */ + + $(window).on('load', function () { + $('[data-spy="scroll"]').each(function () { + var $spy = $(this) + $spy.scrollspy($spy.data()) + }) + }) + +}(window.jQuery); +/* ======================================================== + * bootstrap-tab.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#tabs + * ======================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ======================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TAB CLASS DEFINITION + * ==================== */ + + var Tab = function (element) { + this.element = $(element) + } + + Tab.prototype = { + + constructor: Tab + + , show: function () { + var $this = this.element + , $ul = $this.closest('ul:not(.dropdown-menu)') + , selector = $this.attr('data-target') + , previous + , $target + , e + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + if ( $this.parent('li').hasClass('active') ) return + + previous = $ul.find('.active:last a')[0] + + e = $.Event('show', { + relatedTarget: previous + }) + + $this.trigger(e) + + if (e.isDefaultPrevented()) return + + $target = $(selector) + + this.activate($this.parent('li'), $ul) + this.activate($target, $target.parent(), function () { + $this.trigger({ + type: 'shown' + , relatedTarget: previous + }) + }) + } + + , activate: function ( element, container, callback) { + var $active = container.find('> .active') + , transition = callback + && $.support.transition + && $active.hasClass('fade') + + function next() { + $active + .removeClass('active') + .find('> .dropdown-menu > .active') + .removeClass('active') + + element.addClass('active') + + if (transition) { + element[0].offsetWidth // reflow for transition + element.addClass('in') + } else { + element.removeClass('fade') + } + + if ( element.parent('.dropdown-menu') ) { + element.closest('li.dropdown').addClass('active') + } + + callback && callback() + } + + transition ? + $active.one($.support.transition.end, next) : + next() + + $active.removeClass('in') + } + } + + + /* TAB PLUGIN DEFINITION + * ===================== */ + + var old = $.fn.tab + + $.fn.tab = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tab') + if (!data) $this.data('tab', (data = new Tab(this))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tab.Constructor = Tab + + + /* TAB NO CONFLICT + * =============== */ + + $.fn.tab.noConflict = function () { + $.fn.tab = old + return this + } + + + /* TAB DATA-API + * ============ */ + + $(document).on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { + e.preventDefault() + $(this).tab('show') + }) + +}(window.jQuery); +/* =========================================================== + * bootstrap-tooltip.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION + * =============================== */ + + var Tooltip = function (element, options) { + this.init('tooltip', element, options) + } + + Tooltip.prototype = { + + constructor: Tooltip + + , init: function (type, element, options) { + var eventIn + , eventOut + , triggers + , trigger + , i + + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.enabled = true + + triggers = this.options.trigger.split(' ') + + for (i = triggers.length; i--;) { + trigger = triggers[i] + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + , getOptions: function (options) { + options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + , enter: function (e) { + var defaults = $.fn[this.type].defaults + , options = {} + , self + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }, this) + + self = $(e.currentTarget)[this.type](options).data(this.type) + + if (!self.options.delay || !self.options.delay.show) return self.show() + + clearTimeout(this.timeout) + self.hoverState = 'in' + this.timeout = setTimeout(function() { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + , leave: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (this.timeout) clearTimeout(this.timeout) + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.hoverState = 'out' + this.timeout = setTimeout(function() { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + , show: function () { + var $tip + , pos + , actualWidth + , actualHeight + , placement + , tp + , e = $.Event('show') + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $tip = this.tip() + this.setContent() + + if (this.options.animation) { + $tip.addClass('fade') + } + + placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + + pos = this.getPosition() + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + switch (placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} + break + } + + this.applyPlacement(tp, placement) + this.$element.trigger('shown') + } + } + + , applyPlacement: function(offset, placement){ + var $tip = this.tip() + , width = $tip[0].offsetWidth + , height = $tip[0].offsetHeight + , actualWidth + , actualHeight + , delta + , replace + + $tip + .offset(offset) + .addClass(placement) + .addClass('in') + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + offset.top = offset.top + height - actualHeight + replace = true + } + + if (placement == 'bottom' || placement == 'top') { + delta = 0 + + if (offset.left < 0){ + delta = offset.left * -2 + offset.left = 0 + $tip.offset(offset) + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + } + + this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') + } else { + this.replaceArrow(actualHeight - height, actualHeight, 'top') + } + + if (replace) $tip.offset(offset) + } + + , replaceArrow: function(delta, dimension, position){ + this + .arrow() + .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') + } + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + , hide: function () { + var that = this + , $tip = this.tip() + , e = $.Event('hide') + + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + function removeWithAnimation() { + var timeout = setTimeout(function () { + $tip.off($.support.transition.end).detach() + }, 500) + + $tip.one($.support.transition.end, function () { + clearTimeout(timeout) + $tip.detach() + }) + } + + $.support.transition && this.$tip.hasClass('fade') ? + removeWithAnimation() : + $tip.detach() + + this.$element.trigger('hidden') + + return this + } + + , fixTitle: function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getPosition: function () { + var el = this.$element[0] + return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { + width: el.offsetWidth + , height: el.offsetHeight + }, this.$element.offset()) + } + + , getTitle: function () { + var title + , $e = this.$element + , o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + , tip: function () { + return this.$tip = this.$tip || $(this.options.template) + } + + , arrow: function(){ + return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") + } + + , validate: function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function () { + this.enabled = true + } + + , disable: function () { + this.enabled = false + } + + , toggleEnabled: function () { + this.enabled = !this.enabled + } + + , toggle: function (e) { + var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this + self.tip().hasClass('in') ? self.hide() : self.show() + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + } + + + /* TOOLTIP PLUGIN DEFINITION + * ========================= */ + + var old = $.fn.tooltip + + $.fn.tooltip = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tooltip') + , options = typeof option == 'object' && option + if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + $.fn.tooltip.defaults = { + animation: true + , placement: 'top' + , selector: false + , template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' + , trigger: 'hover focus' + , title: '' + , delay: 0 + , html: false + , container: false + } + + + /* TOOLTIP NO CONFLICT + * =================== */ + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(window.jQuery); + +/* =========================================================== + * bootstrap-popover.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#popovers + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * =========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* POPOVER PUBLIC CLASS DEFINITION + * =============================== */ + + var Popover = function (element, options) { + this.init('popover', element, options) + } + + + /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js + ========================================== */ + + Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { + + constructor: Popover + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + , content = this.getContent() + + $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) + $tip.find('.popover-content')[this.options.html ? 'html' : 'text'](content) + + $tip.removeClass('fade top bottom left right in') + } + + , hasContent: function () { + return this.getTitle() || this.getContent() + } + + , getContent: function () { + var content + , $e = this.$element + , o = this.options + + content = (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) + || $e.attr('data-content') + + return content + } + + , tip: function () { + if (!this.$tip) { + this.$tip = $(this.options.template) + } + return this.$tip + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + }) + + + /* POPOVER PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.popover + + $.fn.popover = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('popover') + , options = typeof option == 'object' && option + if (!data) $this.data('popover', (data = new Popover(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.popover.Constructor = Popover + + $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { + placement: 'right' + , trigger: 'click' + , content: '' + , template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' + }) + + + /* POPOVER NO CONFLICT + * =================== */ + + $.fn.popover.noConflict = function () { + $.fn.popover = old + return this + } + +}(window.jQuery); + +/* ========================================================== + * bootstrap-affix.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#affix + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* AFFIX CLASS DEFINITION + * ====================== */ + + var Affix = function (element, options) { + this.options = $.extend({}, $.fn.affix.defaults, options) + this.$window = $(window) + .on('scroll.affix.data-api', $.proxy(this.checkPosition, this)) + .on('click.affix.data-api', $.proxy(function () { setTimeout($.proxy(this.checkPosition, this), 1) }, this)) + this.$element = $(element) + this.checkPosition() + } + + Affix.prototype.checkPosition = function () { + if (!this.$element.is(':visible')) return + + var scrollHeight = $(document).height() + , scrollTop = this.$window.scrollTop() + , position = this.$element.offset() + , offset = this.options.offset + , offsetBottom = offset.bottom + , offsetTop = offset.top + , reset = 'affix affix-top affix-bottom' + , affix + + if (typeof offset != 'object') offsetBottom = offsetTop = offset + if (typeof offsetTop == 'function') offsetTop = offset.top() + if (typeof offsetBottom == 'function') offsetBottom = offset.bottom() + + affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ? + false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ? + 'bottom' : offsetTop != null && scrollTop <= offsetTop ? + 'top' : false + + if (this.affixed === affix) return + + this.affixed = affix + this.unpin = affix == 'bottom' ? position.top - scrollTop : null + + this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : '')) + } + + + /* AFFIX PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.affix + + $.fn.affix = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('affix') + , options = typeof option == 'object' && option + if (!data) $this.data('affix', (data = new Affix(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.affix.Constructor = Affix + + $.fn.affix.defaults = { + offset: 0 + } + + + /* AFFIX NO CONFLICT + * ================= */ + + $.fn.affix.noConflict = function () { + $.fn.affix = old + return this + } + + + /* AFFIX DATA-API + * ============== */ + + $(window).on('load', function () { + $('[data-spy="affix"]').each(function () { + var $spy = $(this) + , data = $spy.data() + + data.offset = data.offset || {} + + data.offsetBottom && (data.offset.bottom = data.offsetBottom) + data.offsetTop && (data.offset.top = data.offsetTop) + + $spy.affix(data) + }) + }) + + +}(window.jQuery); +/* ========================================================== + * bootstrap-alert.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#alerts + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* ALERT CLASS DEFINITION + * ====================== */ + + var dismiss = '[data-dismiss="alert"]' + , Alert = function (el) { + $(el).on('click', dismiss, this.close) + } + + Alert.prototype.close = function (e) { + var $this = $(this) + , selector = $this.attr('data-target') + , $parent + + if (!selector) { + selector = $this.attr('href') + selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 + } + + $parent = $(selector) + + e && e.preventDefault() + + $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) + + $parent.trigger(e = $.Event('close')) + + if (e.isDefaultPrevented()) return + + $parent.removeClass('in') + + function removeElement() { + $parent + .trigger('closed') + .remove() + } + + $.support.transition && $parent.hasClass('fade') ? + $parent.on($.support.transition.end, removeElement) : + removeElement() + } + + + /* ALERT PLUGIN DEFINITION + * ======================= */ + + var old = $.fn.alert + + $.fn.alert = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('alert') + if (!data) $this.data('alert', (data = new Alert(this))) + if (typeof option == 'string') data[option].call($this) + }) + } + + $.fn.alert.Constructor = Alert + + + /* ALERT NO CONFLICT + * ================= */ + + $.fn.alert.noConflict = function () { + $.fn.alert = old + return this + } + + + /* ALERT DATA-API + * ============== */ + + $(document).on('click.alert.data-api', dismiss, Alert.prototype.close) + +}(window.jQuery); +/* ============================================================ + * bootstrap-button.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#buttons + * ============================================================ + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* BUTTON PUBLIC CLASS DEFINITION + * ============================== */ + + var Button = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.button.defaults, options) + } + + Button.prototype.setState = function (state) { + var d = 'disabled' + , $el = this.$element + , data = $el.data() + , val = $el.is('input') ? 'val' : 'html' + + state = state + 'Text' + data.resetText || $el.data('resetText', $el[val]()) + + $el[val](data[state] || this.options[state]) + + // push to event loop to allow forms to submit + setTimeout(function () { + state == 'loadingText' ? + $el.addClass(d).attr(d, d) : + $el.removeClass(d).removeAttr(d) + }, 0) + } + + Button.prototype.toggle = function () { + var $parent = this.$element.closest('[data-toggle="buttons-radio"]') + + $parent && $parent + .find('.active') + .removeClass('active') + + this.$element.toggleClass('active') + } + + + /* BUTTON PLUGIN DEFINITION + * ======================== */ + + var old = $.fn.button + + $.fn.button = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('button') + , options = typeof option == 'object' && option + if (!data) $this.data('button', (data = new Button(this, options))) + if (option == 'toggle') data.toggle() + else if (option) data.setState(option) + }) + } + + $.fn.button.defaults = { + loadingText: 'loading...' + } + + $.fn.button.Constructor = Button + + + /* BUTTON NO CONFLICT + * ================== */ + + $.fn.button.noConflict = function () { + $.fn.button = old + return this + } + + + /* BUTTON DATA-API + * =============== */ + + $(document).on('click.button.data-api', '[data-toggle^=button]', function (e) { + var $btn = $(e.target) + if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') + $btn.button('toggle') + }) + +}(window.jQuery); +/* ============================================================= + * bootstrap-collapse.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#collapse + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* COLLAPSE PUBLIC CLASS DEFINITION + * ================================ */ + + var Collapse = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.collapse.defaults, options) + + if (this.options.parent) { + this.$parent = $(this.options.parent) + } + + this.options.toggle && this.toggle() + } + + Collapse.prototype = { + + constructor: Collapse + + , dimension: function () { + var hasWidth = this.$element.hasClass('width') + return hasWidth ? 'width' : 'height' + } + + , show: function () { + var dimension + , scroll + , actives + , hasData + + if (this.transitioning || this.$element.hasClass('in')) return + + dimension = this.dimension() + scroll = $.camelCase(['scroll', dimension].join('-')) + actives = this.$parent && this.$parent.find('> .accordion-group > .in') + + if (actives && actives.length) { + hasData = actives.data('collapse') + if (hasData && hasData.transitioning) return + actives.collapse('hide') + hasData || actives.data('collapse', null) + } + + this.$element[dimension](0) + this.transition('addClass', $.Event('show'), 'shown') + $.support.transition && this.$element[dimension](this.$element[0][scroll]) + } + + , hide: function () { + var dimension + if (this.transitioning || !this.$element.hasClass('in')) return + dimension = this.dimension() + this.reset(this.$element[dimension]()) + this.transition('removeClass', $.Event('hide'), 'hidden') + this.$element[dimension](0) + } + + , reset: function (size) { + var dimension = this.dimension() + + this.$element + .removeClass('collapse') + [dimension](size || 'auto') + [0].offsetWidth + + this.$element[size !== null ? 'addClass' : 'removeClass']('collapse') + + return this + } + + , transition: function (method, startEvent, completeEvent) { + var that = this + , complete = function () { + if (startEvent.type == 'show') that.reset() + that.transitioning = 0 + that.$element.trigger(completeEvent) + } + + this.$element.trigger(startEvent) + + if (startEvent.isDefaultPrevented()) return + + this.transitioning = 1 + + this.$element[method]('in') + + $.support.transition && this.$element.hasClass('collapse') ? + this.$element.one($.support.transition.end, complete) : + complete() + } + + , toggle: function () { + this[this.$element.hasClass('in') ? 'hide' : 'show']() + } + + } + + + /* COLLAPSE PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.collapse + + $.fn.collapse = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('collapse') + , options = $.extend({}, $.fn.collapse.defaults, $this.data(), typeof option == 'object' && option) + if (!data) $this.data('collapse', (data = new Collapse(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.collapse.defaults = { + toggle: true + } + + $.fn.collapse.Constructor = Collapse + + + /* COLLAPSE NO CONFLICT + * ==================== */ + + $.fn.collapse.noConflict = function () { + $.fn.collapse = old + return this + } + + + /* COLLAPSE DATA-API + * ================= */ + + $(document).on('click.collapse.data-api', '[data-toggle=collapse]', function (e) { + var $this = $(this), href + , target = $this.attr('data-target') + || e.preventDefault() + || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7 + , option = $(target).data('collapse') ? 'toggle' : $this.data() + $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed') + $(target).collapse(option) + }) + +}(window.jQuery); +/* ========================================================== + * bootstrap-carousel.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#carousel + * ========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* CAROUSEL CLASS DEFINITION + * ========================= */ + + var Carousel = function (element, options) { + this.$element = $(element) + this.$indicators = this.$element.find('.carousel-indicators') + this.options = options + this.options.pause == 'hover' && this.$element + .on('mouseenter', $.proxy(this.pause, this)) + .on('mouseleave', $.proxy(this.cycle, this)) + } + + Carousel.prototype = { + + cycle: function (e) { + if (!e) this.paused = false + if (this.interval) clearInterval(this.interval); + this.options.interval + && !this.paused + && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) + return this + } + + , getActiveIndex: function () { + this.$active = this.$element.find('.item.active') + this.$items = this.$active.parent().children() + return this.$items.index(this.$active) + } + + , to: function (pos) { + var activeIndex = this.getActiveIndex() + , that = this + + if (pos > (this.$items.length - 1) || pos < 0) return + + if (this.sliding) { + return this.$element.one('slid', function () { + that.to(pos) + }) + } + + if (activeIndex == pos) { + return this.pause().cycle() + } + + return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos])) + } + + , pause: function (e) { + if (!e) this.paused = true + if (this.$element.find('.next, .prev').length && $.support.transition.end) { + this.$element.trigger($.support.transition.end) + this.cycle(true) + } + clearInterval(this.interval) + this.interval = null + return this + } + + , next: function () { + if (this.sliding) return + return this.slide('next') + } + + , prev: function () { + if (this.sliding) return + return this.slide('prev') + } + + , slide: function (type, next) { + var $active = this.$element.find('.item.active') + , $next = next || $active[type]() + , isCycling = this.interval + , direction = type == 'next' ? 'left' : 'right' + , fallback = type == 'next' ? 'first' : 'last' + , that = this + , e + + this.sliding = true + + isCycling && this.pause() + + $next = $next.length ? $next : this.$element.find('.item')[fallback]() + + e = $.Event('slide', { + relatedTarget: $next[0] + , direction: direction + }) + + if ($next.hasClass('active')) return + + if (this.$indicators.length) { + this.$indicators.find('.active').removeClass('active') + this.$element.one('slid', function () { + var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()]) + $nextIndicator && $nextIndicator.addClass('active') + }) + } + + if ($.support.transition && this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $next.addClass(type) + $next[0].offsetWidth // force reflow + $active.addClass(direction) + $next.addClass(direction) + this.$element.one($.support.transition.end, function () { + $next.removeClass([type, direction].join(' ')).addClass('active') + $active.removeClass(['active', direction].join(' ')) + that.sliding = false + setTimeout(function () { that.$element.trigger('slid') }, 0) + }) + } else { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.removeClass('active') + $next.addClass('active') + this.sliding = false + this.$element.trigger('slid') + } + + isCycling && this.cycle() + + return this + } + + } + + + /* CAROUSEL PLUGIN DEFINITION + * ========================== */ + + var old = $.fn.carousel + + $.fn.carousel = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('carousel') + , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option) + , action = typeof option == 'string' ? option : options.slide + if (!data) $this.data('carousel', (data = new Carousel(this, options))) + if (typeof option == 'number') data.to(option) + else if (action) data[action]() + else if (options.interval) data.pause().cycle() + }) + } + + $.fn.carousel.defaults = { + interval: 5000 + , pause: 'hover' + } + + $.fn.carousel.Constructor = Carousel + + + /* CAROUSEL NO CONFLICT + * ==================== */ + + $.fn.carousel.noConflict = function () { + $.fn.carousel = old + return this + } + + /* CAROUSEL DATA-API + * ================= */ + + $(document).on('click.carousel.data-api', '[data-slide], [data-slide-to]', function (e) { + var $this = $(this), href + , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 + , options = $.extend({}, $target.data(), $this.data()) + , slideIndex + + $target.carousel(options) + + if (slideIndex = $this.attr('data-slide-to')) { + $target.data('carousel').pause().to(slideIndex).cycle() + } + + e.preventDefault() + }) + +}(window.jQuery); +/* ============================================================= + * bootstrap-typeahead.js v2.3.2 + * http://twitter.github.com/bootstrap/javascript.html#typeahead + * ============================================================= + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============================================================ */ + + +!function($){ + + "use strict"; // jshint ;_; + + + /* TYPEAHEAD PUBLIC CLASS DEFINITION + * ================================= */ + + var Typeahead = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.typeahead.defaults, options) + this.matcher = this.options.matcher || this.matcher + this.sorter = this.options.sorter || this.sorter + this.highlighter = this.options.highlighter || this.highlighter + this.updater = this.options.updater || this.updater + this.source = this.options.source + this.$menu = $(this.options.menu) + this.shown = false + this.listen() + } + + Typeahead.prototype = { + + constructor: Typeahead + + , select: function () { + var val = this.$menu.find('.active').attr('data-value') + this.$element + .val(this.updater(val)) + .change() + return this.hide() + } + + , updater: function (item) { + return item + } + + , show: function () { + var pos = $.extend({}, this.$element.position(), { + height: this.$element[0].offsetHeight + }) + + this.$menu + .insertAfter(this.$element) + .css({ + top: pos.top + pos.height + , left: pos.left + }) + .show() + + this.shown = true + return this + } + + , hide: function () { + this.$menu.hide() + this.shown = false + return this + } + + , lookup: function (event) { + var items + + this.query = this.$element.val() + + if (!this.query || this.query.length < this.options.minLength) { + return this.shown ? this.hide() : this + } + + items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source + + return items ? this.process(items) : this + } + + , process: function (items) { + var that = this + + items = $.grep(items, function (item) { + return that.matcher(item) + }) + + items = this.sorter(items) + + if (!items.length) { + return this.shown ? this.hide() : this + } + + return this.render(items.slice(0, this.options.items)).show() + } + + , matcher: function (item) { + return ~item.toLowerCase().indexOf(this.query.toLowerCase()) + } + + , sorter: function (items) { + var beginswith = [] + , caseSensitive = [] + , caseInsensitive = [] + , item + + while (item = items.shift()) { + if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) + else if (~item.indexOf(this.query)) caseSensitive.push(item) + else caseInsensitive.push(item) + } + + return beginswith.concat(caseSensitive, caseInsensitive) + } + + , highlighter: function (item) { + var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') + return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { + return '<strong>' + match + '</strong>' + }) + } + + , render: function (items) { + var that = this + + items = $(items).map(function (i, item) { + i = $(that.options.item).attr('data-value', item) + i.find('a').html(that.highlighter(item)) + return i[0] + }) + + items.first().addClass('active') + this.$menu.html(items) + return this + } + + , next: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , next = active.next() + + if (!next.length) { + next = $(this.$menu.find('li')[0]) + } + + next.addClass('active') + } + + , prev: function (event) { + var active = this.$menu.find('.active').removeClass('active') + , prev = active.prev() + + if (!prev.length) { + prev = this.$menu.find('li').last() + } + + prev.addClass('active') + } + + , listen: function () { + this.$element + .on('focus', $.proxy(this.focus, this)) + .on('blur', $.proxy(this.blur, this)) + .on('keypress', $.proxy(this.keypress, this)) + .on('keyup', $.proxy(this.keyup, this)) + + if (this.eventSupported('keydown')) { + this.$element.on('keydown', $.proxy(this.keydown, this)) + } + + this.$menu + .on('click', $.proxy(this.click, this)) + .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) + .on('mouseleave', 'li', $.proxy(this.mouseleave, this)) + } + + , eventSupported: function(eventName) { + var isSupported = eventName in this.$element + if (!isSupported) { + this.$element.setAttribute(eventName, 'return;') + isSupported = typeof this.$element[eventName] === 'function' + } + return isSupported + } + + , move: function (e) { + if (!this.shown) return + + switch(e.keyCode) { + case 9: // tab + case 13: // enter + case 27: // escape + e.preventDefault() + break + + case 38: // up arrow + e.preventDefault() + this.prev() + break + + case 40: // down arrow + e.preventDefault() + this.next() + break + } + + e.stopPropagation() + } + + , keydown: function (e) { + this.suppressKeyPressRepeat = ~$.inArray(e.keyCode, [40,38,9,13,27]) + this.move(e) + } + + , keypress: function (e) { + if (this.suppressKeyPressRepeat) return + this.move(e) + } + + , keyup: function (e) { + switch(e.keyCode) { + case 40: // down arrow + case 38: // up arrow + case 16: // shift + case 17: // ctrl + case 18: // alt + break + + case 9: // tab + case 13: // enter + if (!this.shown) return + this.select() + break + + case 27: // escape + if (!this.shown) return + this.hide() + break + + default: + this.lookup() + } + + e.stopPropagation() + e.preventDefault() + } + + , focus: function (e) { + this.focused = true + } + + , blur: function (e) { + this.focused = false + if (!this.mousedover && this.shown) this.hide() + } + + , click: function (e) { + e.stopPropagation() + e.preventDefault() + this.select() + this.$element.focus() + } + + , mouseenter: function (e) { + this.mousedover = true + this.$menu.find('.active').removeClass('active') + $(e.currentTarget).addClass('active') + } + + , mouseleave: function (e) { + this.mousedover = false + if (!this.focused && this.shown) this.hide() + } + + } + + + /* TYPEAHEAD PLUGIN DEFINITION + * =========================== */ + + var old = $.fn.typeahead + + $.fn.typeahead = function (option) { + return this.each(function () { + var $this = $(this) + , data = $this.data('typeahead') + , options = typeof option == 'object' && option + if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.typeahead.defaults = { + source: [] + , items: 8 + , menu: '<ul class="typeahead dropdown-menu"></ul>' + , item: '<li><a href="#"></a></li>' + , minLength: 1 + } + + $.fn.typeahead.Constructor = Typeahead + + + /* TYPEAHEAD NO CONFLICT + * =================== */ + + $.fn.typeahead.noConflict = function () { + $.fn.typeahead = old + return this + } + + + /* TYPEAHEAD DATA-API + * ================== */ + + $(document).on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { + var $this = $(this) + if ($this.data('typeahead')) return + $this.typeahead($this.data()) + }) + +}(window.jQuery); 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..9d1d72869 --- /dev/null +++ b/pyload/web/app/scripts/views/abstract/modalView.js @@ -0,0 +1,125 @@ +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(['text!tpl/default/modal.html'], 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..e7adba475 --- /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({ + + // TODO: refactor + active: $('.breadcrumb .active'), + + 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'); + + // TODO: Both null + 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.active.text(pack.get('name')); + self.contentReady(pack.get('files')); + }, failure: function() { + self.failure(); + }}); + + }, + + contentReady: function(files) { + var old_files = this.files; + this.files = files; + App.vent.trigger('dashboard:contentReady'); + + // show the files when no loading animation is running and not already open + if (!this.isLoading && old_files !== files) + this.show(); + }, + + // Do load animation, remove the old stuff + loading: function() { + this.isLoading = true; + this.files = null; + var self = this; + $(this.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) { + // this works with ids and object + var file = this.files.get(data); + if (file) + if (_.isObject(data)) // update directly + file.set(data); + else // fetch from server + file.fetch(); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/dashboard/fileView.js b/pyload/web/app/scripts/views/dashboard/fileView.js new file mode 100644 index 000000000..4e5884ed8 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/fileView.js @@ -0,0 +1,102 @@ +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() { + 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 renderred 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..64bc56724 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/filterView.js @@ -0,0 +1,133 @@ +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 .filter-type': 'filter_type', + 'click .filter-state': 'switch_filter', + 'submit .form-search': 'search' + }, + + ui: { + 'search': '.search-query', + 'stateMenu': '.dropdown-toggle .state' + }, + + template: template, + state: null, + + initialize: function() { + this.state = Api.DownloadState.All; + + // Apply the filter before the content is shown + App.vent.on('dashboard:contentReady', _.bind(this.apply_filter, this)); + }, + + 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; + }, + + 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..8685fd849 --- /dev/null +++ b/pyload/web/app/scripts/views/dashboard/selectionView.js @@ -0,0 +1,155 @@ +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); + + this.actionBar = $('.actionbar .btn-check'); + this.actionBar.parent().click(_.bind(this.select_toggle, this)); + + // API events, maybe better to rely on internal ones? + App.vent.on('package:deleted', render); + App.vent.on('file:deleted', render); + }, + + get_files: function(all) { + var files = []; + if (App.dashboard.files) + if (all) + files = App.dashboard.files.where({visible: true}); + else + files = App.dashboard.files.where({selected: true, visible: true}); + + return files; + }, + + get_packs: function() { + 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) { + this.actionBar.addClass('icon-check').removeClass('icon-check-empty'); + App.dashboard.ui.packages.addClass('ui-files-selected'); + } + else { + this.actionBar.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('default/confirmDialog.html', function() { + + var pids = []; + // TODO: delete many at once + this.get_packs().map(function(pack) { + pids.push(pack.get('pid')); + pack.destroy(); + }); + + // get only the fids of non deleted packages + var fids = _.filter(this.get_files(),function(file) { + return !_.contains(pids, file.get('package')); + }).map(function(file) { + file.destroyLocal(); + return file.get('fid'); + }); + + if (fids.length > 0) + $.ajax(App.apiRequest('deleteFiles', {fids: fids})); + + this.deselect(); + }, this); + }, + + restart: function() { + this.get_files().map(function(file) { + file.restart(); + }); + this.get_packs().map(function(pack) { + pack.restart(); + }); + + this.deselect(); + }, + + // Select or deselect all visible files + select_toggle: function() { + var files = this.get_files(); + if (files.length === 0) { + this.get_files(true).map(function(file) { + file.set('selected', true); + }); + + } else + files.map(function(file) { + file.set('selected', false); + }); + + this.render(); + } + }); + });
\ No newline at end of file diff --git a/pyload/web/app/scripts/views/headerView.js b/pyload/web/app/scripts/views/headerView.js new file mode 100644 index 000000000..512c7259b --- /dev/null +++ b/pyload/web/app/scripts/views/headerView.js @@ -0,0 +1,240 @@ +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') { + // TODO: load interaction when none available + this.status.set(data); + + // There tasks at the server, but not in queue: so fetch them + // or there are tasks in our queue but not on the server + if (this.status.get('notifications') && !this.notificationView.tasks.hasTaskWaiting() || + !this.status.get('notifications') && this.notificationView.tasks.hasTaskWaiting()) + this.notificationView.tasks.fetch(); + + this.speeds = this.speeds.slice(1); + this.speeds.push([this.speeds[this.speeds.length - 1][0] + 1, Math.floor(data.speed / 1024)]); + + // TODO: if everything is 0 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.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..abfcd8079 --- /dev/null +++ b/pyload/web/app/scripts/views/notificationView.js @@ -0,0 +1,83 @@ +define(['jquery', 'backbone', 'underscore', 'app', 'collections/InteractionList', 'hbs!tpl/notification'], + function($, Backbone, _, App, InteractionList, queryModal, template) { + 'use strict'; + + // Renders context actions for selection packages and files + return Backbone.View.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(); + + this.$el.calculateHeight().height(0); + + App.vent.on('interaction:added', _.bind(this.onAdd, this)); + App.vent.on('interaction:deleted', _.bind(this.onDelete, this)); + + var render = _.bind(this.render, this); + this.listenTo(this.tasks, 'add', render); + this.listenTo(this.tasks, 'remove', render); + + }, + + onAdd: function(task) { + this.tasks.add(task); + }, + + onDelete: function(task) { + this.tasks.remove(task); + }, + + render: function() { + + // only render when it will be visible + if (this.tasks.length > 0) + this.$el.html(this.template(this.tasks.toJSON())); + + if (this.tasks.length > 0 && !this.visible) { + this.$el.slideOut(); + this.visible = true; + } + else if (this.tasks.length === 0 && this.visible) { + this.$el.slideIn(); + this.visible = false; + } + + return this; + }, + + openQuery: function() { + var self = this; + + _.requireOnce(['views/queryModal'], function(ModalView) { + if (self.modal === null) { + self.modal = new ModalView(); + self.modal.parent = self; + } + + self.modal.model = self.tasks.at(0); + self.modal.render(); + self.modal.show(); + }); + + }, + + openNotifications: function() { + + } + }); + });
\ No newline at end of file diff --git a/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..7c6439b49 --- /dev/null +++ b/pyload/web/app/scripts/views/queryModal.js @@ -0,0 +1,69 @@ +define(['jquery', 'underscore', 'app', 'views/abstract/modalView', './input/inputLoader', 'text!tpl/default/queryDialog.html'], + function($, _, App, modalView, load_input, template) { + 'use strict'; + return modalView.extend({ + + // TODO: submit on enter reloads the page sometimes + events: { + 'click .btn-success': 'submit', + 'submit form': 'submit' + }, + template: _.compile(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(); + }, + + 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..e05701b2a --- /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.settingsView.refresh(); + }}); + + }, + + 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..cad5ab075 --- /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 .btn-add': 'choosePlugin', // TODO not in scope + '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.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.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(); + }})); + + } + + }); + });
\ 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/base.less b/pyload/web/app/styles/default/base.less new file mode 100644 index 000000000..bd318127e --- /dev/null +++ b/pyload/web/app/styles/default/base.less @@ -0,0 +1,163 @@ +@import "common"; + +/* + General +*/ + +* { + margin: 0; +} + +html, body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + font-family: 'Abel', sans-serif; + font-size: 16px; + background: url("../../images/default/bgpattern.png") repeat scroll 0 0 transparent; +} + +h1, h2, h3 { + margin: 0; + padding: 0; + font-weight: normal; +} + +a { + text-decoration: none; + color: @blue; +} + +a:hover { + text-decoration: none; + color: @emph; +} + +#wrap { + min-height: 100%; +} + +#content { + padding-bottom: @footer-height; +} + +#content-container:before { + display: block; + content: " "; + height: @header-height; +} + +/* + Additional Responsive Class for larger desktop +*/ + +.visible-large-screen { + display: inherit !important; +} + +@media (max-width: @large-screen) { + .visible-large-screen { + display: none !important; + } +} + +.btn-blue { + background-color: hsl(206, 49%, 35%) !important; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#5493c4), to(#2d5f84)); + background-image: -moz-linear-gradient(top, #5493c4, #2d5f84); + background-image: -ms-linear-gradient(top, #5493c4, #2d5f84); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5493c4), color-stop(100%, #2d5f84)); + background-image: -webkit-linear-gradient(top, #5493c4, #2d5f84); + background-image: -o-linear-gradient(top, #5493c4, #2d5f84); + background-image: linear-gradient(#5493c4, #2d5f84); + border-color: #2d5f84 #2d5f84 hsl(206, 49%, 30%); + color: #fff !important; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.33); + -webkit-font-smoothing: antialiased; + + .caret { + border-bottom-color: #FFFFFF; + border-top-color: #FFFFFF; + } + +} + +.btn-yellow { + background-color: hsl(46, 100%, 57%) !important; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#ffe389), to(#fecb23)); + background-image: -moz-linear-gradient(top, #ffe389, #fecb23); + background-image: -ms-linear-gradient(top, #ffe389, #fecb23); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffe389), color-stop(100%, #fecb23)); + background-image: -webkit-linear-gradient(top, #ffe389, #fecb23); + background-image: -o-linear-gradient(top, #ffe389, #fecb23); + background-image: linear-gradient(#ffe389, #fecb23); + border-color: #fecb23 #fecb23 hsl(46, 100%, 52%); + color: #333 !important; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.33); + -webkit-font-smoothing: antialiased; + + .caret { + border-bottom-color: #FFFFFF; + border-top-color: #FFFFFF; + } +} + +.icon-8x { + font-size: 8em; +} + +// Will not work well in buttons/navs, would require more rules +.icon-larger { + vertical-align: -10%; + font-size: 1.5em; +} + +/* + Modal Overlay +*/ +#modal-overlay { + content: " "; + height: 100%; + width: 100%; + position: absolute; + left: 0; + top: 0; + background: -moz-radial-gradient(center, ellipse cover, rgba(236, 208, 66, 0) 0%, rgba(40, 119, 171, 0.9) 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, rgba(236, 208, 66, 0)), color-stop(100%, rgba(40, 119, 171, 0.9))); + background: -webkit-radial-gradient(center, ellipse cover, rgba(236, 208, 66, 0) 0%, rgba(40, 119, 171, 0.9) 100%); + background: -o-radial-gradient(center, ellipse cover, rgba(236, 208, 66, 0) 0%, rgba(40, 119, 171, 0.9) 100%); + background: -ms-radial-gradient(center, ellipse cover, rgba(236, 208, 66, 0) 0%, rgba(40, 119, 171, 0.9) 100%); + background: radial-gradient(center, ellipse cover, rgba(236, 208, 66, 0) 0%, rgba(40, 119, 171, 0.9) 100%); + z-index: 100; + opacity: 0; +} + +div.modal-header { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + background-color: @blueDark; + color: #FFFFFF; + + .close { + color: @light; + } + +} + +.select2-container { + min-width: 220px; // same as other input fields +} + +div.modal-body { + max-height: 100%; +} + +div.modal { + width: 600px; + +}
\ No newline at end of file diff --git a/pyload/web/app/styles/default/common.less b/pyload/web/app/styles/default/common.less new file mode 100644 index 000000000..338ed38e7 --- /dev/null +++ b/pyload/web/app/styles/default/common.less @@ -0,0 +1,90 @@ +/* + Definitions +*/ + + +/* + Threshold for slightly larger screen +*/ +@large-screen: 1150px; + +@header-height: 70px; +@actionbar-height: 40px; +@footer-height: 66px; + +@light: #ffffff; +@dark: #333333; + +@grey: #757575; +@greyLight: #E5E5E5; +@greyLighter: #F5F5F5; +@greyDark: #444444; +@greyDarker: #111113; + +@yellow: #ffd856; +@yellowLighter: lighten(spin(@yellow, 10), 20%); +@yellowLightest: lighten(spin(@yellow, 15), 30%); +@yellowDark: darken(@yellow, 10%); + +@blue: #3571a2; +@blueLight: lighten(spin(@blue, 5), 10%); +@blueLighter: lighten(spin(@blue, 10), 20%); +@blueLightest: lighten(spin(@blue, 20), 40%); +@blueDark: darken(spin(@blue, -5), 10%); +@blueDarker: darken(spin(@blue, -10), 20%); + +@green: #468847; +@greenLight: lighten(spin(@green, 5), 10%); +@greenDark: darken(spin(@green, -5), 10%); + +@red: #b94a48; +@redLight: lighten(spin(@red, 5), 10%); +@redDark: darken(spin(@red, -5), 10%); + +@emph: #FF7637; + + +/* + Mixins +*/ + +.gradient(@origin: left, @start: #ffffff, @stop: #000000) { + background-color: @start; + background-image: -webkit-linear-gradient(@origin, @start, @stop); + background-image: -moz-linear-gradient(@origin, @start, @stop); + background-image: -o-linear-gradient(@origin, @start, @stop); + background-image: -ms-linear-gradient(@origin, @start, @stop); + background-image: linear-gradient(@origin, @start, @stop); +} + +.transition(@prop: all, @time: 0.25s, @ease: linear) { + -webkit-transition: @prop @time @ease; + -moz-transition: @prop @time @ease; + -o-transition: @prop @time @ease; + -ms-transition: @prop @time @ease; + transition: @prop @time @ease; +} + +.stripes(@color, @color2: rgba(255, 255, 255, 0.15)) { + background-color: @color; + background-image: -webkit-linear-gradient(45deg, @color2 25%, transparent 25%, transparent 50%, @color2 50%, @color2 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, @color2 25%, transparent 25%, transparent 50%, @color2 50%, @color2 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, @color2 25%, transparent 25%, transparent 50%, @color2 50%, @color2 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, @color2 25%, transparent 25%, transparent 50%, @color2 50%, @color2 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} +.stripes-animated { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + + +.default-shadow { + box-shadow: 0 0 5px @dark; +}
\ 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..ab61ba29b --- /dev/null +++ b/pyload/web/app/styles/default/dashboard.less @@ -0,0 +1,331 @@ +@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..15d9943f8 --- /dev/null +++ b/pyload/web/app/styles/default/main.less @@ -0,0 +1,11 @@ +@import "bootstrap/less/bootstrap"; +@import "bootstrap/less/responsive"; +@import "font-awesome/build/assets/font-awesome/less/font-awesome"; + +@FontAwesomePath: "../../fonts"; + +@import "style"; +@import "dashboard"; +@import "settings"; +@import "accounts"; +@import "admin"; 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..6fb5a4857 --- /dev/null +++ b/pyload/web/app/styles/default/style.less @@ -0,0 +1,366 @@ +@import "default/base"; + + +/* + 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; + } + +} + +@header-inner-height: @header-height - 16px; + +// centered header element +.centered { + height: @header-inner-height; + margin: 8px 0; +} + +header:before { + position: absolute; + content: ' '; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: transparent; + z-index: -1; + .default-shadow; +} + +header span.title { + color: white; + float: left; + font-family: SansationRegular, sans-serif; + font-size: 40px; + line-height: @header-height; + cursor: default; +} + +header .logo { + float: left; + margin-right: 10px; + margin-top: 10px; + width: 105px; + height: 107px; + background: url("../../images/default/logo.png") no-repeat; +} + +.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 { // Same gradient as navbar + .gradient(top, #222222, #111111); + color: @grey; + min-height: @footer-height; + margin-top: -@footer-height; + position: relative; + width: 100%; + line-height: 16px; + z-index: 10; +} + +footer:before { + position: absolute; + content: ' '; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: transparent; + box-shadow: 0 0 5px black; + z-index: -1; +} + +footer hr { + margin: 4px 0; + border-top-color: @greyDarker; + border-bottom-color: @greyDark; +} + +footer .span2 { + font-size: 12px; + padding-top: 12px; + padding-bottom: 12px; +} + +// This is the copyright span +footer .offset1 { + padding-top: 8px; + padding-bottom: 0; +} + +footer .copyright { + background: url(../../images/default/logo_grey.png) no-repeat; + background-size: 40px 40px; + background-position: 12px center; + height: 40px; + padding-left: 40px; + padding-top: 10px; + text-align: center; +} + +footer h2 { + color: @light; + font-family: SansationLight, sans-serif; + font-size: 16px; + font-weight: normal; + line-height: 16px; + margin: 0; +}
\ No newline at end of file diff --git a/pyload/web/app/styles/font.css b/pyload/web/app/styles/font.css new file mode 100644 index 000000000..ee117d43b --- /dev/null +++ b/pyload/web/app/styles/font.css @@ -0,0 +1,37 @@ +/** + * @file + * Font styling + */ + +@font-face { + font-family: 'SansationRegular'; + src: url('../fonts/Sansation_Regular-webfont.eot'); + src: url('../fonts/Sansation_Regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/Sansation_Regular-webfont.woff') format('woff'), + url('../fonts/Sansation_Regular-webfont.ttf') format('truetype'), + url('../fonts/Sansation_Regular-webfont.svg#SansationRegular') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'SansationLight'; + src: url('../fonts/Sansation_Light-webfont.eot'); + src: url('../fonts/Sansation_Light-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/Sansation_Light-webfont.woff') format('woff'), + url('../fonts/Sansation_Light-webfont.ttf') format('truetype'), + url('../fonts/Sansation_Light-webfont.svg#SansationLight') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'SansationBold'; + src: url('../fonts/Sansation_Bold-webfont.eot'); + src: url('../fonts/Sansation_Bold-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/Sansation_Bold-webfont.woff') format('woff'), + url('../fonts/Sansation_Bold-webfont.ttf') format('truetype'), + url('../fonts/Sansation_Bold-webfont.svg#SansationBold') format('svg'); + font-weight: normal; + font-style: normal; +}
\ No newline at end of file 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..c2ded16f6 --- /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..815d4593c --- /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"> + <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..6256254da --- /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..c0690a9bf --- /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..73ea391cd --- /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 %><% packs %> package(s)<%/if %> +<%= if files %> +<%= if packs %>, <%/if %> +<% files %> file(s) +<%/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..ae325e83d --- /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..65ae9a880 --- /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..41645c92f --- /dev/null +++ b/pyload/web/app/templates/default/header/progressbar.html @@ -0,0 +1,27 @@ + <%= if single %> + <% name %> (<% 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..3a22bb75b --- /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..0f9b2c9d2 --- /dev/null +++ b/pyload/web/app/templates/default/notification.html @@ -0,0 +1,11 @@ +<%= 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%> +</%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..a9ca6214c --- /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..3ddf16c84 --- /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..ef814414a --- /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 href="#" 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..6d8adb8a7 --- /dev/null +++ b/pyload/web/bower.json @@ -0,0 +1,23 @@ +{ + "name": "pyload", + "version": "0.1.0", + "dependencies": { + "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", + "jed": "~0.5.4", + "select2": "~3.4.0", + "bootstrap": "~2.3.2", + "font-awesome": "~3.1.1" + }, + "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..aaf1c9a8d --- /dev/null +++ b/pyload/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "pyload", + "version": "0.1.0", + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-concat": "~0.1.3", + "grunt-contrib-uglify": "~0.2.0", + "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-bower-requirejs": "~0.4.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", + "connect-livereload": "~0.1.1" + }, + "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..724fddec2 --- /dev/null +++ b/pyload/web/pyload_app.py @@ -0,0 +1,78 @@ +#!/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, exists + +from bottle import route, static_file, response, redirect + +from webinterface import PROJECT_DIR, SETUP, DEVELOP + +from utils import login_required + +########## +# Helper +########## + +app_path = "app" +UNAVAILALBE = False + +# webUI build is available +if exists(join(PROJECT_DIR, "dist", "index.html")) and not DEVELOP: + app_path = "dist" +elif not exists(join(PROJECT_DIR, "app", "components")) or not exists(join(PROJECT_DIR, ".tmp")): + UNAVAILALBE = True + + +@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 + + # TODO: render it as simple template with configuration + return server_static('index.html') + +# 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..b5a933b26 --- /dev/null +++ b/pyload/web/utils.py @@ -0,0 +1,78 @@ +#!/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..37d06bd68 --- /dev/null +++ b/pyload/web/webinterface.py @@ -0,0 +1,98 @@ +#!/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 sys + +from os.path import join, abspath, dirname + +PROJECT_DIR = abspath(dirname(__file__)) +PYLOAD_DIR = abspath(join(PROJECT_DIR, "..", "..")) + +sys.path.append(PYLOAD_DIR) + +from pyload import InitHomeDir + +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') +DEVELOP = config.get('webinterface', 'develop') + +if PREFIX: + PREFIX = PREFIX.rstrip("/") + if PREFIX and not PREFIX.startswith("/"): + PREFIX = "/" + PREFIX + +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) |