summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar RaNaN <Mast3rRaNaN@hotmail.de> 2013-08-18 17:01:17 +0200
committerGravatar RaNaN <Mast3rRaNaN@hotmail.de> 2013-08-18 17:01:17 +0200
commit9a6ea22616cf3cc67e292c908521b79764400faf (patch)
tree1924843f28d992490d867d0557da90dfb1da6404
parentfixed login (diff)
downloadpyload-9a6ea22616cf3cc67e292c908521b79764400faf.tar.xz
new linkgrabber
-rw-r--r--pyload/api/CollectorApi.py37
-rw-r--r--pyload/api/DownloadApi.py11
-rw-r--r--pyload/api/DownloadPreparingApi.py72
-rw-r--r--pyload/api/__init__.py2
-rw-r--r--pyload/database/FileDatabase.py18
-rw-r--r--pyload/interaction/EventManager.py12
-rw-r--r--pyload/plugins/Account.py4
-rw-r--r--pyload/plugins/Crypter.py8
-rw-r--r--pyload/remote/apitypes.py27
-rw-r--r--pyload/remote/apitypes_debug.py17
-rw-r--r--pyload/remote/pyload.thrift29
-rw-r--r--pyload/remote/wsbackend/AsyncHandler.py13
-rw-r--r--pyload/threads/DecrypterThread.py2
-rw-r--r--pyload/threads/InfoThread.py14
-rw-r--r--pyload/threads/ThreadManager.py3
-rw-r--r--pyload/utils/packagetools.py52
-rw-r--r--pyload/web/app/scripts/collections/LinkList.js14
-rw-r--r--pyload/web/app/scripts/config.js6
-rw-r--r--pyload/web/app/scripts/helpers/linkStatus.js18
-rw-r--r--pyload/web/app/scripts/models/CollectorPackage.js77
-rw-r--r--pyload/web/app/scripts/models/LinkStatus.js22
-rw-r--r--pyload/web/app/scripts/views/abstract/modalView.js6
-rw-r--r--pyload/web/app/scripts/views/headerView.js2
-rw-r--r--pyload/web/app/scripts/views/linkGrabberModal.js49
-rw-r--r--pyload/web/app/scripts/views/linkgrabber/collectorView.js21
-rw-r--r--pyload/web/app/scripts/views/linkgrabber/modalView.js61
-rw-r--r--pyload/web/app/scripts/views/linkgrabber/packageView.js58
-rw-r--r--pyload/web/app/styles/default/linkgrabber.less18
-rw-r--r--pyload/web/app/styles/default/main.less1
-rwxr-xr-xpyload/web/app/templates/default/dialogs/linkgrabber.html49
-rwxr-xr-xpyload/web/app/templates/default/linkgrabber/modal.html41
-rw-r--r--pyload/web/app/templates/default/linkgrabber/package.html30
-rw-r--r--pyload/web/app/templates/default/settings/actionbar.html10
-rw-r--r--pyload/web/app/templates/default/settings/layout.html4
-rw-r--r--pyload/web/bower.json2
35 files changed, 487 insertions, 323 deletions
diff --git a/pyload/api/CollectorApi.py b/pyload/api/CollectorApi.py
deleted file mode 100644
index 49340285e..000000000
--- a/pyload/api/CollectorApi.py
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-from pyload.Api import Api, RequirePerm, Permission
-
-from ApiComponent import ApiComponent
-
-class CollectorApi(ApiComponent):
- """ Link collector """
-
- @RequirePerm(Permission.All)
- def getCollector(self):
- pass
-
- @RequirePerm(Permission.Add)
- def addToCollector(self, links):
- pass
-
- @RequirePerm(Permission.Add)
- def addFromCollector(self, name, new_name):
- pass
-
- @RequirePerm(Permission.Delete)
- def deleteCollPack(self, name):
- pass
-
- @RequirePerm(Permission.Add)
- def renameCollPack(self, name, new_name):
- pass
-
- @RequirePerm(Permission.Delete)
- def deleteCollLink(self, url):
- pass
-
-
-if Api.extend(CollectorApi):
- del CollectorApi \ No newline at end of file
diff --git a/pyload/api/DownloadApi.py b/pyload/api/DownloadApi.py
index 0a01007b5..d855dd882 100644
--- a/pyload/api/DownloadApi.py
+++ b/pyload/api/DownloadApi.py
@@ -12,17 +12,6 @@ class DownloadApi(ApiComponent):
""" Component to create, add, delete or modify downloads."""
@RequirePerm(Permission.Add)
- def generateAndAddPackages(self, links, paused=False):
- """Generates and add packages
-
- :param links: list of urls
- :param paused: paused package
- :return: list of package ids
- """
- return [self.addPackageP(name, urls, "", paused) for name, urls
- in self.generatePackages(links).iteritems()]
-
- @RequirePerm(Permission.Add)
def createPackage(self, name, folder, root, password="", site="", comment="", paused=False):
"""Create a new package.
diff --git a/pyload/api/DownloadPreparingApi.py b/pyload/api/DownloadPreparingApi.py
index 0a47fe5ab..d74f0aee2 100644
--- a/pyload/api/DownloadPreparingApi.py
+++ b/pyload/api/DownloadPreparingApi.py
@@ -3,7 +3,9 @@
from itertools import chain
-from pyload.Api import Api, RequirePerm, Permission, OnlineCheck, LinkStatus, urlmatcher
+from pyload.Api import Api, DownloadStatus as DS,\
+ RequirePerm, Permission, OnlineCheck, LinkStatus, urlmatcher
+from pyload.utils import uniqify
from pyload.utils.fs import join
from pyload.utils.packagetools import parseNames
from pyload.network.RequestFactory import getURL
@@ -14,33 +16,13 @@ class DownloadPreparingApi(ApiComponent):
""" All kind of methods to parse links or retrieve online status """
@RequirePerm(Permission.Add)
- def parseURLs(self, html=None, url=None):
- """Parses html content or any arbitrary text for links and returns result of `checkURLs`
-
- :param html: html source
- :return:
- """
- urls = []
-
- if html:
- urls += [x[0] for x in urlmatcher.findall(html)]
-
- if url:
- page = getURL(url)
- urls += [x[0] for x in urlmatcher.findall(page)]
-
- # remove duplicates
- return self.checkURLs(set(urls))
-
-
- @RequirePerm(Permission.Add)
- def checkURLs(self, urls):
+ def parseLinks(self, links):
""" Gets urls and returns pluginname mapped to list of matching urls.
- :param urls:
+ :param links:
:return: {plugin: urls}
"""
- data, crypter = self.core.pluginManager.parseUrls(urls)
+ data, crypter = self.core.pluginManager.parseUrls(links)
plugins = {}
for url, plugin in chain(data, crypter):
@@ -52,31 +34,23 @@ class DownloadPreparingApi(ApiComponent):
return plugins
@RequirePerm(Permission.Add)
- def checkOnlineStatus(self, urls):
+ def checkLinks(self, links):
""" initiates online status check, will also decrypt files.
:param urls:
:return: initial set of data as :class:`OnlineCheck` instance containing the result id
"""
- data, crypter = self.core.pluginManager.parseUrls(urls)
+ hoster, crypter = self.core.pluginManager.parseUrls(links)
# initial result does not contain the crypter links
- tmp = [(url, (url, LinkStatus(url, pluginname, "unknown", 3, 0))) for url, pluginname in data]
+ tmp = [(url, LinkStatus(url, url, pluginname, -1, DS.Queued)) for url, pluginname in hoster + crypter]
data = parseNames(tmp)
- result = {}
-
- for k, v in data.iteritems():
- for url, status in v:
- status.packagename = k
- result[url] = status
-
- data.update(crypter) # hoster and crypter will be processed
- rid = self.core.threadManager.createResultThread(data, False)
+ rid = self.core.threadManager.createResultThread(data)
- return OnlineCheck(rid, result)
+ return OnlineCheck(rid, data)
@RequirePerm(Permission.Add)
- def checkOnlineStatusContainer(self, urls, container, data):
+ def checkContainer(self, filename, data):
""" checks online status of urls and a submitted container file
:param urls: list of urls
@@ -84,11 +58,27 @@ class DownloadPreparingApi(ApiComponent):
:param data: file content
:return: :class:`OnlineCheck`
"""
- th = open(join(self.core.config["general"]["download_folder"], "tmp_" + container), "wb")
+ th = open(join(self.core.config["general"]["download_folder"], "tmp_" + filename), "wb")
th.write(str(data))
th.close()
- urls.append(th.name)
- return self.checkOnlineStatus(urls)
+ return self.checkLinks([th.name])
+
+ @RequirePerm(Permission.Add)
+ def checkHTML(self, html, url):
+ """Parses html content or any arbitrary text for links and returns result of `checkURLs`
+
+ :param html: html source
+ :return:
+ """
+ urls = []
+ if html:
+ urls += [x[0] for x in urlmatcher.findall(html)]
+ if url:
+ page = getURL(url)
+ urls += [x[0] for x in urlmatcher.findall(page)]
+
+ # remove duplicates
+ return self.checkLinks(uniqify(urls))
@RequirePerm(Permission.Add)
def pollResults(self, rid):
diff --git a/pyload/api/__init__.py b/pyload/api/__init__.py
index 1348fd26f..bea46011d 100644
--- a/pyload/api/__init__.py
+++ b/pyload/api/__init__.py
@@ -1,5 +1,5 @@
__all__ = ["CoreApi", "ConfigApi", "DownloadApi", "DownloadPreparingApi", "FileApi",
- "CollectorApi", "UserInteractionApi", "AccountApi", "AddonApi"]
+ "UserInteractionApi", "AccountApi", "AddonApi"]
# Import all components
# from .import *
diff --git a/pyload/database/FileDatabase.py b/pyload/database/FileDatabase.py
index 649c1f1ef..a07486443 100644
--- a/pyload/database/FileDatabase.py
+++ b/pyload/database/FileDatabase.py
@@ -128,24 +128,6 @@ class FileMethods(DatabaseMethods):
self.c.execute('UPDATE files SET fileorder=fileorder-1 WHERE fileorder > ? AND package=? AND owner=?',
(order, package, owner))
- @async
- def saveCollector(self, owner, data):
- """ simply save the json string to database """
- self.c.execute("INSERT INTO collector(owner, data) VALUES (?,?)", (owner, data))
-
- @queue
- def retrieveCollector(self, owner):
- """ retrieve the saved string """
- self.c.execute('SELECT data FROM collector WHERE owner=?', (owner,))
- r = self.c.fetchone()
- if not r: return None
- return r[0]
-
- @async
- def deleteCollector(self, owner):
- """ drop saved user collector """
- self.c.execute('DELETE FROM collector WHERE owner=?', (owner,))
-
@queue
def getAllFiles(self, package=None, search=None, state=None, owner=None):
""" Return dict with file information
diff --git a/pyload/interaction/EventManager.py b/pyload/interaction/EventManager.py
index 329961d93..8cc1e81d2 100644
--- a/pyload/interaction/EventManager.py
+++ b/pyload/interaction/EventManager.py
@@ -55,23 +55,21 @@ class EventManager:
if func in events:
events.remove(func)
- def dispatchEvent(self, event, *args):
+ def dispatchEvent(self, event, *args, **kwargs):
"""dispatches event with args"""
for f in self.events["event"]:
try:
- f(event, *args)
+ f(event, *args, **kwargs)
except Exception, e:
self.log.warning("Error calling event handler %s: %s, %s, %s"
% ("event", f, args, str(e)))
- if self.core.debug:
- print_exc()
+ self.core.print_exc()
if event in self.events:
for f in self.events[event]:
try:
- f(*args)
+ f(*args, **kwargs)
except Exception, e:
self.log.warning("Error calling event handler %s: %s, %s, %s"
% (event, f, args, str(e)))
- if self.core.debug:
- print_exc() \ No newline at end of file
+ self.core.print_exc() \ No newline at end of file
diff --git a/pyload/plugins/Account.py b/pyload/plugins/Account.py
index 26a6124b6..ed0769fb4 100644
--- a/pyload/plugins/Account.py
+++ b/pyload/plugins/Account.py
@@ -95,9 +95,9 @@ class Account(Base):
return self.config_data[option].input.default_value
def setConfig(self, option, value):
- """ Sets a config value for this account instance or global plugin config """
+ """ Sets a config value for this account instance. Modifying the global values is not allowed. """
if option not in self.config_data:
- return Base.setConfig(self, option, value)
+ return
value = from_string(value, self.config_data[option].input.type)
# given value is the default value and does not need to be saved at all
diff --git a/pyload/plugins/Crypter.py b/pyload/plugins/Crypter.py
index d14960308..d780ad805 100644
--- a/pyload/plugins/Crypter.py
+++ b/pyload/plugins/Crypter.py
@@ -2,6 +2,7 @@
from traceback import print_exc
+from pyload.Api import LinkStatus
from pyload.utils import to_list, has_method, uniqify
from pyload.utils.fs import exists, remove, fs_encode
from pyload.utils.packagetools import parseNames
@@ -12,10 +13,9 @@ from Base import Base, Retry
class Package:
""" Container that indicates that a new package should be created """
- def __init__(self, name, urls=None, folder=None):
+ def __init__(self, name=None, urls=None):
self.name = name
self.urls = urls if urls else []
- self.folder = folder
# nested packages
self.packs = []
@@ -23,6 +23,10 @@ class Package:
def addURL(self, url):
self.urls.append(url)
+ def addLink(self, url, name, status, size):
+ # TODO: allow to add urls with known information
+ pass
+
def addPackage(self, pack):
self.packs.append(pack)
diff --git a/pyload/remote/apitypes.py b/pyload/remote/apitypes.py
index 385f4ca07..d186717d4 100644
--- a/pyload/remote/apitypes.py
+++ b/pyload/remote/apitypes.py
@@ -241,15 +241,14 @@ class InvalidConfigSection(ExceptionObject):
self.section = section
class LinkStatus(BaseObject):
- __slots__ = ['url', 'name', 'plugin', 'size', 'status', 'packagename']
+ __slots__ = ['url', 'name', 'plugin', 'size', 'status']
- def __init__(self, url=None, name=None, plugin=None, size=None, status=None, packagename=None):
+ def __init__(self, url=None, name=None, plugin=None, size=None, status=None):
self.url = url
self.name = name
self.plugin = plugin
self.size = size
self.status = status
- self.packagename = packagename
class OnlineCheck(BaseObject):
__slots__ = ['rid', 'data']
@@ -368,8 +367,6 @@ class UserDoesNotExists(ExceptionObject):
self.user = user
class Iface(object):
- def addFromCollector(self, name, paused):
- pass
def addLinks(self, pid, links):
pass
def addLocalFile(self, pid, name, path):
@@ -380,26 +377,20 @@ class Iface(object):
pass
def addPackageP(self, name, links, password, paused):
pass
- def addToCollector(self, links):
- pass
def addUser(self, username, password):
pass
def callAddon(self, plugin, func, arguments):
pass
def callAddonHandler(self, plugin, func, pid_or_fid):
pass
- def checkOnlineStatus(self, urls):
+ def checkContainer(self, filename, data):
pass
- def checkOnlineStatusContainer(self, urls, filename, data):
+ def checkHTML(self, html, url):
pass
- def checkURLs(self, urls):
+ def checkLinks(self, links):
pass
def createPackage(self, name, folder, root, password, site, comment, paused):
pass
- def deleteCollLink(self, url):
- pass
- def deleteCollPack(self, name):
- pass
def deleteConfig(self, plugin):
pass
def deleteFiles(self, fids):
@@ -412,8 +403,6 @@ class Iface(object):
pass
def freeSpace(self):
pass
- def generateAndAddPackages(self, links, paused):
- pass
def generateDownloadLink(self, fid, timeout):
pass
def generatePackages(self, links):
@@ -432,8 +421,6 @@ class Iface(object):
pass
def getAvailablePlugins(self):
pass
- def getCollector(self):
- pass
def getConfig(self):
pass
def getConfigValue(self, section, option):
@@ -484,7 +471,7 @@ class Iface(object):
pass
def orderPackage(self, pids, position):
pass
- def parseURLs(self, html, url):
+ def parseLinks(self, links):
pass
def pauseServer(self):
pass
@@ -498,8 +485,6 @@ class Iface(object):
pass
def removeUser(self, uid):
pass
- def renameCollPack(self, name, new_name):
- pass
def restart(self):
pass
def restartFailed(self):
diff --git a/pyload/remote/apitypes_debug.py b/pyload/remote/apitypes_debug.py
index 177029054..1c34b702e 100644
--- a/pyload/remote/apitypes_debug.py
+++ b/pyload/remote/apitypes_debug.py
@@ -32,7 +32,7 @@ classes = {
'Input' : [int, (None, basestring), (None, basestring)],
'InteractionTask' : [int, int, Input, basestring, basestring, basestring],
'InvalidConfigSection' : [basestring],
- 'LinkStatus' : [basestring, basestring, basestring, int, int, basestring],
+ 'LinkStatus' : [basestring, basestring, basestring, int, int],
'OnlineCheck' : [int, (dict, basestring, LinkStatus)],
'PackageDoesNotExists' : [int],
'PackageInfo' : [int, basestring, basestring, int, int, basestring, basestring, basestring, int, (list, basestring), int, bool, int, PackageStats, (list, int), (list, int)],
@@ -47,29 +47,24 @@ classes = {
}
methods = {
- 'addFromCollector': int,
'addLinks': None,
'addLocalFile': None,
'addPackage': int,
'addPackageChild': int,
'addPackageP': int,
- 'addToCollector': None,
'addUser': UserData,
'callAddon': None,
'callAddonHandler': None,
- 'checkOnlineStatus': OnlineCheck,
- 'checkOnlineStatusContainer': OnlineCheck,
- 'checkURLs': (dict, basestring, list),
+ 'checkContainer': OnlineCheck,
+ 'checkHTML': OnlineCheck,
+ 'checkLinks': OnlineCheck,
'createPackage': int,
- 'deleteCollLink': None,
- 'deleteCollPack': None,
'deleteConfig': None,
'deleteFiles': None,
'deletePackages': None,
'findFiles': TreeCollection,
'findPackages': TreeCollection,
'freeSpace': int,
- 'generateAndAddPackages': (list, int),
'generateDownloadLink': basestring,
'generatePackages': (dict, basestring, list),
'getAccountInfo': AccountInfo,
@@ -79,7 +74,6 @@ methods = {
'getAllFiles': TreeCollection,
'getAllUserData': (dict, int, UserData),
'getAvailablePlugins': (list, ConfigInfo),
- 'getCollector': (list, LinkStatus),
'getConfig': (dict, basestring, ConfigHolder),
'getConfigValue': basestring,
'getCoreConfig': (list, ConfigInfo),
@@ -105,14 +99,13 @@ methods = {
'movePackage': bool,
'orderFiles': None,
'orderPackage': None,
- 'parseURLs': (dict, basestring, list),
+ 'parseLinks': (dict, basestring, list),
'pauseServer': None,
'pollResults': OnlineCheck,
'quit': None,
'recheckPackage': None,
'removeAccount': None,
'removeUser': None,
- 'renameCollPack': None,
'restart': None,
'restartFailed': None,
'restartFile': None,
diff --git a/pyload/remote/pyload.thrift b/pyload/remote/pyload.thrift
index 702bd9b94..d41b6d10b 100644
--- a/pyload/remote/pyload.thrift
+++ b/pyload/remote/pyload.thrift
@@ -193,14 +193,13 @@ struct TreeCollection {
3: map<PackageID, PackageInfo> packages
}
-// general info about link, used for collector and online results
+// general info about link, used for online results
struct LinkStatus {
1: string url,
2: string name,
3: PluginName plugin,
4: ByteCount size, // size <= 0 : unknown
5: DownloadStatus status,
- 6: string packagename,
}
struct ServerStatus {
@@ -299,7 +298,7 @@ struct AccountInfo {
struct OnlineCheck {
1: ResultID rid, // -1 -> nothing more to get
- 2: map<string, LinkStatus> data, // url to result
+ 2: map<string, LinkStatus> data, // package name to result
}
// exceptions
@@ -383,12 +382,12 @@ service Pyload {
// Download Preparing
///////////////////////
- map<PluginName, LinkList> checkURLs(1: LinkList urls),
- map<PluginName, LinkList> parseURLs(1: string html, 2: string url),
+ map<PluginName, LinkList> parseLinks(1: LinkList links),
// parses results and generates packages
- OnlineCheck checkOnlineStatus(1: LinkList urls),
- OnlineCheck checkOnlineStatusContainer(1: LinkList urls, 2: string filename, 3: binary data)
+ OnlineCheck checkLinks(1: LinkList links),
+ OnlineCheck checkContainer(1: string filename, 2: binary data),
+ OnlineCheck checkHTML(1: string html, 2: string url),
// poll results from previously started online check
OnlineCheck pollResults(1: ResultID rid),
@@ -400,8 +399,6 @@ service Pyload {
// Download
///////////////////////
- list<PackageID> generateAndAddPackages(1: LinkList links, 2: bool paused),
-
PackageID createPackage(1: string name, 2: string folder, 3: PackageID root, 4: string password,
5: string site, 6: string comment, 7: bool paused),
@@ -426,22 +423,10 @@ service Pyload {
void restartPackage(1: PackageID pid),
void restartFile(1: FileID fid),
void recheckPackage(1: PackageID pid),
- void restartFailed(),
+ void restartFailed()
void stopDownloads(1: list<FileID> fids),
void stopAllDownloads(),
- ///////////////////////
- // Collector
- ///////////////////////
-
- list<LinkStatus> getCollector(),
-
- void addToCollector(1: LinkList links),
- PackageID addFromCollector(1: string name, 2: bool paused),
- void renameCollPack(1: string name, 2: string new_name),
- void deleteCollPack(1: string name),
- void deleteCollLink(1: string url),
-
////////////////////////////
// File Information retrieval
////////////////////////////
diff --git a/pyload/remote/wsbackend/AsyncHandler.py b/pyload/remote/wsbackend/AsyncHandler.py
index 7eee40707..d53d6bcea 100644
--- a/pyload/remote/wsbackend/AsyncHandler.py
+++ b/pyload/remote/wsbackend/AsyncHandler.py
@@ -75,10 +75,18 @@ class AsyncHandler(AbstractHandler):
pass
@lock
- def add_event(self, event, *args):
+ def add_event(self, event, *args, **kwargs):
# Convert arguments to json suited instance
event = EventInfo(event, [x.toInfoData() if hasattr(x, 'toInfoData') else x for x in args])
+ # use the user kwarg argument to determine access
+ user = None
+ if 'user' in kwargs:
+ user = kwargs['user']
+ del kwargs['user']
+ if hasattr(user, 'uid'):
+ user = user.uid
+
for req in self.clients:
# Not logged in yet
if not req.api: continue
@@ -87,6 +95,9 @@ class AsyncHandler(AbstractHandler):
# TODO: events are security critical, this should be revised later
# TODO: permissions? interaction etc
if not req.api.user.isAdmin():
+ if user is not None and req.api.primaryUID != user:
+ break
+
skip = False
for arg in args:
if hasattr(arg, 'owner') and arg.owner != req.api.primaryUID:
diff --git a/pyload/threads/DecrypterThread.py b/pyload/threads/DecrypterThread.py
index 77502569c..e4df2ee75 100644
--- a/pyload/threads/DecrypterThread.py
+++ b/pyload/threads/DecrypterThread.py
@@ -55,7 +55,7 @@ class DecrypterThread(BaseThread):
plugin.logDebug("Decrypted", plugin_result)
result.extend(plugin_result)
- #TODO
+ #TODO package names are optional
result = uniqify(result)
pack_names = {}
urls = []
diff --git a/pyload/threads/InfoThread.py b/pyload/threads/InfoThread.py
index fba2a9056..6e685cdcf 100644
--- a/pyload/threads/InfoThread.py
+++ b/pyload/threads/InfoThread.py
@@ -4,7 +4,7 @@
from time import time
from traceback import print_exc
-from pyload.Api import LinkStatus
+from pyload.Api import LinkStatus, DownloadStatus
from pyload.utils.packagetools import parseNames
from pyload.utils import has_method, accumulate
@@ -100,17 +100,11 @@ class InfoThread(BaseThread):
if len(self.cache) >= 20 or force:
#used for package generating
- tmp = [(name, (url, LinkStatus(name, plugin, "unknown", status, int(size))))
- for name, size, status, url in self.cache]
+ tmp = [(name, LinkStatus(url, name, plugin, int(size), status))
+ for name, size, status, url in self.cache]
data = parseNames(tmp)
- result = {}
- for k, v in data.iteritems():
- for url, status in v:
- status.packagename = k
- result[url] = status
-
- self.m.setInfoResults(self.rid, result)
+ self.m.setInfoResults(self.rid, data)
self.cache = []
diff --git a/pyload/threads/ThreadManager.py b/pyload/threads/ThreadManager.py
index 086e8ba51..a2e0aa400 100644
--- a/pyload/threads/ThreadManager.py
+++ b/pyload/threads/ThreadManager.py
@@ -106,7 +106,7 @@ class ThreadManager:
def getInfoResult(self, rid):
"""returns result and clears it"""
self.timestamp = time() + 5 * 60
-
+ # TODO: restrict user to his own results
if rid in self.infoResults:
data = self.infoResults[rid]
self.infoResults[rid] = {}
@@ -116,6 +116,7 @@ class ThreadManager:
@lock
def setInfoResults(self, rid, result):
+ self.core.evm.dispatchEvent("onlineResult:updated", rid, result)
self.infoResults[rid].update(result)
def getActiveDownloads(self, user=None):
diff --git a/pyload/utils/packagetools.py b/pyload/utils/packagetools.py
index 791a46d51..02dfa4739 100644
--- a/pyload/utils/packagetools.py
+++ b/pyload/utils/packagetools.py
@@ -5,6 +5,31 @@
import re
from urlparse import urlparse
+endings = "\\.(3gp|7zip|7z|abr|ac3|aiff|aifc|aif|ai|au|avi|bin|bz2|cbr|cbz|ccf|cue|cvd|chm|dta|deb|divx|djvu|dlc|dmg|doc|docx|dot|eps|exe|ff|flv|f4v|gsd|gif|gz|iwd|iso|ipsw|java|jar|jpg|jpeg|jdeatme|load|mws|mw|m4v|m4a|mkv|mp2|mp3|mp4|mov|movie|mpeg|mpe|mpg|msi|msu|msp|nfo|npk|oga|ogg|ogv|otrkey|pkg|png|pdf|pptx|ppt|pps|ppz|pot|psd|qt|rmvb|rm|rar|ram|ra|rev|rnd|r\\d+|rpm|run|rsdf|rtf|sh(!?tml)|srt|snd|sfv|swf|tar|tif|tiff|ts|txt|viv|vivo|vob|wav|wmv|xla|xls|xpi|zeno|zip|z\\d+|_[_a-z]{2}|\\d+$)"
+
+rarPats = [re.compile("(.*)(\\.|_|-)pa?r?t?\\.?[0-9]+.(rar|exe)$", re.I),
+ re.compile("(.*)(\\.|_|-)part\\.?[0]*[1].(rar|exe)$", re.I),
+ re.compile("(.*)\\.rar$", re.I),
+ re.compile("(.*)\\.r\\d+$", re.I),
+ re.compile("(.*)(\\.|_|-)\\d+$", re.I)]
+
+zipPats = [re.compile("(.*)\\.zip$", re.I),
+ re.compile("(.*)\\.z\\d+$", re.I),
+ re.compile("(?is).*\\.7z\\.[\\d]+$", re.I),
+ re.compile("(.*)\\.a.$", re.I)]
+
+ffsjPats = [re.compile("(.*)\\._((_[a-z])|([a-z]{2}))(\\.|$)"),
+ re.compile("(.*)(\\.|_|-)[\\d]+(" + endings + "$)", re.I)]
+
+iszPats = [re.compile("(.*)\\.isz$", re.I),
+ re.compile("(.*)\\.i\\d{2}$", re.I)]
+
+pat1 = re.compile("(\\.?CD\\d+)", re.I)
+pat2 = re.compile("(\\.?part\\d+)", re.I)
+
+pat3 = re.compile("(.+)[\\.\\-_]+$")
+pat4 = re.compile("(.+)\\.\\d+\\.xtm$")
+
def matchFirst(string, *args):
""" matches against list of regexp and returns first match"""
for patternlist in args:
@@ -25,31 +50,6 @@ def parseNames(files):
"""
packs = {}
- endings = "\\.(3gp|7zip|7z|abr|ac3|aiff|aifc|aif|ai|au|avi|bin|bz2|cbr|cbz|ccf|cue|cvd|chm|dta|deb|divx|djvu|dlc|dmg|doc|docx|dot|eps|exe|ff|flv|f4v|gsd|gif|gz|iwd|iso|ipsw|java|jar|jpg|jpeg|jdeatme|load|mws|mw|m4v|m4a|mkv|mp2|mp3|mp4|mov|movie|mpeg|mpe|mpg|msi|msu|msp|nfo|npk|oga|ogg|ogv|otrkey|pkg|png|pdf|pptx|ppt|pps|ppz|pot|psd|qt|rmvb|rm|rar|ram|ra|rev|rnd|r\\d+|rpm|run|rsdf|rtf|sh(!?tml)|srt|snd|sfv|swf|tar|tif|tiff|ts|txt|viv|vivo|vob|wav|wmv|xla|xls|xpi|zeno|zip|z\\d+|_[_a-z]{2}|\\d+$)"
-
- rarPats = [re.compile("(.*)(\\.|_|-)pa?r?t?\\.?[0-9]+.(rar|exe)$", re.I),
- re.compile("(.*)(\\.|_|-)part\\.?[0]*[1].(rar|exe)$", re.I),
- re.compile("(.*)\\.rar$", re.I),
- re.compile("(.*)\\.r\\d+$", re.I),
- re.compile("(.*)(\\.|_|-)\\d+$", re.I)]
-
- zipPats = [re.compile("(.*)\\.zip$", re.I),
- re.compile("(.*)\\.z\\d+$", re.I),
- re.compile("(?is).*\\.7z\\.[\\d]+$", re.I),
- re.compile("(.*)\\.a.$", re.I)]
-
- ffsjPats = [re.compile("(.*)\\._((_[a-z])|([a-z]{2}))(\\.|$)"),
- re.compile("(.*)(\\.|_|-)[\\d]+(" + endings + "$)", re.I)]
-
- iszPats = [re.compile("(.*)\\.isz$", re.I),
- re.compile("(.*)\\.i\\d{2}$", re.I)]
-
- pat1 = re.compile("(\\.?CD\\d+)", re.I)
- pat2 = re.compile("(\\.?part\\d+)", re.I)
-
- pat3 = re.compile("(.+)[\\.\\-_]+$")
- pat4 = re.compile("(.+)\\.\\d+\\.xtm$")
-
for file, url in files:
patternMatch = False
@@ -127,7 +127,7 @@ def parseNames(files):
# fallback : default name
if not name:
- name = "unknown"
+ name = _("Unnamed package")
# build mapping
if name in packs:
diff --git a/pyload/web/app/scripts/collections/LinkList.js b/pyload/web/app/scripts/collections/LinkList.js
new file mode 100644
index 000000000..170a2c039
--- /dev/null
+++ b/pyload/web/app/scripts/collections/LinkList.js
@@ -0,0 +1,14 @@
+define(['jquery', 'backbone', 'underscore', 'models/LinkStatus'], function($, Backbone, _, LinkStatus) {
+ 'use strict';
+
+ return Backbone.Collection.extend({
+
+ model: LinkStatus,
+
+ comparator: function(link) {
+ return link.get('name');
+ }
+
+ });
+
+}); \ No newline at end of file
diff --git a/pyload/web/app/scripts/config.js b/pyload/web/app/scripts/config.js
index ff4082ce4..51ea63285 100644
--- a/pyload/web/app/scripts/config.js
+++ b/pyload/web/app/scripts/config.js
@@ -59,8 +59,10 @@ require.config({
deps: ['underscore', 'jquery'],
exports: 'Backbone'
},
-
- marionette: ['backbone'],
+ marionette: {
+ deps: ['backbone'],
+ exports: 'Backbone'
+ },
handlebars: {
exports: 'Handlebars'
},
diff --git a/pyload/web/app/scripts/helpers/linkStatus.js b/pyload/web/app/scripts/helpers/linkStatus.js
new file mode 100644
index 000000000..2497785fb
--- /dev/null
+++ b/pyload/web/app/scripts/helpers/linkStatus.js
@@ -0,0 +1,18 @@
+define('helpers/linkStatus', ['underscore', 'handlebars', 'utils/apitypes', 'utils/i18n'],
+ function(_, Handlebars, Api, i18n) {
+ 'use strict';
+ function linkStatus(status) {
+ var s;
+ if (status === Api.DownloadStatus.Online)
+ s = '<span class="text-success">' + i18n.gettext('online') + '</span>';
+ else if (status === Api.DownloadState.Offline)
+ s = '<span class="text-error">' + i18n.gettext('offline') + '</span>';
+ else
+ s = '<span class="text-info">' + i18n.gettext('unknown') + '</span>';
+
+ return new Handlebars.SafeString(s);
+ }
+
+ Handlebars.registerHelper('linkStatus', linkStatus);
+ return linkStatus;
+ });
diff --git a/pyload/web/app/scripts/models/CollectorPackage.js b/pyload/web/app/scripts/models/CollectorPackage.js
new file mode 100644
index 000000000..293342440
--- /dev/null
+++ b/pyload/web/app/scripts/models/CollectorPackage.js
@@ -0,0 +1,77 @@
+define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes', 'collections/LinkList'],
+ function($, Backbone, _, App, Api, LinkList) {
+ 'use strict';
+ return Backbone.Model.extend({
+
+ idAttribute: 'name',
+ defaults: {
+ name: 'Unnamed package',
+ new_name: null,
+ links: null
+ },
+
+ initialize: function() {
+ this.set('links', new LinkList());
+ },
+
+ destroy: function() {
+ // Copied from backbones destroy method
+ var model = this;
+ model.trigger('destroy', model, model.collection);
+ },
+
+ // get the actual name
+ getName: function() {
+ var new_name = this.get('new_name');
+ if (new_name)
+ return new_name;
+
+ return this.get('name');
+
+ },
+ // Add the package to pyload
+ add: function() {
+ var self = this;
+ var links = this.get('links').pluck('url');
+
+ $.ajax(App.apiRequest('addPackage',
+ {name: this.getName(),
+ links: links},
+ {success: function() {
+ self.destroy();
+ App.vent.trigger('package:added');
+ }}));
+
+ },
+
+ updateLinks: function(links) {
+ this.get('links').set(links, {remove: false});
+ this.trigger('change');
+ },
+
+ toJSON: function() {
+ var data = {
+ name: this.getName(),
+ links: this.get('links').toJSON()
+ };
+ var links = this.get('links');
+ data.length = links.length;
+ data.online = 0;
+ data.offline = 0;
+ data.unknown = 0;
+
+ // Summary
+ links.each(function(link) {
+ if (link.get('status') === Api.DownloadStatus.Online)
+ data.online++;
+ else if (link.get('status') === Api.DownloadStatus.Offline)
+ data.offline++;
+ else
+ data.unknown++;
+ });
+
+ return data;
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/pyload/web/app/scripts/models/LinkStatus.js b/pyload/web/app/scripts/models/LinkStatus.js
new file mode 100644
index 000000000..2be1ce368
--- /dev/null
+++ b/pyload/web/app/scripts/models/LinkStatus.js
@@ -0,0 +1,22 @@
+define(['jquery', 'backbone', 'underscore', 'app', 'utils/apitypes'],
+ function($, Backbone, _, App, Api) {
+ 'use strict';
+
+ return Backbone.Model.extend({
+
+ idAttribute: 'url',
+
+ defaults: {
+ name: '',
+ plugin: '',
+ size: -1,
+ status: Api.DownloadStatus.Queued
+ },
+
+ destroy: function() {
+ var model = this;
+ model.trigger('destroy', model, model.collection);
+ }
+
+ });
+ }); \ 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
index b6d9f0eab..61016a9fb 100644
--- a/pyload/web/app/scripts/views/abstract/modalView.js
+++ b/pyload/web/app/scripts/views/abstract/modalView.js
@@ -117,10 +117,14 @@ define(['jquery', 'backbone', 'underscore', 'omniwindow'], function($, Backbone,
},
destroy: function() {
+ this.onDestroy();
this.$el.remove();
this.dialog = null;
this.remove();
- }
+ },
+ onDestroy: function() {
+
+ }
});
}); \ No newline at end of file
diff --git a/pyload/web/app/scripts/views/headerView.js b/pyload/web/app/scripts/views/headerView.js
index 3fdfe32ba..60a47ad62 100644
--- a/pyload/web/app/scripts/views/headerView.js
+++ b/pyload/web/app/scripts/views/headerView.js
@@ -172,7 +172,7 @@ define(['jquery', 'underscore', 'backbone', 'app', 'models/ServerStatus', 'colle
open_grabber: function() {
var self = this;
- _.requireOnce(['views/linkGrabberModal'], function(ModalView) {
+ _.requireOnce(['views/linkgrabber/modalView'], function(ModalView) {
if (self.grabber === null)
self.grabber = new ModalView();
diff --git a/pyload/web/app/scripts/views/linkGrabberModal.js b/pyload/web/app/scripts/views/linkGrabberModal.js
deleted file mode 100644
index e6f59c134..000000000
--- a/pyload/web/app/scripts/views/linkGrabberModal.js
+++ /dev/null
@@ -1,49 +0,0 @@
-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/linkgrabber/collectorView.js b/pyload/web/app/scripts/views/linkgrabber/collectorView.js
new file mode 100644
index 000000000..d2b43f699
--- /dev/null
+++ b/pyload/web/app/scripts/views/linkgrabber/collectorView.js
@@ -0,0 +1,21 @@
+define(['jquery', 'underscore', 'backbone', 'app', './packageView'],
+ function($, _, Backbone, App, packageView) {
+ 'use strict';
+ return Backbone.Marionette.CollectionView.extend({
+ itemView: packageView,
+ updateData: function(result) {
+ var self = this;
+ _.each(result.data, function(links, name) {
+ var pack = self.collection.get(name);
+ if (!pack) {
+ pack = new self.collection.model({name: name});
+ self.collection.add(pack);
+ }
+
+ // TODO: remove links from all other packages than pack
+ pack.updateLinks(links);
+ });
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/pyload/web/app/scripts/views/linkgrabber/modalView.js b/pyload/web/app/scripts/views/linkgrabber/modalView.js
new file mode 100644
index 000000000..6e4781ac2
--- /dev/null
+++ b/pyload/web/app/scripts/views/linkgrabber/modalView.js
@@ -0,0 +1,61 @@
+define(['jquery', 'underscore', 'backbone', 'app', 'models/CollectorPackage', 'views/abstract/modalView', './collectorView', 'hbs!tpl/linkgrabber/modal'],
+ function($, _, Backbone, App, CollectorPackage, modalView, CollectorView, template) {
+ 'use strict';
+ // Modal dialog for package adding - triggers package:added when package was added
+ return modalView.extend({
+
+ events: {
+ 'keypress #inputLinks': 'addOnEnter'
+ },
+
+ template: template,
+
+ // Holds the view that display the packages
+ collectorView: null,
+
+ 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;
+ // split, trim and remove empty links
+ var links = _.filter(_.map(this.$('#inputLinks').val().split('\n'), function(link) {
+ return $.trim(link);
+ }), function(link) {
+ return link.length > 0;
+ });
+
+ var options = App.apiRequest('checkLinks',
+ {links: links},
+ {
+ success: function(data) {
+ self.collectorView.updateData(data);
+ }
+ });
+
+ $.ajax(options);
+ this.$('#inputLinks').val('');
+ },
+
+ onRender: function() {
+ // anonymous collection
+ this.collectorView = new CollectorView({collection: new (Backbone.Collection.extend({
+ model: CollectorPackage
+ }))()});
+ this.collectorView.setElement(this.$('.prepared-packages'));
+ },
+
+ onDestroy: function() {
+ if (this.collectorView)
+ this.collectorView.close();
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/pyload/web/app/scripts/views/linkgrabber/packageView.js b/pyload/web/app/scripts/views/linkgrabber/packageView.js
new file mode 100644
index 000000000..97d00f722
--- /dev/null
+++ b/pyload/web/app/scripts/views/linkgrabber/packageView.js
@@ -0,0 +1,58 @@
+define(['jquery', 'underscore', 'backbone', 'app', 'hbs!tpl/linkgrabber/package'],
+ function($, _, Backbone, App, template) {
+ 'use strict';
+ return Backbone.Marionette.ItemView.extend({
+
+ tagName: 'div',
+ className: 'row-fluid package',
+ template: template,
+
+ modelEvents: {
+ change: 'render'
+ },
+
+ ui: {
+ 'table': 'table'
+ },
+
+ events: {
+ 'click .btn-expand': 'expand',
+ 'click .btn-add': 'addPackage',
+ 'click .btn-delete': 'deletePackage',
+ 'click .btn-mini': 'deleteLink'
+ },
+
+ expanded: false,
+
+ serializeData: function() {
+ var data = this.model.toJSON();
+ data.expanded = this.expanded;
+ return data;
+ },
+
+ addPackage: function() {
+ this.model.add();
+ },
+
+ deletePackage: function() {
+ this.model.destroy();
+ },
+
+ deleteLink: function(e) {
+ var el = $(e.target);
+ var id = parseInt(el.data('index'), 10);
+
+ var model = this.model.get('links').at(id);
+ if (model)
+ model.destroy();
+
+ this.render();
+ },
+
+ expand: function() {
+ this.expanded ^= true;
+ this.ui.table.toggle();
+ }
+
+ });
+ }); \ No newline at end of file
diff --git a/pyload/web/app/styles/default/linkgrabber.less b/pyload/web/app/styles/default/linkgrabber.less
new file mode 100644
index 000000000..364c9c420
--- /dev/null
+++ b/pyload/web/app/styles/default/linkgrabber.less
@@ -0,0 +1,18 @@
+.prepared-packages {
+ hr {
+ margin: 0;
+ }
+
+ .package > .btn {
+ margin-bottom: 3px;
+ }
+
+ img {
+ height: 22px;
+ }
+
+ .table {
+ margin-bottom: 0;
+ }
+
+} \ No newline at end of file
diff --git a/pyload/web/app/styles/default/main.less b/pyload/web/app/styles/default/main.less
index 0bfa4fe2f..6bf21e80b 100644
--- a/pyload/web/app/styles/default/main.less
+++ b/pyload/web/app/styles/default/main.less
@@ -8,6 +8,7 @@
@import "pyload-common/styles/basic-layout";
@import "style";
+@import "linkgrabber";
@import "dashboard";
@import "settings";
@import "accounts";
diff --git a/pyload/web/app/templates/default/dialogs/linkgrabber.html b/pyload/web/app/templates/default/dialogs/linkgrabber.html
deleted file mode 100755
index 8d90131ac..000000000
--- a/pyload/web/app/templates/default/dialogs/linkgrabber.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</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&hellip;</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/linkgrabber/modal.html b/pyload/web/app/templates/default/linkgrabber/modal.html
new file mode 100755
index 000000000..67e2e4fe3
--- /dev/null
+++ b/pyload/web/app/templates/default/linkgrabber/modal.html
@@ -0,0 +1,41 @@
+<div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+ <h3>
+ {{_ "Add links" }}
+ <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="inputLinks">{{ _ "Links" }}</label>
+
+ <div class="controls">
+ <textarea id="inputLinks" class="span4" rows="2" placeholder="{{_ " Paste your links here..."}}"></textarea>
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label" for="inputContainer">{{_ "Upload container file" }}</label>
+ <div class="controls controls-row">
+ <input type="text" id="inputContainer" class="span3" readonly>
+ <button id="inputContainer-btn" class="btn span1" type="button">{{_ "Browse" }}&hellip;</button>
+ </div>
+ </div>
+
+ <legend>
+ {{_ "Packages" }}
+ </legend>
+ <div class="container-fluid prepared-packages">
+
+ </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/linkgrabber/package.html b/pyload/web/app/templates/default/linkgrabber/package.html
new file mode 100644
index 000000000..54f6f4ad1
--- /dev/null
+++ b/pyload/web/app/templates/default/linkgrabber/package.html
@@ -0,0 +1,30 @@
+<strong>{{ name }}</strong> - <button class="btn btn-small btn-blue btn-expand"><i class="icon-arrow-down"></i> </button> <button class="btn btn-small btn-success btn-add"><i class="icon-plus"></i> </button> <button class="btn btn-small btn-danger btn-delete"><i class="icon-trash"></i> </button> <br>
+<table class="table table-condensed" {{#unless expanded}}style="display: none"{{/unless}}>
+ <tbody>
+ {{#each links}}
+ <tr>
+ <td>{{ name }}</td>
+ <td><img src="{{ pluginIcon plugin }}"> {{ plugin }}</td>
+ <td>{{ linkStatus status }}</td>
+ <td><button class="btn btn-danger btn-mini" data-index={{@index}}><i class="icon-trash"></i></button></td>
+ </tr>
+ {{/each}}
+ </tbody>
+</table>
+<hr>
+{{ ngettext "%d link" "%d links" length }}:
+{{#if online}}
+<span class="text-success">
+ {{ online }} {{_ "online" }}
+</span>
+{{/if}}
+{{#if offline}}
+<span class="text-error">
+ {{ offline }} {{_ "offline" }}
+</span>
+{{/if}}
+{{#if unknown}}
+<span class="text-info">
+ {{ unknown }} {{_ "unknown" }}
+</span>
+{{/if}}
diff --git a/pyload/web/app/templates/default/settings/actionbar.html b/pyload/web/app/templates/default/settings/actionbar.html
index 25b10d463..647d0af99 100644
--- a/pyload/web/app/templates/default/settings/actionbar.html
+++ b/pyload/web/app/templates/default/settings/actionbar.html
@@ -1,5 +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
+<ul class="actionbar nav span8 offset3">
+ <li>
+ <button class="btn btn-small btn-blue btn-add">Add Plugin</button>
+ </li>
+</ul> \ 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
index 39f1a2ec9..143d0caad 100644
--- a/pyload/web/app/templates/default/settings/layout.html
+++ b/pyload/web/app/templates/default/settings/layout.html
@@ -1,8 +1,8 @@
-<div class="span2">
+<div class="span3">
<ul class="nav nav-list well settings-menu">
</ul>
</div>
-<div class="span10">
+<div class="span9">
<div class="well setting-box">
<form class="form-horizontal" action="#">
<h1>Please choose a config section</h1>
diff --git a/pyload/web/bower.json b/pyload/web/bower.json
index 1d12378a0..b0176a891 100644
--- a/pyload/web/bower.json
+++ b/pyload/web/bower.json
@@ -13,7 +13,7 @@
"flot": "~0.8.1",
"underscore": "~1.4.4",
"backbone": "~1.0.0",
- "backbone.marionette": "~1.0.3",
+ "backbone.marionette": "~1.1.0",
"handlebars.js": "1.0.0-rc.3",
"jed": "~0.5.4",
"select2": "~3.4.0",