diff --git a/ChangeLog b/ChangeLog index 6391b0803..a41e13a52 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ === Deluge 1.3.0 (In Development) === * Improved Logging - * Enforced the use of the "deluge.plugins" namespace to reduce package names clashing beetween regular packages and deluge plugins. + * Removed the AutoAdd feature on the core. It's now handled with the AutoAdd + plugin, which is also shipped with Deluge, and it does a better job and + now, it even supports multiple users perfectly. + * Authentication/Permission exceptions are now sent to clients and recreated + there to allow acting upon them. + * Enforced the use of the "deluge.plugins" namespace to reduce package + names clashing beetween regular packages and deluge plugins. ==== Core ==== * Implement #1063 option to delete torrent file copy on torrent removal - patch from Ghent @@ -10,11 +16,29 @@ * #1112: Fix renaming files in add torrent dialog * #1247: Fix deluge-gtk from hanging on shutdown * #995: Rewrote tracker_icons - * Make the distinction between adding to the session new unmanaged torrents and torrents loaded from state. This will break backwards compatability. - * Pass a copy of an event instead of passing the event arguments to the event handlers. This will break backwards compatability. + * Make the distinction between adding to the session new unmanaged torrents + and torrents loaded from state. This will break backwards compatability. + * Pass a copy of an event instead of passing the event arguments to the + event handlers. This will break backwards compatability. + * Allow changing ownership of torrents. + * File modifications on the auth file are now detected and when they happen, + the file is reloaded. Upon finding an old auth file with an old format, an + upgrade to the new format is made, file saved, and reloaded. + * Authentication no longer requires a username/password. If one or both of + these is missing, an authentication error will be sent to the client + which sould then ask the username/password to the user. + * Implemented sequential downloads. + * #378: Provide information about a torrent's pieces states ==== GtkUI ==== * Fix uncaught exception when closing deluge in classic mode + * Allow changing ownership of torrents + * Host entries in the Connection Manager UI are now editable. They're + now also migrated from the old format were automatic localhost logins were + possible, which no longer is, this fixes #1814. + * Implemented sequential downloads UI handling. + * #378: Allow showing a pieces bar instead of a regular progress bar in a + torrent's status tab. ==== WebUI ==== * Migrate to ExtJS 3.1 diff --git a/README b/README index 0befe28f9..3ad036bb2 100644 --- a/README +++ b/README @@ -15,9 +15,9 @@ For past developers and contributers see: http://dev.deluge-torrent.org/wiki/Abo License ========================== Deluge is under the GNU GPLv3 license. -Icon data/pixmaps/deluge.svg and derivatives in data/icons are copyright +Icon ui/data/pixmaps/deluge.svg and derivatives in ui/data/icons are copyright Andrew Wedderburn and are under the GNU GPLv3. -All other icons in data/pixmaps are copyright Andrew Resch and are under +All other icons in ui/data/pixmaps are copyright Andrew Resch and are under the GNU GPLv3. ========================== diff --git a/create_potfiles_in.py b/create_potfiles_in.py old mode 100644 new mode 100755 index 6ccb39e1d..e500a0a06 --- a/create_potfiles_in.py +++ b/create_potfiles_in.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python import os # Paths to exclude diff --git a/createicons.sh b/createicons.sh index 114a822c5..9303e5adb 100755 --- a/createicons.sh +++ b/createicons.sh @@ -1,6 +1,6 @@ #!/bin/bash -for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/data/\ +for size in 16 22 24 32 36 48 64 72 96 128 192 256; do mkdir -p deluge/ui/data/\ icons/hicolor/${size}x${size}/apps; rsvg-convert -w ${size} -h ${size} \ --o deluge/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/data/pixmaps\ -/deluge.svg; mkdir -p deluge/data/icons/scalable/apps/; cp deluge/data/pixmaps/\ -deluge.svg deluge/data/icons/scalable/apps/deluge.svg; done +-o deluge/ui/data/icons/hicolor/${size}x${size}/apps/deluge.png deluge/ui/data/pixmaps\ +/deluge.svg; mkdir -p deluge/ui/data/icons/scalable/apps/; cp deluge/ui/data/pixmaps/\ +deluge.svg deluge/ui/data/icons/scalable/apps/deluge.svg; done diff --git a/deluge/common.py b/deluge/common.py index aba2904f9..8e8926803 100644 --- a/deluge/common.py +++ b/deluge/common.py @@ -40,7 +40,6 @@ import os import time import subprocess import platform -import sys import chardet import logging @@ -167,6 +166,18 @@ def get_default_download_dir(): if windows_check(): return os.path.expanduser("~") else: + from xdg.BaseDirectory import xdg_config_home + userdir_file = os.path.join(xdg_config_home, 'user-dirs.dirs') + try: + for line in open(userdir_file, 'r'): + if not line.startswith('#') and 'XDG_DOWNLOAD_DIR' in line: + download_dir = os.path.expandvars(\ + line.partition("=")[2].rstrip().strip('"')) + if os.path.isdir(download_dir): + return download_dir + except IOError: + pass + return os.environ.get("HOME") def windows_check(): @@ -201,7 +212,7 @@ def osx_check(): def get_pixmap(fname): """ - Provides easy access to files in the deluge/data/pixmaps folder within the Deluge egg + Provides easy access to files in the deluge/ui/data/pixmaps folder within the Deluge egg :param fname: the filename to look for :type fname: string @@ -209,7 +220,7 @@ def get_pixmap(fname): :rtype: string """ - return pkg_resources.resource_filename("deluge", os.path.join("data", \ + return pkg_resources.resource_filename("deluge", os.path.join("ui/data", \ "pixmaps", fname)) def open_file(path): @@ -593,7 +604,7 @@ def utf8_encoded(s): """ if isinstance(s, str): - s = decode_string(s, locale.getpreferredencoding()) + s = decode_string(s) elif isinstance(s, unicode): s = s.encode("utf8", "ignore") return s @@ -632,3 +643,43 @@ class VersionSplit(object): v1 = [self.version, self.suffix or 'z', self.dev] v2 = [ver.version, ver.suffix or 'z', ver.dev] return cmp(v1, v2) + + +# Common AUTH stuff +AUTH_LEVEL_NONE = 0 +AUTH_LEVEL_READONLY = 1 +AUTH_LEVEL_NORMAL = 5 +AUTH_LEVEL_ADMIN = 10 +AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL + +def create_auth_file(): + import stat, configmanager + auth_file = configmanager.get_config_dir("auth") + # Check for auth file and create if necessary + if not os.path.exists(auth_file): + fd = open(auth_file, "w") + fd.flush() + os.fsync(fd.fileno()) + fd.close() + # Change the permissions on the file so only this user can read/write it + os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE) + +def create_localclient_account(append=False): + import configmanager, random + auth_file = configmanager.get_config_dir("auth") + if not os.path.exists(auth_file): + create_auth_file() + + try: + from hashlib import sha1 as sha_hash + except ImportError: + from sha import new as sha_hash + fd = open(auth_file, "a" if append else "w") + fd.write(":".join([ + "localclient", + sha_hash(str(random.random())).hexdigest(), + str(AUTH_LEVEL_ADMIN) + ]) + '\n') + fd.flush() + os.fsync(fd.fileno()) + fd.close() diff --git a/deluge/component.py b/deluge/component.py index 4a6f39622..9fa4e3c67 100644 --- a/deluge/component.py +++ b/deluge/component.py @@ -98,6 +98,9 @@ class Component(object): self._component_stopping_deferred = None _ComponentRegistry.register(self) + def __del__(self): + _ComponentRegistry.deregister(self._component_name) + def _component_start_timer(self): if hasattr(self, "update"): self._component_timer = LoopingCall(self.update) @@ -141,11 +144,18 @@ class Component(object): self._component_timer.stop() return True + def on_stop_fail(result): + self._component_state = "Started" + self._component_stopping_deferred = None + log.error(result) + return result + if self._component_state != "Stopped" and self._component_state != "Stopping": if hasattr(self, "stop"): self._component_state = "Stopping" d = maybeDeferred(self.stop) d.addCallback(on_stop) + d.addErrback(on_stop_fail) self._component_stopping_deferred = d else: d = maybeDeferred(on_stop, None) diff --git a/deluge/config.py b/deluge/config.py index 5dbb68197..482415c95 100644 --- a/deluge/config.py +++ b/deluge/config.py @@ -268,6 +268,31 @@ what is currently in the config and it could not convert the value else: return self.__config[key] + def __delitem__(self, key): + """ + See + :meth:`del_item` + """ + self.del_item(key) + + def del_item(self, key): + """ + Deletes item with a specific key from the configuration. + + :param key: the item which you wish to delete. + :raises KeyError: if 'key' is not in the config dictionary + + **Usage** + >>> config = Config("test.conf", defaults={"test": 5}) + >>> del config["test"] + """ + del self.__config[key] + # We set the save_timer for 5 seconds if not already set + from twisted.internet import reactor + if not self._save_timer or not self._save_timer.active(): + self._save_timer = reactor.callLater(5, self.save) + + def register_change_callback(self, callback): """ Registers a callback function that will be called when a value is changed in the config dictionary diff --git a/deluge/core/authmanager.py b/deluge/core/authmanager.py index 106351b43..771043e8b 100644 --- a/deluge/core/authmanager.py +++ b/deluge/core/authmanager.py @@ -2,6 +2,7 @@ # authmanager.py # # Copyright (C) 2009 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -36,28 +37,56 @@ import os import random import stat +import shutil import logging import deluge.component as component import deluge.configmanager as configmanager -import deluge.error +from deluge.common import (AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE, AUTH_LEVEL_NORMAL, + AUTH_LEVEL_READONLY, AUTH_LEVEL_DEFAULT, + create_localclient_account) + +from deluge.error import AuthManagerError, AuthenticationRequired, BadLoginError log = logging.getLogger(__name__) -AUTH_LEVEL_NONE = 0 -AUTH_LEVEL_READONLY = 1 -AUTH_LEVEL_NORMAL = 5 -AUTH_LEVEL_ADMIN = 10 +AUTH_LEVELS_MAPPING = { + 'NONE': AUTH_LEVEL_NONE, + 'READONLY': AUTH_LEVEL_READONLY, + 'DEFAULT': AUTH_LEVEL_NORMAL, + 'NORMAL': AUTH_LEVEL_DEFAULT, + 'ADMIN': AUTH_LEVEL_ADMIN +} -AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL +AUTH_LEVELS_MAPPING_REVERSE = {} +for key, value in AUTH_LEVELS_MAPPING.iteritems(): + AUTH_LEVELS_MAPPING_REVERSE[value] = key + +class Account(object): + __slots__ = ('username', 'password', 'authlevel') + def __init__(self, username, password, authlevel): + self.username = username + self.password = password + self.authlevel = authlevel + + def data(self): + return { + 'username': self.username, + 'password': self.password, + 'authlevel': AUTH_LEVELS_MAPPING_REVERSE[self.authlevel], + 'authlevel_int': self.authlevel + } + + def __repr__(self): + return ('' % + self.__dict__) -class BadLoginError(deluge.error.DelugeError): - pass class AuthManager(component.Component): def __init__(self): - component.Component.__init__(self, "AuthManager") + component.Component.__init__(self, "AuthManager", interval=10) self.__auth = {} + self.__auth_modification_time = None def start(self): self.__load_auth_file() @@ -68,6 +97,19 @@ class AuthManager(component.Component): def shutdown(self): pass + def update(self): + auth_file = configmanager.get_config_dir("auth") + # Check for auth file and create if necessary + if not os.path.exists(auth_file): + log.info("Authfile not found, recreating it.") + self.__load_auth_file() + return + + auth_file_modification_time = os.stat(auth_file).st_mtime + if self.__auth_modification_time != auth_file_modification_time: + log.info("Auth file changed, reloading it!") + self.__load_auth_file() + def authorize(self, username, password): """ Authorizes users based on username and password @@ -77,49 +119,121 @@ class AuthManager(component.Component): :returns: int, the auth level for this user :rtype: int - :raises BadLoginError: if the username does not exist or password does not match + :raises AuthenticationRequired: if aditional details are required to + authenticate. + :raises BadLoginError: if the username does not exist or password does + not match. """ + if not username: + raise AuthenticationRequired( + "Username and Password are required.", username + ) if username not in self.__auth: # Let's try to re-load the file.. Maybe it's been updated self.__load_auth_file() if username not in self.__auth: - raise BadLoginError("Username does not exist") + raise BadLoginError("Username does not exist", username) - if self.__auth[username][0] == password: + if self.__auth[username].password == password: # Return the users auth level - return int(self.__auth[username][1]) + return self.__auth[username].authlevel + elif not password and self.__auth[username].password: + raise AuthenticationRequired("Password is required", username) else: - raise BadLoginError("Password does not match") + raise BadLoginError("Password does not match", username) - def __create_localclient_account(self): + def has_account(self, username): + return username in self.__auth + + def get_known_accounts(self): """ - Returns the string. + Returns a list of known deluge usernames. """ - # We create a 'localclient' account with a random password + self.__load_auth_file() + return [account.data() for account in self.__auth.values()] + + def create_account(self, username, password, authlevel): + if username in self.__auth: + raise AuthManagerError("Username in use.", username) try: - from hashlib import sha1 as sha_hash - except ImportError: - from sha import new as sha_hash - return "localclient:" + sha_hash(str(random.random())).hexdigest() + ":" + str(AUTH_LEVEL_ADMIN) + "\n" + self.__auth[username] = Account(username, password, + AUTH_LEVELS_MAPPING[authlevel]) + self.write_auth_file() + return True + except Exception, err: + log.exception(err) + raise err - def __load_auth_file(self): - auth_file = configmanager.get_config_dir("auth") - # Check for auth file and create if necessary - if not os.path.exists(auth_file): - localclient = self.__create_localclient_account() - fd = open(auth_file, "w") - fd.write(localclient) + def update_account(self, username, password, authlevel): + if username not in self.__auth: + raise AuthManagerError("Username not known", username) + try: + self.__auth[username].username = username + self.__auth[username].password = password + self.__auth[username].authlevel = AUTH_LEVELS_MAPPING[authlevel] + self.write_auth_file() + return True + except Exception, err: + log.exception(err) + raise err + + def remove_account(self, username): + if username not in self.__auth: + raise AuthManagerError("Username not known", username) + elif username == component.get("RPCServer").get_session_user(): + raise AuthManagerError( + "You cannot delete your own account while logged in!", username + ) + + del self.__auth[username] + self.write_auth_file() + return True + + def write_auth_file(self): + old_auth_file = configmanager.get_config_dir("auth") + new_auth_file = old_auth_file + '.new' + bak_auth_file = old_auth_file + '.bak' + # Let's first create a backup + if os.path.exists(old_auth_file): + shutil.copy2(old_auth_file, bak_auth_file) + + try: + fd = open(new_auth_file, "w") + for account in self.__auth.values(): + fd.write( + "%(username)s:%(password)s:%(authlevel_int)s\n" % + account.data() + ) fd.flush() os.fsync(fd.fileno()) fd.close() - # Change the permissions on the file so only this user can read/write it - os.chmod(auth_file, stat.S_IREAD | stat.S_IWRITE) - f = [localclient] - else: - # Load the auth file into a dictionary: {username: password, ...} - f = open(auth_file, "r").readlines() + os.rename(new_auth_file, old_auth_file) + except: + # Something failed, let's restore the previous file + if os.path.exists(bak_auth_file): + os.rename(bak_auth_file, old_auth_file) + + self.__load_auth_file() + + def __load_auth_file(self): + save_and_reload = False + auth_file = configmanager.get_config_dir("auth") + # Check for auth file and create if necessary + if not os.path.exists(auth_file): + create_localclient_account() + return self.__load_auth_file() + + auth_file_modification_time = os.stat(auth_file).st_mtime + if self.__auth_modification_time is None: + self.__auth_modification_time = auth_file_modification_time + elif self.__auth_modification_time == auth_file_modification_time: + # File didn't change, no need for re-parsing's + return + + # Load the auth file into a dictionary: {username: Account(...)} + f = open(auth_file, "r").readlines() for line in f: if line.startswith("#"): @@ -133,15 +247,43 @@ class AuthManager(component.Component): continue if len(lsplit) == 2: username, password = lsplit - log.warning("Your auth entry for %s contains no auth level, using AUTH_LEVEL_DEFAULT(%s)..", username, AUTH_LEVEL_DEFAULT) - level = AUTH_LEVEL_DEFAULT + log.warning("Your auth entry for %s contains no auth level, " + "using AUTH_LEVEL_DEFAULT(%s)..", username, + AUTH_LEVEL_DEFAULT) + if username == 'localclient': + authlevel = AUTH_LEVEL_ADMIN + else: + authlevel = AUTH_LEVEL_DEFAULT + # This is probably an old auth file + save_and_reload = True elif len(lsplit) == 3: - username, password, level = lsplit + username, password, authlevel = lsplit else: - log.error("Your auth file is malformed: Incorrect number of fields!") + log.error("Your auth file is malformed: " + "Incorrect number of fields!") continue - self.__auth[username.strip()] = (password.strip(), level) + username = username.strip() + password = password.strip() + try: + authlevel = int(authlevel) + except ValueError: + try: + authlevel = AUTH_LEVELS_MAPPING[authlevel] + except KeyError: + log.error("Your auth file is malformed: %r is not a valid auth " + "level" % authlevel) + continue + + self.__auth[username] = Account(username, password, authlevel) if "localclient" not in self.__auth: - open(auth_file, "a").write(self.__create_localclient_account()) + create_localclient_account(True) + return self.__load_auth_file() + + + if save_and_reload: + log.info("Re-writing auth file (upgrade)") + self.write_auth_file() + self.__auth_modification_time = auth_file_modification_time + diff --git a/deluge/core/autoadd.py b/deluge/core/autoadd.py deleted file mode 100644 index 911b560ce..000000000 --- a/deluge/core/autoadd.py +++ /dev/null @@ -1,137 +0,0 @@ -# -# autoadd.py -# -# Copyright (C) 2008 Andrew Resch -# -# Deluge is free software. -# -# You may 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. -# -# deluge 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 deluge. If not, write to: -# The Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor -# Boston, MA 02110-1301, USA. -# -# In addition, as a special exception, the copyright holders give -# permission to link the code of portions of this program with the OpenSSL -# library. -# You must obey the GNU General Public License in all respects for all of -# the code used other than OpenSSL. If you modify file(s) with this -# exception, you may extend this exception to your version of the file(s), -# but you are not obligated to do so. If you do not wish to do so, delete -# this exception statement from your version. If you delete this exception -# statement from all source files in the program, then also delete it here. -# -# - - -import os -import logging - -from deluge._libtorrent import lt - -import deluge.component as component -from deluge.configmanager import ConfigManager - -MAX_NUM_ATTEMPTS = 10 - -log = logging.getLogger(__name__) - -class AutoAdd(component.Component): - def __init__(self): - component.Component.__init__(self, "AutoAdd", depend=["TorrentManager"], interval=5) - # Get the core config - self.config = ConfigManager("core.conf") - - # A list of filenames - self.invalid_torrents = [] - # Filename:Attempts - self.attempts = {} - - # Register set functions - self.config.register_set_function("autoadd_enable", - self._on_autoadd_enable, apply_now=True) - self.config.register_set_function("autoadd_location", - self._on_autoadd_location) - - def update(self): - if not self.config["autoadd_enable"]: - # We shouldn't be updating because autoadd is not enabled - component.pause("AutoAdd") - return - - # Check the auto add folder for new torrents to add - if not os.path.isdir(self.config["autoadd_location"]): - log.warning("Invalid AutoAdd folder: %s", self.config["autoadd_location"]) - component.pause("AutoAdd") - return - - for filename in os.listdir(self.config["autoadd_location"]): - if filename.split(".")[-1] == "torrent": - try: - filepath = os.path.join(self.config["autoadd_location"], filename) - except UnicodeDecodeError, e: - log.error("Unable to auto add torrent due to inproper filename encoding: %s", e) - continue - try: - filedump = self.load_torrent(filepath) - except (RuntimeError, Exception), e: - # If the torrent is invalid, we keep track of it so that we - # can try again on the next pass. This is because some - # torrents may not be fully saved during the pass. - log.debug("Torrent is invalid: %s", e) - if filename in self.invalid_torrents: - self.attempts[filename] += 1 - if self.attempts[filename] >= MAX_NUM_ATTEMPTS: - os.rename(filepath, filepath + ".invalid") - del self.attempts[filename] - self.invalid_torrents.remove(filename) - else: - self.invalid_torrents.append(filename) - self.attempts[filename] = 1 - continue - - # The torrent looks good, so lets add it to the session - component.get("TorrentManager").add(filedump=filedump, filename=filename) - - os.remove(filepath) - - def load_torrent(self, filename): - try: - log.debug("Attempting to open %s for add.", filename) - _file = open(filename, "rb") - filedump = _file.read() - if not filedump: - raise RuntimeError, "Torrent is 0 bytes!" - _file.close() - except IOError, e: - log.warning("Unable to open %s: %s", filename, e) - raise e - - # Get the info to see if any exceptions are raised - info = lt.torrent_info(lt.bdecode(filedump)) - - return filedump - - def _on_autoadd_enable(self, key, value): - log.debug("_on_autoadd_enable") - if value: - component.resume("AutoAdd") - else: - component.pause("AutoAdd") - - def _on_autoadd_location(self, key, value): - log.debug("_on_autoadd_location") - # We need to resume the component just incase it was paused due to - # an invalid autoadd location. - if self.config["autoadd_enable"]: - component.resume("AutoAdd") diff --git a/deluge/core/core.py b/deluge/core/core.py index 14eb00833..1cd3e3cc3 100644 --- a/deluge/core/core.py +++ b/deluge/core/core.py @@ -2,6 +2,7 @@ # core.py # # Copyright (C) 2007-2009 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -38,17 +39,13 @@ from deluge._libtorrent import lt import os import glob import base64 -import shutil import logging import threading -import pkg_resources -import warnings import tempfile +from urlparse import urljoin - -from twisted.internet import reactor, defer -from twisted.internet.task import LoopingCall import twisted.web.client +import twisted.web.error from deluge.httpdownloader import download_file @@ -57,12 +54,13 @@ import deluge.common import deluge.component as component from deluge.event import * from deluge.error import * +from deluge.core.authmanager import AUTH_LEVEL_ADMIN, AUTH_LEVEL_NONE +from deluge.core.authmanager import AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE from deluge.core.torrentmanager import TorrentManager from deluge.core.pluginmanager import PluginManager from deluge.core.alertmanager import AlertManager from deluge.core.filtermanager import FilterManager from deluge.core.preferencesmanager import PreferencesManager -from deluge.core.autoadd import AutoAdd from deluge.core.authmanager import AuthManager from deluge.core.eventmanager import EventManager from deluge.core.rpcserver import export @@ -78,7 +76,8 @@ class Core(component.Component): log.info("Starting libtorrent %s session..", lt.version) # Create the client fingerprint - version = [int(value.split("-")[0]) for value in deluge.common.get_version().split(".")] + version = [int(value.split("-")[0]) for value in + deluge.common.get_version().split(".")] while len(version) < 4: version.append(0) @@ -89,10 +88,17 @@ class Core(component.Component): # Set the user agent self.settings = lt.session_settings() - self.settings.user_agent = "Deluge %s" % deluge.common.get_version() + self.settings.user_agent = "Deluge/%(deluge_version)s Libtorrent/%(lt_version)s" % \ + { 'deluge_version': deluge.common.get_version(), + 'lt_version': self.get_libtorrent_version().rpartition(".")[0] } # Set session settings self.settings.send_redundant_have = True + if deluge.common.windows_check(): + self.settings.disk_io_write_mode = \ + lt.io_buffer_mode_t.disable_os_cache_for_aligned_files + self.settings.disk_io_read_mode = \ + lt.io_buffer_mode_t.disable_os_cache_for_aligned_files self.session.set_settings(self.settings) # Load metadata extension @@ -107,7 +113,6 @@ class Core(component.Component): self.pluginmanager = PluginManager(self) self.torrentmanager = TorrentManager() self.filtermanager = FilterManager(self) - self.autoadd = AutoAdd() self.authmanager = AuthManager() # New release check information @@ -115,6 +120,8 @@ class Core(component.Component): # Get the core config self.config = deluge.configmanager.ConfigManager("core.conf") + self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2) + self.config.save() # If there was an interface value from the command line, use it, but # store the one in the config so we can restore it on shutdown @@ -148,19 +155,25 @@ class Core(component.Component): def __save_session_state(self): """Saves the libtorrent session state""" try: - open(deluge.configmanager.get_config_dir("session.state"), "wb").write( - lt.bencode(self.session.state())) + session_state = deluge.configmanager.get_config_dir("session.state") + open(session_state, "wb").write(lt.bencode(self.session.state())) except Exception, e: log.warning("Failed to save lt state: %s", e) def __load_session_state(self): """Loads the libtorrent session state""" try: - self.session.load_state(lt.bdecode( - open(deluge.configmanager.get_config_dir("session.state"), "rb").read())) + session_state = deluge.configmanager.get_config_dir("session.state") + self.session.load_state(lt.bdecode(open(session_state, "rb").read())) except Exception, e: log.warning("Failed to load lt state: %s", e) + + def __migrate_config_1_to_2(self, config): + if 'sequential_download' not in config: + config['sequential_download'] = False + return config + def save_dht_state(self): """Saves the dht state to a file""" try: @@ -213,7 +226,9 @@ class Core(component.Component): log.exception(e) try: - torrent_id = self.torrentmanager.add(filedump=filedump, options=options, filename=filename) + torrent_id = self.torrentmanager.add( + filedump=filedump, options=options, filename=filename + ) except Exception, e: log.error("There was an error adding the torrent file %s", filename) log.exception(e) @@ -237,7 +252,7 @@ class Core(component.Component): :returns: a Deferred which returns the torrent_id as a str or None """ log.info("Attempting to add url %s", url) - def on_get_file(filename): + def on_download_success(filename): # We got the file, so add it to the session f = open(filename, "rb") data = f.read() @@ -246,17 +261,35 @@ class Core(component.Component): os.remove(filename) except Exception, e: log.warning("Couldn't remove temp file: %s", e) - return self.add_torrent_file(filename, base64.encodestring(data), options) + return self.add_torrent_file( + filename, base64.encodestring(data), options + ) - def on_get_file_error(failure): - # Log the error and pass the failure onto the client - log.error("Error occured downloading torrent from %s", url) - log.error("Reason: %s", failure.getErrorMessage()) - return failure + def on_download_fail(failure): + if failure.check(twisted.web.error.PageRedirect): + new_url = urljoin(url, failure.getErrorMessage().split(" to ")[1]) + result = download_file( + new_url, tempfile.mkstemp()[1], headers=headers, + force_filename=True + ) + result.addCallbacks(on_download_success, on_download_fail) + elif failure.check(twisted.web.client.PartialDownloadError): + result = download_file( + url, tempfile.mkstemp()[1], headers=headers, + force_filename=True, allow_compression=False + ) + result.addCallbacks(on_download_success, on_download_fail) + else: + # Log the error and pass the failure onto the client + log.error("Error occured downloading torrent from %s", url) + log.error("Reason: %s", failure.getErrorMessage()) + result = failure + return result - d = download_file(url, tempfile.mkstemp()[1], headers=headers) - d.addCallback(on_get_file) - d.addErrback(on_get_file_error) + d = download_file( + url, tempfile.mkstemp()[1], headers=headers, force_filename=True + ) + d.addCallbacks(on_download_success, on_download_fail) return d @export @@ -394,7 +427,11 @@ class Core(component.Component): @export def get_torrent_status(self, torrent_id, keys, diff=False): # Build the status dictionary - status = self.torrentmanager[torrent_id].get_status(keys, diff) + try: + status = self.torrentmanager[torrent_id].get_status(keys, diff) + except KeyError: + # Torrent was probaly removed meanwhile + return {} # Get the leftover fields and ask the plugin manager to fill them leftover_fields = list(set(keys) - set(status.keys())) @@ -542,6 +579,11 @@ class Core(component.Component): """Sets a higher priority to the first and last pieces""" return self.torrentmanager[torrent_id].set_prioritize_first_last(value) + @export + def set_torrent_sequential_download(self, torrent_id, value): + """Toggle sequencial pieces download""" + return self.torrentmanager[torrent_id].set_sequential_download(value) + @export def set_torrent_auto_managed(self, torrent_id, value): """Sets the auto managed flag for queueing purposes""" @@ -572,6 +614,32 @@ class Core(component.Component): """Sets the path for the torrent to be moved when completed""" return self.torrentmanager[torrent_id].set_move_completed_path(value) + @export(AUTH_LEVEL_ADMIN) + def set_torrents_owner(self, torrent_ids, username): + """Set's the torrent owner. + + :param torrent_id: the torrent_id of the torrent to remove + :type torrent_id: string + :param username: the new owner username + :type username: string + + :raises DelugeError: if the username is not known + """ + if not self.authmanager.has_account(username): + raise DelugeError("Username \"%s\" is not known." % username) + if isinstance(torrent_ids, basestring): + torrent_ids = [torrent_ids] + for torrent_id in torrent_ids: + self.torrentmanager[torrent_id].set_owner(username) + return None + + @export + def set_torrents_shared(self, torrent_ids, shared): + if isinstance(torrent_ids, basestring): + torrent_ids = [torrent_ids] + for torrent_id in torrent_ids: + self.torrentmanager[torrent_id].set_options({"shared": shared}) + @export def get_path_size(self, path): """Returns the size of the file or folder 'path' and -1 if the path is @@ -754,7 +822,11 @@ class Core(component.Component): def on_get_page(result): return bool(int(result)) + def logError(failure): + log.warning("Error testing listen port: %s", failure) + d.addCallback(on_get_page) + d.addErrback(logError) return d @@ -790,3 +862,23 @@ class Core(component.Component): """ return lt.version + + @export(AUTH_LEVEL_ADMIN) + def get_known_accounts(self): + return self.authmanager.get_known_accounts() + + @export(AUTH_LEVEL_NONE) + def get_auth_levels_mappings(self): + return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE) + + @export(AUTH_LEVEL_ADMIN) + def create_account(self, username, password, authlevel): + return self.authmanager.create_account(username, password, authlevel) + + @export(AUTH_LEVEL_ADMIN) + def update_account(self, username, password, authlevel): + return self.authmanager.update_account(username, password, authlevel) + + @export(AUTH_LEVEL_ADMIN) + def remove_account(self, username): + return self.authmanager.remove_account(username) diff --git a/deluge/core/daemon.py b/deluge/core/daemon.py index 5cad58af0..0343febe5 100644 --- a/deluge/core/daemon.py +++ b/deluge/core/daemon.py @@ -54,7 +54,9 @@ class Daemon(object): if os.path.isfile(deluge.configmanager.get_config_dir("deluged.pid")): # Get the PID and the port of the supposedly running daemon try: - (pid, port) = open(deluge.configmanager.get_config_dir("deluged.pid")).read().strip().split(";") + (pid, port) = open( + deluge.configmanager.get_config_dir("deluged.pid") + ).read().strip().split(";") pid = int(pid) port = int(port) except ValueError: @@ -93,7 +95,10 @@ class Daemon(object): else: # This is a deluged! s.close() - raise deluge.error.DaemonRunningError("There is a deluge daemon running with this config directory!") + raise deluge.error.DaemonRunningError( + "There is a deluge daemon running with this config " + "directory!" + ) # Initialize gettext try: @@ -191,15 +196,6 @@ class Daemon(object): except twisted.internet.error.ReactorNotRunning: log.debug("Tried to stop the reactor but it is not running..") - @export() - def info(self): - """ - Returns some info from the daemon. - - :returns: str, the version number - """ - return deluge.common.get_version() - @export() def get_method_list(self): """ diff --git a/deluge/core/eventmanager.py b/deluge/core/eventmanager.py index 1199813d8..16cfa42ee 100644 --- a/deluge/core/eventmanager.py +++ b/deluge/core/eventmanager.py @@ -55,7 +55,10 @@ class EventManager(component.Component): if event.name in self.handlers: for handler in self.handlers[event.name]: #log.debug("Running handler %s for event %s with args: %s", event.name, handler, event.args) - handler(event.copy()) + try: + handler(*event.args) + except: + log.error("Event handler %s failed in %s", event.name, handler) def register_event_handler(self, event, handler): """ diff --git a/deluge/core/filtermanager.py b/deluge/core/filtermanager.py index 359eb45a5..a4fd54d8e 100644 --- a/deluge/core/filtermanager.py +++ b/deluge/core/filtermanager.py @@ -78,6 +78,27 @@ def filter_one_keyword(torrent_ids, keyword): yield torrent_id break +def filter_by_name(torrent_ids, search_string): + all_torrents = component.get("TorrentManager").torrents + try: + search_string, match_case = search_string[0].split('::match') + except ValueError: + search_string = search_string[0] + match_case = False + + if match_case is False: + search_string = search_string.lower() + + for torrent_id in torrent_ids: + torrent_name = all_torrents[torrent_id].get_name() + if match_case is False: + torrent_name = all_torrents[torrent_id].get_name().lower() + else: + torrent_name = all_torrents[torrent_id].get_name() + + if search_string in torrent_name: + yield torrent_id + def tracker_error_filter(torrent_ids, values): filtered_torrent_ids = [] tm = component.get("TorrentManager") @@ -108,6 +129,7 @@ class FilterManager(component.Component): self.torrents = core.torrentmanager self.registered_filters = {} self.register_filter("keyword", filter_keywords) + self.register_filter("name", filter_by_name) self.tree_fields = {} self.register_tree_field("state", self._init_state_tree) @@ -136,7 +158,7 @@ class FilterManager(component.Component): if "id"in filter_dict: #optimized filter for id: - torrent_ids = filter_dict["id"] + torrent_ids = list(filter_dict["id"]) del filter_dict["id"] else: torrent_ids = self.torrents.get_torrent_list() diff --git a/deluge/core/pluginmanager.py b/deluge/core/pluginmanager.py index 119928979..5ef5ef55f 100644 --- a/deluge/core/pluginmanager.py +++ b/deluge/core/pluginmanager.py @@ -37,8 +37,6 @@ """PluginManager for Core""" import logging -from twisted.internet import reactor -from twisted.internet.task import LoopingCall from deluge.event import PluginEnabledEvent, PluginDisabledEvent import deluge.pluginmanagerbase diff --git a/deluge/core/preferencesmanager.py b/deluge/core/preferencesmanager.py index a79659866..d42be335c 100644 --- a/deluge/core/preferencesmanager.py +++ b/deluge/core/preferencesmanager.py @@ -38,7 +38,6 @@ import os import logging import threading import pkg_resources -from twisted.internet import reactor from twisted.internet.task import LoopingCall from deluge._libtorrent import lt @@ -64,6 +63,7 @@ DEFAULT_PREFS = { "torrentfiles_location": deluge.common.get_default_download_dir(), "plugins_location": os.path.join(deluge.configmanager.get_config_dir(), "plugins"), "prioritize_first_last_pieces": False, + "sequential_download": False, "random_port": True, "dht": True, "upnp": True, @@ -87,8 +87,6 @@ DEFAULT_PREFS = { "max_upload_speed_per_torrent": -1, "max_download_speed_per_torrent": -1, "enabled_plugins": [], - "autoadd_location": deluge.common.get_default_download_dir(), - "autoadd_enable": False, "add_paused": False, "max_active_seeding": 5, "max_active_downloading": 3, @@ -143,7 +141,7 @@ DEFAULT_PREFS = { "geoip_db_location": "/usr/share/GeoIP/GeoIP.dat", "cache_size": 512, "cache_expiry": 60, - "public": False + "shared": False } class PreferencesManager(component.Component): @@ -151,6 +149,11 @@ class PreferencesManager(component.Component): component.Component.__init__(self, "PreferencesManager") self.config = deluge.configmanager.ConfigManager("core.conf", DEFAULT_PREFS) + if 'public' in self.config: + log.debug("Updating configuration file: Renamed torrent's public " + "attribute to shared.") + self.config["shared"] = self.config["public"] + del self.config["public"] def start(self): self.core = component.get("Core") @@ -193,7 +196,9 @@ class PreferencesManager(component.Component): # Only set the listen ports if random_port is not true if self.config["random_port"] is not True: log.debug("listen port range set to %s-%s", value[0], value[1]) - self.session.listen_on(value[0], value[1], str(self.config["listen_interface"])) + self.session.listen_on( + value[0], value[1], str(self.config["listen_interface"]) + ) def _on_set_listen_interface(self, key, value): # Call the random_port callback since it'll do what we need @@ -215,7 +220,10 @@ class PreferencesManager(component.Component): # Set the listen ports log.debug("listen port range set to %s-%s", listen_ports[0], listen_ports[1]) - self.session.listen_on(listen_ports[0], listen_ports[1], str(self.config["listen_interface"])) + self.session.listen_on( + listen_ports[0], listen_ports[1], + str(self.config["listen_interface"]) + ) def _on_set_outgoing_ports(self, key, value): if not self.config["random_outgoing_ports"]: @@ -442,8 +450,12 @@ class PreferencesManager(component.Component): geoip_db = "" if os.path.exists(value): geoip_db = value - elif os.path.exists(pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat"))): - geoip_db = pkg_resources.resource_filename("deluge", os.path.join("data", "GeoIP.dat")) + elif os.path.exists( + pkg_resources.resource_filename("deluge", + os.path.join("data", "GeoIP.dat"))): + geoip_db = pkg_resources.resource_filename( + "deluge", os.path.join("data", "GeoIP.dat") + ) else: log.warning("Unable to find GeoIP database file!") diff --git a/deluge/core/rpcserver.py b/deluge/core/rpcserver.py index 2858b3b30..d176ff01b 100644 --- a/deluge/core/rpcserver.py +++ b/deluge/core/rpcserver.py @@ -43,7 +43,7 @@ import logging import traceback from twisted.internet.protocol import Factory, Protocol -from twisted.internet import ssl, reactor, defer +from twisted.internet import reactor, defer from OpenSSL import crypto, SSL from types import FunctionType @@ -55,7 +55,10 @@ except ImportError: import deluge.component as component import deluge.configmanager -from deluge.core.authmanager import AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT +from deluge.core.authmanager import (AUTH_LEVEL_NONE, AUTH_LEVEL_DEFAULT, + AUTH_LEVEL_ADMIN) +from deluge.error import (DelugeError, NotAuthorizedError, + _ClientSideRecreateError, IncompatibleClient) RPC_RESPONSE = 1 RPC_ERROR = 2 @@ -117,12 +120,6 @@ def format_request(call): else: return s -class DelugeError(Exception): - pass - -class NotAuthorizedError(DelugeError): - pass - class ServerContextFactory(object): def getContext(self): """ @@ -180,7 +177,8 @@ class DelugeRPCProtocol(Protocol): for call in request: if len(call) != 4: - log.debug("Received invalid rpc request: number of items in request is %s", len(call)) + log.debug("Received invalid rpc request: number of items " + "in request is %s", len(call)) continue #log.debug("RPCRequest: %s", format_request(call)) reactor.callLater(0, self.dispatch, *call) @@ -201,7 +199,8 @@ class DelugeRPCProtocol(Protocol): This method is called when a new client connects. """ peer = self.transport.getPeer() - log.info("Deluge Client connection made from: %s:%s", peer.host, peer.port) + log.info("Deluge Client connection made from: %s:%s", + peer.host, peer.port) # Set the initial auth level of this session to AUTH_LEVEL_NONE self.factory.authorized_sessions[self.transport.sessionno] = AUTH_LEVEL_NONE @@ -223,6 +222,9 @@ class DelugeRPCProtocol(Protocol): log.info("Deluge client disconnected: %s", reason.value) + def valid_session(self): + return self.transport.sessionno in self.factory.authorized_sessions + def dispatch(self, request_id, method, args, kwargs): """ This method is run when a RPC Request is made. It will run the local method @@ -244,33 +246,49 @@ class DelugeRPCProtocol(Protocol): Sends an error response with the contents of the exception that was raised. """ exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() + try: + self.sendData(( + RPC_ERROR, + request_id, + exceptionType.__name__, + exceptionValue._args, + exceptionValue._kwargs, + "".join(traceback.format_tb(exceptionTraceback)) + )) + except Exception, err: + log.error("An exception occurred while sending RPC_ERROR to " + "client. Error to send(exception goes next): %s", + "".join(traceback.format_tb(exceptionTraceback))) + log.exception(err) - self.sendData(( - RPC_ERROR, - request_id, - (exceptionType.__name__, - exceptionValue.args[0] if len(exceptionValue.args) == 1 else "", - "".join(traceback.format_tb(exceptionTraceback))) - )) - - if method == "daemon.login": + if method == "daemon.info": + # This is a special case and used in the initial connection process + self.sendData((RPC_RESPONSE, request_id, deluge.common.get_version())) + return + elif method == "daemon.login": # This is a special case and used in the initial connection process # We need to authenticate the user here + log.debug("RPC dispatch daemon.login") try: + client_version = kwargs.pop('client_version', None) + if client_version is None: + raise IncompatibleClient(deluge.common.get_version()) ret = component.get("AuthManager").authorize(*args, **kwargs) if ret: self.factory.authorized_sessions[self.transport.sessionno] = (ret, args[0]) self.factory.session_protocols[self.transport.sessionno] = self except Exception, e: sendError() - log.exception(e) + if not isinstance(e, _ClientSideRecreateError): + log.exception(e) else: self.sendData((RPC_RESPONSE, request_id, (ret))) if not ret: self.transport.loseConnection() finally: return - elif method == "daemon.set_event_interest" and self.transport.sessionno in self.factory.authorized_sessions: + elif method == "daemon.set_event_interest" and self.valid_session(): + log.debug("RPC dispatch daemon.set_event_interest") # This special case is to allow clients to set which events they are # interested in receiving. # We are expecting a sequence from the client. @@ -285,21 +303,24 @@ class DelugeRPCProtocol(Protocol): finally: return - if method in self.factory.methods and self.transport.sessionno in self.factory.authorized_sessions: + if method in self.factory.methods and self.valid_session(): + log.debug("RPC dispatch %s", method) try: method_auth_requirement = self.factory.methods[method]._rpcserver_auth_level auth_level = self.factory.authorized_sessions[self.transport.sessionno][0] if auth_level < method_auth_requirement: # This session is not allowed to call this method - log.debug("Session %s is trying to call a method it is not authorized to call!", self.transport.sessionno) - raise NotAuthorizedError("Auth level too low: %s < %s" % (auth_level, method_auth_requirement)) + log.debug("Session %s is trying to call a method it is not " + "authorized to call!", self.transport.sessionno) + raise NotAuthorizedError(auth_level, method_auth_requirement) # Set the session_id in the factory so that methods can know # which session is calling it. self.factory.session_id = self.transport.sessionno ret = self.factory.methods[method](*args, **kwargs) except Exception, e: sendError() - # Don't bother printing out DelugeErrors, because they are just for the client + # Don't bother printing out DelugeErrors, because they are just + # for the client if not isinstance(e, DelugeError): log.exception("Exception calling RPC request: %s", e) else: @@ -353,6 +374,7 @@ class RPCServer(component.Component): # Holds the interested event list for the sessions self.factory.interested_events = {} + self.listen = listen if not listen: return @@ -437,6 +459,8 @@ class RPCServer(component.Component): :rtype: string """ + if not self.listen: + return "localclient" session_id = self.get_session_id() if session_id > -1 and session_id in self.factory.authorized_sessions: return self.factory.authorized_sessions[session_id][1] @@ -451,6 +475,8 @@ class RPCServer(component.Component): :returns: the auth level :rtype: int """ + if not self.listen: + return AUTH_LEVEL_ADMIN return self.factory.authorized_sessions[self.get_session_id()][0] def get_rpc_auth_level(self, rpc): @@ -486,8 +512,7 @@ class RPCServer(component.Component): # Find sessions interested in this event for session_id, interest in self.factory.interested_events.iteritems(): if event.name in interest: - log.debug("Emit Event: %s %s", event.name, zip(event.__slots__, - event.args)) + log.debug("Emit Event: %s %s", event.name, event.args) # This session is interested so send a RPC_EVENT self.factory.session_protocols[session_id].sendData( (RPC_EVENT, event.name, event.args) @@ -536,8 +561,12 @@ def generate_ssl_keys(): # Write out files ssl_dir = deluge.configmanager.get_config_dir("ssl") - open(os.path.join(ssl_dir, "daemon.pkey"), "w").write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) - open(os.path.join(ssl_dir, "daemon.cert"), "w").write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + open(os.path.join(ssl_dir, "daemon.pkey"), "w").write( + crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) + ) + open(os.path.join(ssl_dir, "daemon.cert"), "w").write( + crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + ) # Make the files only readable by this user for f in ("daemon.pkey", "daemon.cert"): os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE) diff --git a/deluge/core/torrent.py b/deluge/core/torrent.py index 7651b6e5b..3c0d4ce24 100644 --- a/deluge/core/torrent.py +++ b/deluge/core/torrent.py @@ -55,22 +55,23 @@ class TorrentOptions(dict): def __init__(self): config = ConfigManager("core.conf").config options_conf_map = { - "max_connections": "max_connections_per_torrent", - "max_upload_slots": "max_upload_slots_per_torrent", - "max_upload_speed": "max_upload_speed_per_torrent", - "max_download_speed": "max_download_speed_per_torrent", - "prioritize_first_last_pieces": "prioritize_first_last_pieces", - "compact_allocation": "compact_allocation", - "download_location": "download_location", - "auto_managed": "auto_managed", - "stop_at_ratio": "stop_seed_at_ratio", - "stop_ratio": "stop_seed_ratio", - "remove_at_ratio": "remove_seed_at_ratio", - "move_completed": "move_completed", - "move_completed_path": "move_completed_path", - "add_paused": "add_paused", - "public": "public" - } + "max_connections": "max_connections_per_torrent", + "max_upload_slots": "max_upload_slots_per_torrent", + "max_upload_speed": "max_upload_speed_per_torrent", + "max_download_speed": "max_download_speed_per_torrent", + "prioritize_first_last_pieces": "prioritize_first_last_pieces", + "sequential_download": "sequential_download", + "compact_allocation": "compact_allocation", + "download_location": "download_location", + "auto_managed": "auto_managed", + "stop_at_ratio": "stop_seed_at_ratio", + "stop_ratio": "stop_seed_ratio", + "remove_at_ratio": "remove_seed_at_ratio", + "move_completed": "move_completed", + "move_completed_path": "move_completed_path", + "add_paused": "add_paused", + "shared": "shared" + } for opt_k, conf_k in options_conf_map.iteritems(): self[opt_k] = config[conf_k] self["file_priorities"] = [] @@ -188,8 +189,14 @@ class Torrent(object): else: self.owner = owner + # Keep track of last seen complete + if state: + self._last_seen_complete = state.last_seen_complete or 0.0 + else: + self._last_seen_complete = 0.0 + # Keep track if we're forcing a recheck of the torrent so that we can - # repause it after its done if necessary + # re-pause it after its done if necessary self.forcing_recheck = False self.forcing_recheck_paused = False @@ -206,17 +213,44 @@ class Torrent(object): "max_download_speed": self.set_max_download_speed, "max_upload_slots": self.handle.set_max_uploads, "max_upload_speed": self.set_max_upload_speed, - "prioritize_first_last_pieces": self.set_prioritize_first_last + "prioritize_first_last_pieces": self.set_prioritize_first_last, + "sequential_download": self.set_sequential_download + } for (key, value) in options.items(): if OPTIONS_FUNCS.has_key(key): OPTIONS_FUNCS[key](value) - self.options.update(options) def get_options(self): return self.options + def get_name(self): + if self.handle.has_metadata(): + name = self.torrent_info.file_at(0).path.split("/", 1)[0] + if not name: + name = self.torrent_info.name() + try: + return name.decode("utf8", "ignore") + except UnicodeDecodeError: + return name + elif self.magnet: + try: + keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')]) + name = keys.get('dn') + if not name: + return self.torrent_id + name = unquote(name).replace('+', ' ') + try: + return name.decode("utf8", "ignore") + except UnicodeDecodeError: + return name + except: + pass + return self.torrent_id + + def set_owner(self, account): + self.owner = account def set_max_connections(self, max_connections): self.options["max_connections"] = int(max_connections) @@ -245,14 +279,30 @@ class Torrent(object): def set_prioritize_first_last(self, prioritize): self.options["prioritize_first_last_pieces"] = prioritize - if prioritize: - if self.handle.has_metadata(): - if self.handle.get_torrent_info().num_files() == 1: - # We only do this if one file is in the torrent - priorities = [1] * self.handle.get_torrent_info().num_pieces() - priorities[0] = 7 - priorities[-1] = 7 - self.handle.prioritize_pieces(priorities) + if self.handle.has_metadata(): + if self.options["compact_allocation"]: + log.debug("Setting first/last priority with compact " + "allocation does not work!") + return + + paths = {} + ti = self.handle.get_torrent_info() + for n in range(ti.num_pieces()): + slices = ti.map_block(n, 0, ti.piece_size(n)) + for slice in slices: + fe = ti.file_at(slice.file_index) + paths.setdefault(fe.path, []).append(n) + + priorities = self.handle.piece_priorities() + for pieces in paths.itervalues(): + two_percent = 2*100/len(pieces) + for piece in pieces[:two_percent]+pieces[-two_percent:]: + priorities[piece] = prioritize and 7 or 1 + self.handle.prioritize_pieces(priorities) + + def set_sequential_download(self, set_sequencial): + self.options["sequential_download"] = set_sequencial + self.handle.set_sequential_download(set_sequencial) def set_auto_managed(self, auto_managed): self.options["auto_managed"] = auto_managed @@ -333,7 +383,7 @@ class Torrent(object): # Set the tracker list in the torrent object self.trackers = trackers if len(trackers) > 0: - # Force a reannounce if there is at least 1 tracker + # Force a re-announce if there is at least 1 tracker self.force_reannounce() self.tracker_host = None @@ -556,6 +606,16 @@ class Torrent(object): return host return "" + def get_last_seen_complete(self): + """ + Returns the time a torrent was last seen complete, ie, with all pieces + available. + """ + if lt.version_minor > 15: + return self.status.last_seen_complete + self.calculate_last_seen_complete() + return self._last_seen_complete + def get_status(self, keys, diff=False): """ Returns the status of the torrent based on the keys provided @@ -584,7 +644,13 @@ class Torrent(object): if distributed_copies < 0: distributed_copies = 0.0 - #if you add a key here->add it to core.py STATUS_KEYS too. + # Calculate the seeds:peers ratio + if self.status.num_incomplete == 0: + # Use -1.0 to signify infinity + seeds_peers_ratio = -1.0 + else: + seeds_peers_ratio = self.status.num_complete / float(self.status.num_incomplete) + full_status = { "active_time": self.status.active_time, "all_time_download": self.status.all_time_download, @@ -602,17 +668,21 @@ class Torrent(object): "message": self.statusmsg, "move_on_completed_path": self.options["move_completed_path"], "move_on_completed": self.options["move_completed"], + "move_completed_path": self.options["move_completed_path"], + "move_completed": self.options["move_completed"], "next_announce": self.status.next_announce.seconds, "num_peers": self.status.num_peers - self.status.num_seeds, "num_seeds": self.status.num_seeds, "owner": self.owner, "paused": self.status.paused, "prioritize_first_last": self.options["prioritize_first_last_pieces"], + "sequential_download": self.options["sequential_download"], "progress": progress, - "public": self.options["public"], + "shared": self.options["shared"], "remove_at_ratio": self.options["remove_at_ratio"], "save_path": self.options["download_location"], "seeding_time": self.status.seeding_time, + "seeds_peers_ratio": seeds_peers_ratio, "seed_rank": self.status.seed_rank, "state": self.state, "stop_at_ratio": self.options["stop_at_ratio"], @@ -639,32 +709,6 @@ class Torrent(object): return self.torrent_info.comment() return "" - def ti_name(): - if self.handle.has_metadata(): - name = self.torrent_info.file_at(0).path.split("/", 1)[0] - if not name: - name = self.torrent_info.name() - try: - return name.decode("utf8", "ignore") - except UnicodeDecodeError: - return name - - elif self.magnet: - try: - keys = dict([k.split('=') for k in self.magnet.split('?')[-1].split('&')]) - name = keys.get('dn') - if not name: - return self.torrent_id - name = unquote(name).replace('+', ' ') - try: - return name.decode("utf8", "ignore") - except UnicodeDecodeError: - return name - except: - pass - - return self.torrent_id - def ti_priv(): if self.handle.has_metadata(): return self.torrent_info.priv() @@ -685,6 +729,10 @@ class Torrent(object): if self.handle.has_metadata(): return self.torrent_info.piece_length() return 0 + def ti_pieces_info(): + if self.handle.has_metadata(): + return self.get_pieces_info() + return None fns = { "comment": ti_comment, @@ -692,9 +740,10 @@ class Torrent(object): "file_progress": self.get_file_progress, "files": self.get_files, "is_seed": self.handle.is_seed, - "name": ti_name, + "name": self.get_name, "num_files": ti_num_files, "num_pieces": ti_num_pieces, + "pieces": ti_pieces_info, "peers": self.get_peers, "piece_length": ti_piece_length, "private": ti_priv, @@ -702,6 +751,7 @@ class Torrent(object): "ratio": self.get_ratio, "total_size": ti_total_size, "tracker_host": self.get_tracker_host, + "last_seen_complete": self.get_last_seen_complete } # Create the desired status dictionary and return it @@ -745,6 +795,7 @@ class Torrent(object): self.handle.set_upload_limit(int(self.max_upload_speed * 1024)) self.handle.set_download_limit(int(self.max_download_speed * 1024)) self.handle.prioritize_files(self.file_priorities) + self.handle.set_sequential_download(self.options["sequential_download"]) self.handle.resolve_countries(True) def pause(self): @@ -807,16 +858,27 @@ class Torrent(object): def move_storage(self, dest): """Move a torrent's storage location""" - if not os.path.exists(dest): + + # Attempt to convert utf8 path to unicode + # Note: Inconsistent encoding for 'dest', needs future investigation + try: + dest_u = unicode(dest, "utf-8") + except TypeError: + # String is already unicode + dest_u = dest + + if not os.path.exists(dest_u): try: # Try to make the destination path if it doesn't exist - os.makedirs(dest) + os.makedirs(dest_u) except IOError, e: log.exception(e) - log.error("Could not move storage for torrent %s since %s does not exist and could not create the directory.", self.torrent_id, dest) + log.error("Could not move storage for torrent %s since %s does " + "not exist and could not create the directory.", + self.torrent_id, dest_u) return False try: - self.handle.move_storage(dest.encode("utf8")) + self.handle.move_storage(dest_u) except: return False @@ -903,8 +965,8 @@ class Torrent(object): log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder) return - if new_folder[-1:] != "/": - new_folder += "/" + # Make sure the new folder path is nice and has a trailing slash + new_folder = os.path.normpath(new_folder) + "/" wait_on_folder = (folder, new_folder, []) for f in self.get_files(): @@ -923,3 +985,52 @@ class Torrent(object): for key in self.prev_status.keys(): if not self.rpcserver.is_session_valid(key): del self.prev_status[key] + + def calculate_last_seen_complete(self): + if self._last_seen_complete+60 > time.time(): + # Simple caching. Only calculate every 1 min at minimum + return self._last_seen_complete + + availability = self.handle.piece_availability() + if filter(lambda x: x<1, availability): + # Torrent does not have all the pieces + return + log.trace("Torrent %s has all the pieces. Setting last seen complete.", + self.torrent_id) + self._last_seen_complete = time.time() + + def get_pieces_info(self): + pieces = {} + # First get the pieces availability. + availability = self.handle.piece_availability() + # Pieces from connected peers + for peer_info in self.handle.get_peer_info(): + if peer_info.downloading_piece_index < 0: + # No piece index, then we're not downloading anything from + # this peer + continue + pieces[peer_info.downloading_piece_index] = 2 + + # Now, the rest of the pieces + for idx, piece in enumerate(self.handle.status().pieces): + if idx in pieces: + # Piece beeing downloaded, handled above + continue + elif piece: + # Completed Piece + pieces[idx] = 3 + continue + elif availability[idx] > 0: + # Piece not downloaded nor beeing downloaded but available + pieces[idx] = 1 + continue + # If we reached here, it means the piece is missing, ie, there's + # no known peer with this piece, or this piece has not been asked + # for so far. + pieces[idx] = 0 + + sorted_indexes = pieces.keys() + sorted_indexes.sort() + # Return only the piece states, no need for the piece index + # Keep the order + return [pieces[idx] for idx in sorted_indexes] diff --git a/deluge/core/torrentmanager.py b/deluge/core/torrentmanager.py index b02443cb6..014bfad6d 100644 --- a/deluge/core/torrentmanager.py +++ b/deluge/core/torrentmanager.py @@ -42,8 +42,8 @@ import time import shutil import operator import logging +import re -from twisted.internet import reactor from twisted.internet.task import LoopingCall from deluge._libtorrent import lt @@ -52,6 +52,7 @@ from deluge.event import * from deluge.error import * import deluge.component as component from deluge.configmanager import ConfigManager, get_config_dir +from deluge.core.authmanager import AUTH_LEVEL_ADMIN from deluge.core.torrent import Torrent from deluge.core.torrent import TorrentOptions import deluge.core.oldstateupgrader @@ -73,6 +74,7 @@ class TorrentState: max_upload_speed=-1.0, max_download_speed=-1.0, prioritize_first_last=False, + sequential_download=False, file_priorities=None, queue=None, auto_managed=True, @@ -84,8 +86,9 @@ class TorrentState: move_completed_path=None, magnet=None, time_added=-1, - owner="", - public=False + last_seen_complete=0.0, # 0 is the default returned when the info + owner="", # does not exist on lt >= .16 + shared=False ): self.torrent_id = torrent_id self.filename = filename @@ -95,6 +98,7 @@ class TorrentState: self.is_finished = is_finished self.magnet = magnet self.time_added = time_added + self.last_seen_complete = last_seen_complete self.owner = owner # Options @@ -106,6 +110,7 @@ class TorrentState: self.max_upload_speed = max_upload_speed self.max_download_speed = max_download_speed self.prioritize_first_last = prioritize_first_last + self.sequential_download = sequential_download self.file_priorities = file_priorities self.auto_managed = auto_managed self.stop_ratio = stop_ratio @@ -113,7 +118,7 @@ class TorrentState: self.remove_at_ratio = remove_at_ratio self.move_completed = move_completed self.move_completed_path = move_completed_path - self.public = public + self.shared = shared class TorrentManagerState: def __init__(self): @@ -127,7 +132,8 @@ class TorrentManager(component.Component): """ def __init__(self): - component.Component.__init__(self, "TorrentManager", interval=5, depend=["CorePluginManager"]) + component.Component.__init__(self, "TorrentManager", interval=5, + depend=["CorePluginManager"]) log.debug("TorrentManager init..") # Set the libtorrent session self.session = component.get("Core").session @@ -142,6 +148,7 @@ class TorrentManager(component.Component): # Create the torrents dict { torrent_id: Torrent } self.torrents = {} + self.last_seen_complete_loop = None # This is a list of torrent_id when we shutdown the torrentmanager. # We use this list to determine if all active torrents have been paused @@ -214,6 +221,9 @@ class TorrentManager(component.Component): self.save_resume_data_timer = LoopingCall(self.save_resume_data) self.save_resume_data_timer.start(190) + if self.last_seen_complete_loop: + self.last_seen_complete_loop.start(60) + def stop(self): # Stop timers if self.save_state_timer.running: @@ -222,6 +232,9 @@ class TorrentManager(component.Component): if self.save_resume_data_timer.running: self.save_resume_data_timer.stop() + if self.last_seen_complete_loop: + self.last_seen_complete_loop.stop() + # Save state on shutdown self.save_state() @@ -261,9 +274,12 @@ class TorrentManager(component.Component): def update(self): for torrent_id, torrent in self.torrents.items(): - if torrent.options["stop_at_ratio"] and torrent.state not in ("Checking", "Allocating", "Paused", "Queued"): - # If the global setting is set, but the per-torrent isn't.. Just skip to the next torrent - # This is so that a user can turn-off the stop at ratio option on a per-torrent basis + if torrent.options["stop_at_ratio"] and torrent.state not in ( + "Checking", "Allocating", "Paused", "Queued"): + # If the global setting is set, but the per-torrent isn't.. + # Just skip to the next torrent. + # This is so that a user can turn-off the stop at ratio option + # on a per-torrent basis if not torrent.options["stop_at_ratio"]: continue if torrent.get_ratio() >= torrent.options["stop_ratio"] and torrent.is_finished: @@ -279,7 +295,16 @@ class TorrentManager(component.Component): def get_torrent_list(self): """Returns a list of torrent_ids""" - return self.torrents.keys() + torrent_ids = self.torrents.keys() + if component.get("RPCServer").get_session_auth_level() == AUTH_LEVEL_ADMIN: + return torrent_ids + + current_user = component.get("RPCServer").get_session_user() + for torrent_id in torrent_ids[:]: + torrent_status = self[torrent_id].get_status(["owner", "shared"]) + if torrent_status["owner"] != current_user and torrent_status["shared"] == False: + torrent_ids.pop(torrent_ids.index(torrent_id)) + return torrent_ids def get_torrent_info_from_file(self, filepath): """Returns a torrent_info for the file specified or None""" @@ -319,7 +344,8 @@ class TorrentManager(component.Component): log.warning("Unable to delete the fastresume file: %s", e) def add(self, torrent_info=None, state=None, options=None, save_state=True, - filedump=None, filename=None, magnet=None, resume_data=None): + filedump=None, filename=None, magnet=None, resume_data=None, + owner='localclient'): """Add a torrent to the manager and returns it's torrent_id""" if torrent_info is None and state is None and filedump is None and magnet is None: @@ -348,6 +374,7 @@ class TorrentManager(component.Component): options["max_upload_speed"] = state.max_upload_speed options["max_download_speed"] = state.max_download_speed options["prioritize_first_last_pieces"] = state.prioritize_first_last + options["sequential_download"] = state.sequential_download options["file_priorities"] = state.file_priorities options["compact_allocation"] = state.compact options["download_location"] = state.save_path @@ -358,7 +385,7 @@ class TorrentManager(component.Component): options["move_completed"] = state.move_completed options["move_completed_path"] = state.move_completed_path options["add_paused"] = state.paused - options["public"] = state.public + options["shared"] = state.shared ti = self.get_torrent_info_from_file( os.path.join(get_config_dir(), @@ -396,7 +423,6 @@ class TorrentManager(component.Component): torrent_info.rename_file(index, utf8_encoded(name)) add_torrent_params["ti"] = torrent_info - add_torrent_params["resume_data"] = "" #log.info("Adding torrent: %s", filename) log.debug("options: %s", options) @@ -437,7 +463,12 @@ class TorrentManager(component.Component): # Set auto_managed to False because the torrent is paused handle.auto_managed(False) # Create a Torrent object - owner = state.owner if state else component.get("RPCServer").get_session_user() + owner = state.owner if state else ( + owner if owner else component.get("RPCServer").get_session_user() + ) + account_exists = component.get("AuthManager").has_account(owner) + if not account_exists: + owner = 'localclient' torrent = Torrent(handle, options, state, filename, magnet, owner) # Add the torrent object to the dictionary self.torrents[torrent.torrent_id] = torrent @@ -484,10 +515,10 @@ class TorrentManager(component.Component): component.get("EventManager").emit( TorrentAddedEvent(torrent.torrent_id, from_state) ) - log.info("Torrent %s %s by user: %s", + log.info("Torrent %s from user \"%s\" %s", torrent.get_status(["name"])["name"], - (from_state and "added" or "loaded"), - component.get("RPCServer").get_session_user()) + torrent.get_status(["owner"])["owner"], + (from_state and "added" or "loaded")) return torrent.torrent_id def load_torrent(self, torrent_id): @@ -568,7 +599,7 @@ class TorrentManager(component.Component): # Remove the torrent from deluge's session try: del self.torrents[torrent_id] - except KeyError, ValueError: + except (KeyError, ValueError): return False # Save the session state @@ -576,7 +607,8 @@ class TorrentManager(component.Component): # Emit the signal to the clients component.get("EventManager").emit(TorrentRemovedEvent(torrent_id)) - log.info("Torrent %s removed by user: %s", torrent_name, component.get("RPCServer").get_session_user()) + log.info("Torrent %s removed by user: %s", torrent_name, + component.get("RPCServer").get_session_user()) return True def load_state(self): @@ -616,6 +648,17 @@ class TorrentManager(component.Component): log.error("Torrent state file is either corrupt or incompatible! %s", e) break + + if lt.version_minor < 16: + log.debug("libtorrent version is lower than 0.16. Start looping " + "callback to calculate last_seen_complete info.") + def calculate_last_seen_complete(): + for torrent in self.torrents.values(): + torrent.calculate_last_seen_complete() + self.last_seen_complete_loop = LoopingCall( + calculate_last_seen_complete + ) + component.get("EventManager").emit(SessionStartedEvent()) def save_state(self): @@ -640,6 +683,7 @@ class TorrentManager(component.Component): torrent.options["max_upload_speed"], torrent.options["max_download_speed"], torrent.options["prioritize_first_last_pieces"], + torrent.options["sequential_download"], torrent.options["file_priorities"], torrent.get_queue_position(), torrent.options["auto_managed"], @@ -651,8 +695,9 @@ class TorrentManager(component.Component): torrent.options["move_completed_path"], torrent.magnet, torrent.time_added, + torrent.get_last_seen_complete(), torrent.owner, - torrent.options["public"] + torrent.options["shared"] ) state.torrents.append(torrent_state) @@ -747,6 +792,38 @@ class TorrentManager(component.Component): except IOError: log.warning("Error trying to save fastresume file") + def remove_empty_folders(self, torrent_id, folder): + """ + Recursively removes folders but only if they are empty. + Cleans up after libtorrent folder renames. + + """ + if torrent_id not in self.torrents: + raise InvalidTorrentError("torrent_id is not in session") + + info = self.torrents[torrent_id].get_status(['save_path']) + # Regex removes leading slashes that causes join function to ignore save_path + folder_full_path = os.path.join(info['save_path'], re.sub("^/*", "", folder)) + folder_full_path = os.path.normpath(folder_full_path) + + try: + if not os.listdir(folder_full_path): + os.removedirs(folder_full_path) + log.debug("Removed Empty Folder %s", folder_full_path) + else: + for root, dirs, files in os.walk(folder_full_path, topdown=False): + for name in dirs: + try: + os.removedirs(os.path.join(root, name)) + log.debug("Removed Empty Folder %s", os.path.join(root, name)) + except OSError as (errno, strerror): + if errno == 39: + # Error raised if folder is not empty + log.debug("%s", strerror) + + except OSError as (errno, strerror): + log.debug("Cannot Remove Folder: %s (ErrNo %s)", strerror, errno) + def queue_top(self, torrent_id): """Queue torrent to top""" if self.torrents[torrent_id].get_queue_position() == 0: @@ -1008,6 +1085,8 @@ class TorrentManager(component.Component): if len(wait_on_folder[2]) == 1: # This is the last alert we were waiting for, time to send signal component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent_id, wait_on_folder[0], wait_on_folder[1])) + # Empty folders are removed after libtorrent folder renames + self.remove_empty_folders(torrent_id, wait_on_folder[0]) del torrent.waiting_on_folder_rename[i] self.save_resume_data((torrent_id,)) break diff --git a/deluge/data/share/applications/deluge.desktop b/deluge/data/share/applications/deluge.desktop deleted file mode 100644 index 2f3b16fdb..000000000 --- a/deluge/data/share/applications/deluge.desktop +++ /dev/null @@ -1,12 +0,0 @@ -[Desktop Entry] -Version=1.0 -Name=Deluge BitTorrent Client -GenericName=Bittorrent Client -Comment=Transfer files using the Bittorrent protocol -Exec=deluge-gtk -Icon=deluge -Terminal=false -Type=Application -Categories=Network; -StartupNotify=true -MimeType=application/x-bittorrent; diff --git a/deluge/error.py b/deluge/error.py index 24c1e8352..ea7c64671 100644 --- a/deluge/error.py +++ b/deluge/error.py @@ -2,6 +2,7 @@ # error.py # # Copyright (C) 2008 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -35,7 +36,21 @@ class DelugeError(Exception): - pass + def _get_message(self): + return self._message + def _set_message(self, message): + self._message = message + message = property(_get_message, _set_message) + del _get_message, _set_message + + def __str__(self): + return self.message + + def __new__(cls, *args, **kwargs): + inst = super(DelugeError, cls).__new__(cls, *args, **kwargs) + inst._args = args + inst._kwargs = kwargs + return inst class NoCoreError(DelugeError): pass @@ -48,3 +63,49 @@ class InvalidTorrentError(DelugeError): class InvalidPathError(DelugeError): pass + +class _ClientSideRecreateError(DelugeError): + pass + +class IncompatibleClient(_ClientSideRecreateError): + def __init__(self, daemon_version): + self.daemon_version = daemon_version + self.message = _( + "Your deluge client is not compatible with the daemon. " + "Please upgrade your client to %(daemon_version)s" + ) % dict(daemon_version=self.daemon_version) + +class NotAuthorizedError(_ClientSideRecreateError): + + def __init__(self, current_level, required_level): + self.message = _( + "Auth level too low: %(current_level)s < %(required_level)s" % + dict(current_level=current_level, required_level=required_level) + ) + self.current_level = current_level + self.required_level = required_level + + +class _UsernameBasedPasstroughError(_ClientSideRecreateError): + + def _get_username(self): + return self._username + def _set_username(self, username): + self._username = username + username = property(_get_username, _set_username) + del _get_username, _set_username + + def __init__(self, message, username): + super(_UsernameBasedPasstroughError, self).__init__(message) + self.message = message + self.username = username + + +class BadLoginError(_UsernameBasedPasstroughError): + pass + +class AuthenticationRequired(_UsernameBasedPasstroughError): + pass + +class AuthManagerError(_UsernameBasedPasstroughError): + pass diff --git a/deluge/event.py b/deluge/event.py index a4fc8b4a3..23027673f 100644 --- a/deluge/event.py +++ b/deluge/event.py @@ -2,7 +2,6 @@ # event.py # # Copyright (C) 2009 Andrew Resch -# Copyright (C) 2010 Pedro Algarvio # # Deluge is free software. # @@ -48,8 +47,6 @@ class DelugeEventMetaClass(type): """ This metaclass simply keeps a list of all events classes created. """ - __slots__ = () - def __init__(cls, name, bases, dct): super(DelugeEventMetaClass, cls).__init__(name, bases, dct) if name != "DelugeEvent": @@ -65,26 +62,23 @@ class DelugeEvent(object): :type args: list """ - __slots__ = () __metaclass__ = DelugeEventMetaClass def _get_name(self): return self.__class__.__name__ - name = property(fget=_get_name) def _get_args(self): - return [getattr(self, arg) for arg in self.__slots__] - args = property(fget=_get_args) + if not hasattr(self, "_args"): + return [] + return self._args - def copy(self): - return self.__class__(*self.args) + name = property(fget=_get_name) + args = property(fget=_get_args) class TorrentAddedEvent(DelugeEvent): """ Emitted when a new torrent is successfully added to the session. """ - __slots__ = ('torrent_id', 'from_state') - def __init__(self, torrent_id, from_state): """ :param torrent_id: the torrent_id of the torrent that was added @@ -92,41 +86,34 @@ class TorrentAddedEvent(DelugeEvent): :param from_state: was the torrent loaded from state? Or is it a new torrent. :type from_state: bool """ - self.torrent_id = torrent_id - self.from_state = from_state + self._args = [torrent_id, from_state] class TorrentRemovedEvent(DelugeEvent): """ Emitted when a torrent has been removed from the session. """ - __slots__ = ('torrent_id',) - def __init__(self, torrent_id): """ :param torrent_id: the torrent_id :type torrent_id: string """ - self.torrent_id = torrent_id + self._args = [torrent_id] class PreTorrentRemovedEvent(DelugeEvent): """ Emitted when a torrent is about to be removed from the session. """ - __slots__ = ('torrent_id',) - def __init__(self, torrent_id): """ :param torrent_id: the torrent_id :type torrent_id: string """ - self.torrent_id = torrent_id + self._args = [torrent_id] class TorrentStateChangedEvent(DelugeEvent): """ Emitted when a torrent changes state. """ - __slots__ = ('torrent_id', 'state') - def __init__(self, torrent_id, state): """ :param torrent_id: the torrent_id @@ -134,20 +121,18 @@ class TorrentStateChangedEvent(DelugeEvent): :param state: the new state :type state: string """ - self.torrent_id = torrent_id - self.state = state + self._args = [torrent_id, state] class TorrentQueueChangedEvent(DelugeEvent): """ Emitted when the queue order has changed. """ + pass class TorrentFolderRenamedEvent(DelugeEvent): """ Emitted when a folder within a torrent has been renamed. """ - __slots__ = ('torrent_id', 'old', 'new') - def __init__(self, torrent_id, old, new): """ :param torrent_id: the torrent_id @@ -157,54 +142,44 @@ class TorrentFolderRenamedEvent(DelugeEvent): :param new: the new folder name :type new: string """ - self.torrent_id = torrent_id - self.old = old - self.new = new + self._args = [torrent_id, old, new] class TorrentFileRenamedEvent(DelugeEvent): """ Emitted when a file within a torrent has been renamed. """ - __slots__ = ('torrent_id', 'index', 'filename') - - def __init__(self, torrent_id, index, filename): + def __init__(self, torrent_id, index, name): """ :param torrent_id: the torrent_id :type torrent_id: string :param index: the index of the file :type index: int - :param filename: the new filename - :type filename: string + :param name: the new filename + :type name: string """ - self.torrent_id = torrent_id - self.index = index - self.filename = filename + self._args = [torrent_id, index, name] class TorrentFinishedEvent(DelugeEvent): """ Emitted when a torrent finishes downloading. """ - __slots__ = ('torrent_id',) - def __init__(self, torrent_id): """ :param torrent_id: the torrent_id :type torrent_id: string """ - self.torrent_id = torrent_id + self._args = [torrent_id] class TorrentResumedEvent(DelugeEvent): """ Emitted when a torrent resumes from a paused state. """ - __slots__ = ('torrent_id',) - def __init__(self, torrent_id): """ :param torrent_id: the torrent_id :type torrent_id: string """ - self.torrent_id = torrent_id + self._args = [torrent_id] class TorrentFileCompletedEvent(DelugeEvent): """ @@ -213,8 +188,6 @@ class TorrentFileCompletedEvent(DelugeEvent): This will only work with libtorrent 0.15 or greater. """ - __slots__ = ('torrent_id', 'index') - def __init__(self, torrent_id, index): """ :param torrent_id: the torrent_id @@ -222,75 +195,61 @@ class TorrentFileCompletedEvent(DelugeEvent): :param index: the file index :type index: int """ - self.torrent_id = torrent_id - self.index = index + self._args = [torrent_id, index] class NewVersionAvailableEvent(DelugeEvent): """ Emitted when a more recent version of Deluge is available. """ - __slots__ = ('new_release',) - def __init__(self, new_release): """ :param new_release: the new version that is available :type new_release: string """ - self.new_release = new_release + self._args = [new_release] class SessionStartedEvent(DelugeEvent): """ Emitted when a session has started. This typically only happens once when the daemon is initially started. """ + pass class SessionPausedEvent(DelugeEvent): """ Emitted when the session has been paused. """ + pass class SessionResumedEvent(DelugeEvent): """ Emitted when the session has been resumed. """ + pass class ConfigValueChangedEvent(DelugeEvent): """ Emitted when a config value changes in the Core. """ - __slots__ = ('key', 'value') - def __init__(self, key, value): """ :param key: the key that changed :type key: string :param value: the new value of the `:param:key` """ - self.key = key - self.value = value + self._args = [key, value] class PluginEnabledEvent(DelugeEvent): """ Emitted when a plugin is enabled in the Core. """ - __slots__ = ('plugin_name',) - def __init__(self, plugin_name): - """ - :param plugin_name: the plugin name - :type plugin_name: string - """ - self.plugin_name = plugin_name + self._args = [plugin_name] class PluginDisabledEvent(DelugeEvent): """ Emitted when a plugin is disabled in the Core. """ - __slots__ = ('plugin_name',) - def __init__(self, plugin_name): - """ - :param plugin_name: the plugin name - :type plugin_name: string - """ - self.plugin_name = plugin_name + self._args = [plugin_name] + diff --git a/deluge/log.py b/deluge/log.py index b014fa9c7..04dfd75f0 100644 --- a/deluge/log.py +++ b/deluge/log.py @@ -48,9 +48,9 @@ __all__ = ["setupLogger", "setLoggerLevel", "getPluginLogger", "LOG"] LoggingLoggerClass = logging.getLoggerClass() if 'dev' in common.get_version(): - DEFAULT_LOGGING_FORMAT = "%%(asctime)s.%%(msecs)03.0f [%%(name)-%ds:%%(lineno)-4d][%%(levelname)-8s] %%(message)s" + DEFAULT_LOGGING_FORMAT = "%%(asctime)s.%%(msecs)03.0f [%%(levelname)-8s][%%(name)-%ds:%%(lineno)-4d] %%(message)s" else: - DEFAULT_LOGGING_FORMAT = "%%(asctime)s [%%(name)-%ds][%%(levelname)-8s] %%(message)s" + DEFAULT_LOGGING_FORMAT = "%%(asctime)s [%%(levelname)-8s][%%(name)-%ds] %%(message)s" MAX_LOGGER_NAME_LENGTH = 3 class Logging(LoggingLoggerClass): @@ -117,6 +117,7 @@ class Logging(LoggingLoggerClass): return rv levels = { + "none": logging.NOTSET, "info": logging.INFO, "warn": logging.WARNING, "warning": logging.WARNING, @@ -140,8 +141,10 @@ def setupLogger(level="error", filename=None, filemode="w"): if logging.getLoggerClass() is not Logging: logging.setLoggerClass(Logging) + logging.addLevelName(5, 'TRACE') + logging.addLevelName(1, 'GARBAGE') - level = levels.get(level, "error") + level = levels.get(level, logging.ERROR) rootLogger = logging.getLogger() @@ -149,7 +152,7 @@ def setupLogger(level="error", filename=None, filemode="w"): import logging.handlers handler = logging.handlers.RotatingFileHandler( filename, filemode, - maxBytes=5*1024*1024, # 5 Mb + maxBytes=50*1024*1024, # 50 Mb backupCount=3, encoding='utf-8', delay=0 @@ -162,6 +165,7 @@ def setupLogger(level="error", filename=None, filemode="w"): ) else: handler = logging.StreamHandler() + handler.setLevel(level) formatter = logging.Formatter( @@ -263,21 +267,30 @@ class __BackwardsCompatibleLOG(object): import warnings logger_name = 'deluge' stack = inspect.stack() - module_stack = stack.pop(1) + stack.pop(0) # The logging call from this module + module_stack = stack.pop(0) # The module that called the log function caller_module = inspect.getmodule(module_stack[0]) + # In some weird cases caller_module might be None, try to continue + caller_module_name = getattr(caller_module, '__name__', '') warnings.warn_explicit(DEPRECATION_WARNING, DeprecationWarning, module_stack[1], module_stack[2], - caller_module.__name__) - for member in stack: - module = inspect.getmodule(member[0]) - if not module: - continue - if module.__name__ in ('deluge.plugins.pluginbase', - 'deluge.plugins.init'): - logger_name += '.plugin.%s' % caller_module.__name__ - # Monkey Patch The Plugin Module - caller_module.log = logging.getLogger(logger_name) - break + caller_module_name) + if caller_module: + for member in stack: + module = inspect.getmodule(member[0]) + if not module: + continue + if module.__name__ in ('deluge.plugins.pluginbase', + 'deluge.plugins.init'): + logger_name += '.plugin.%s' % caller_module_name + # Monkey Patch The Plugin Module + caller_module.log = logging.getLogger(logger_name) + break + else: + logging.getLogger(logger_name).warning( + "Unable to monkey-patch the calling module's `log` attribute! " + "You should really update and rebuild your plugins..." + ) return getattr(logging.getLogger(logger_name), name) LOG = __BackwardsCompatibleLOG() diff --git a/deluge/main.py b/deluge/main.py index dfb489d59..b52988da6 100644 --- a/deluge/main.py +++ b/deluge/main.py @@ -149,14 +149,20 @@ this should be an IP address", metavar="IFACE", parser.add_option("-u", "--ui-interface", dest="ui_interface", help="Interface daemon will listen for UI connections on, this should be\ an IP address", metavar="IFACE", action="store", type="str") - parser.add_option("-d", "--do-not-daemonize", dest="donot", - help="Do not daemonize", action="store_true", default=False) + if not (deluge.common.windows_check() or deluge.common.osx_check()): + parser.add_option("-d", "--do-not-daemonize", dest="donot", + help="Do not daemonize", action="store_true", default=False) parser.add_option("-c", "--config", dest="config", help="Set the config location", action="store", type="str") parser.add_option("-l", "--logfile", dest="logfile", help="Set the logfile location", action="store", type="str") parser.add_option("-P", "--pidfile", dest="pidfile", help="Use pidfile to store process id", action="store", type="str") + if not deluge.common.windows_check(): + parser.add_option("-U", "--user", dest="user", + help="User to switch to. Only use it when starting as root", action="store", type="str") + parser.add_option("-g", "--group", dest="group", + help="Group to switch to. Only use it when starting as root", action="store", type="str") parser.add_option("-L", "--loglevel", dest="loglevel", help="Set the log level: none, info, warning, error, critical, debug", action="store", type="str") parser.add_option("-q", "--quiet", dest="quiet", @@ -197,24 +203,30 @@ this should be an IP address", metavar="IFACE", open(options.pidfile, "wb").write("%s\n" % os.getpid()) # If the donot daemonize is set, then we just skip the forking - if not options.donot: - # Windows check, we log to the config folder by default - if deluge.common.windows_check() or deluge.common.osx_check(): - open_logfile() - write_pidfile() - else: - if os.fork() == 0: - os.setsid() - if os.fork() == 0: - open_logfile() - write_pidfile() - else: - os._exit(0) - else: - os._exit(0) - else: - # Do not daemonize - write_pidfile() + if not (deluge.common.windows_check() or deluge.common.osx_check() or options.donot): + if os.fork(): + # We've forked and this is now the parent process, so die! + os._exit(0) + os.setsid() + # Do second fork + if os.fork(): + os._exit(0) + + # Write pid file before chuid + write_pidfile() + + if options.user: + if not options.user.isdigit(): + import pwd + options.user = pwd.getpwnam(options.user)[2] + os.setuid(options.user) + if options.group: + if not options.group.isdigit(): + import grp + options.group = grp.getgrnam(options.group)[2] + os.setuid(options.group) + + open_logfile() # Setup the logger try: diff --git a/deluge/maketorrent.py b/deluge/maketorrent.py index 585dcaa4e..196399df7 100644 --- a/deluge/maketorrent.py +++ b/deluge/maketorrent.py @@ -38,7 +38,7 @@ import os from hashlib import sha1 as sha from deluge.common import get_path_size -from deluge.bencode import bencode, bdecode +from deluge.bencode import bencode class InvalidPath(Exception): """ diff --git a/deluge/plugins/AutoAdd/deluge/plugins/autoadd/core.py b/deluge/plugins/AutoAdd/deluge/plugins/autoadd/core.py index 4f48a9799..e0b3f5b53 100644 --- a/deluge/plugins/AutoAdd/deluge/plugins/autoadd/core.py +++ b/deluge/plugins/AutoAdd/deluge/plugins/autoadd/core.py @@ -2,6 +2,7 @@ # core.py # # Copyright (C) 2009 GazpachoKing +# Copyright (C) 2011 Pedro Algarvio # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -43,6 +44,7 @@ from deluge.log import getPluginLogger from deluge.plugins.pluginbase import CorePluginBase import deluge.component as component import deluge.configmanager +from deluge.common import AUTH_LEVEL_ADMIN from deluge.core.rpcserver import export from twisted.internet.task import LoopingCall, deferLater from twisted.internet import reactor @@ -59,6 +61,8 @@ OPTIONS_AVAILABLE = { #option: builtin "enabled":False, "path":False, "append_extension":False, + "copy_torrent": False, + "delete_copy_torrent_toggle": False, "abspath":False, "download_location":True, "max_download_speed":True, @@ -74,13 +78,16 @@ OPTIONS_AVAILABLE = { #option: builtin "move_completed_path":True, "label":False, "add_paused":True, - "queue_to_top":False + "queue_to_top":False, + "owner": "localclient" } MAX_NUM_ATTEMPTS = 10 class AutoaddOptionsChangedEvent(DelugeEvent): """Emitted when the options for the plugin are changed.""" + def __init__(self): + pass def CheckInput(cond, message): if not cond: @@ -91,36 +98,31 @@ class Core(CorePluginBase): #reduce typing, assigning some values to self... self.config = deluge.configmanager.ConfigManager("autoadd.conf", DEFAULT_PREFS) + self.config.run_converter((0, 1), 2, self.__migrate_config_1_to_2) + self.config.save() self.watchdirs = self.config["watchdirs"] - self.core_cfg = deluge.configmanager.ConfigManager("core.conf") + + component.get("EventManager").register_event_handler( + "PreTorrentRemovedEvent", self.__on_pre_torrent_removed + ) # Dict of Filename:Attempts self.invalid_torrents = {} # Loopingcall timers for each enabled watchdir self.update_timers = {} - # If core autoadd folder is enabled, move it to the plugin - if self.core_cfg.config.get('autoadd_enable'): - # Disable core autoadd - self.core_cfg['autoadd_enable'] = False - self.core_cfg.save() - # Check if core autoadd folder is already added in plugin - for watchdir in self.watchdirs: - if os.path.abspath(self.core_cfg['autoadd_location']) == watchdir['abspath']: - watchdir['enabled'] = True - break - else: - # didn't find core watchdir, add it - self.add({'path':self.core_cfg['autoadd_location'], 'enabled':True}) deferLater(reactor, 5, self.enable_looping) def enable_looping(self): - #Enable all looping calls for enabled watchdirs here + # Enable all looping calls for enabled watchdirs here for watchdir_id, watchdir in self.watchdirs.iteritems(): if watchdir['enabled']: self.enable_watchdir(watchdir_id) def disable(self): #disable all running looping calls + component.get("EventManager").deregister_event_handler( + "PreTorrentRemovedEvent", self.__on_pre_torrent_removed + ) for loopingcall in self.update_timers.itervalues(): loopingcall.stop() self.config.save() @@ -128,21 +130,25 @@ class Core(CorePluginBase): def update(self): pass - @export() + @export def set_options(self, watchdir_id, options): """Update the options for a watch folder.""" watchdir_id = str(watchdir_id) options = self._make_unicode(options) - CheckInput(watchdir_id in self.watchdirs , _("Watch folder does not exist.")) + CheckInput( + watchdir_id in self.watchdirs, _("Watch folder does not exist.") + ) if options.has_key('path'): options['abspath'] = os.path.abspath(options['path']) - CheckInput(os.path.isdir(options['abspath']), _("Path does not exist.")) + CheckInput( + os.path.isdir(options['abspath']), _("Path does not exist.") + ) for w_id, w in self.watchdirs.iteritems(): if options['abspath'] == w['abspath'] and watchdir_id != w_id: raise Exception("Path is already being watched.") for key in options.keys(): - if not key in OPTIONS_AVAILABLE: - if not key in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]: + if key not in OPTIONS_AVAILABLE: + if key not in [key2+'_toggle' for key2 in OPTIONS_AVAILABLE.iterkeys()]: raise Exception("autoadd: Invalid options key:%s" % key) #disable the watch loop if it was active if watchdir_id in self.update_timers: @@ -168,16 +174,19 @@ class Core(CorePluginBase): raise e # Get the info to see if any exceptions are raised - info = lt.torrent_info(lt.bdecode(filedump)) + lt.torrent_info(lt.bdecode(filedump)) return filedump def update_watchdir(self, watchdir_id): """Check the watch folder for new torrents to add.""" + log.trace("Updating watchdir id: %s", watchdir_id) watchdir_id = str(watchdir_id) watchdir = self.watchdirs[watchdir_id] if not watchdir['enabled']: # We shouldn't be updating because this watchdir is not enabled + log.debug("Watchdir id %s is not enabled. Disabling it.", + watchdir_id) self.disable_watchdir(watchdir_id) return @@ -190,19 +199,23 @@ class Core(CorePluginBase): opts = {} if 'stop_at_ratio_toggle' in watchdir: watchdir['stop_ratio_toggle'] = watchdir['stop_at_ratio_toggle'] - # We default to True wher reading _toggle values, so a config + # We default to True when reading _toggle values, so a config # without them is valid, and applies all its settings. for option, value in watchdir.iteritems(): if OPTIONS_AVAILABLE.get(option): if watchdir.get(option+'_toggle', True): opts[option] = value for filename in os.listdir(watchdir["abspath"]): - if filename.split(".")[-1] == "torrent": - try: - filepath = os.path.join(watchdir["abspath"], filename) - except UnicodeDecodeError, e: - log.error("Unable to auto add torrent due to inproper filename encoding: %s", e) - continue + try: + filepath = os.path.join(watchdir["abspath"], filename) + except UnicodeDecodeError, e: + log.error("Unable to auto add torrent due to improper " + "filename encoding: %s", e) + continue + if os.path.isdir(filepath): + # Skip directories + continue + elif os.path.splitext(filename)[1] == ".torrent": try: filedump = self.load_torrent(filepath) except (RuntimeError, Exception), e: @@ -213,6 +226,10 @@ class Core(CorePluginBase): if filename in self.invalid_torrents: self.invalid_torrents[filename] += 1 if self.invalid_torrents[filename] >= MAX_NUM_ATTEMPTS: + log.warning( + "Maximum attempts reached while trying to add the " + "torrent file with the path %s", filepath + ) os.rename(filepath, filepath + ".invalid") del self.invalid_torrents[filename] else: @@ -220,7 +237,10 @@ class Core(CorePluginBase): continue # The torrent looks good, so lets add it to the session. - torrent_id = component.get("TorrentManager").add(filedump=filedump, filename=filename, options=opts) + torrent_id = component.get("TorrentManager").add( + filedump=filedump, filename=filename, options=opts, + owner=watchdir.get("owner", "localclient") + ) # If the torrent added successfully, set the extra options. if torrent_id: if 'Label' in component.get("CorePluginManager").get_enabled_plugins(): @@ -234,43 +254,72 @@ class Core(CorePluginBase): component.get("TorrentManager").queue_top(torrent_id) else: component.get("TorrentManager").queue_bottom(torrent_id) - # Rename or delete the torrent once added to deluge. + + # Rename, copy or delete the torrent once added to deluge. if watchdir.get('append_extension_toggle'): if not watchdir.get('append_extension'): watchdir['append_extension'] = ".added" os.rename(filepath, filepath + watchdir['append_extension']) + elif watchdir.get('copy_torrent_toggle'): + copy_torrent_path = watchdir['copy_torrent'] + copy_torrent_file = os.path.join(copy_torrent_path, filename) + log.debug("Moving added torrent file \"%s\" to \"%s\"", + os.path.basename(filepath), copy_torrent_path) + try: + os.rename(filepath, copy_torrent_file) + except OSError, why: + if why.errno == 18: + # This can happen for different mount points + from shutil import copyfile + try: + copyfile(filepath, copy_torrent_file) + os.remove(filepath) + except OSError: + # Last Resort! + try: + open(copy_torrent_file, 'wb').write( + open(filepath, 'rb').read() + ) + os.remove(filepath) + except OSError, why: + raise why + else: + raise why else: os.remove(filepath) def on_update_watchdir_error(self, failure, watchdir_id): - """Disables any watch folders with unhandled exceptions.""" + """Disables any watch folders with un-handled exceptions.""" self.disable_watchdir(watchdir_id) - log.error("Disabling '%s', error during update: %s" % (self.watchdirs[watchdir_id]["path"], failure)) + log.error("Disabling '%s', error during update: %s", + self.watchdirs[watchdir_id]["path"], failure) @export def enable_watchdir(self, watchdir_id): - watchdir_id = str(watchdir_id) + w_id = str(watchdir_id) # Enable the looping call - if watchdir_id not in self.update_timers or not self.update_timers[watchdir_id].running: - self.update_timers[watchdir_id] = LoopingCall(self.update_watchdir, watchdir_id) - self.update_timers[watchdir_id].start(5).addErrback(self.on_update_watchdir_error, watchdir_id) + if w_id not in self.update_timers or not self.update_timers[w_id].running: + self.update_timers[w_id] = LoopingCall(self.update_watchdir, w_id) + self.update_timers[w_id].start(5).addErrback( + self.on_update_watchdir_error, w_id + ) # Update the config - if not self.watchdirs[watchdir_id]['enabled']: - self.watchdirs[watchdir_id]['enabled'] = True + if not self.watchdirs[w_id]['enabled']: + self.watchdirs[w_id]['enabled'] = True self.config.save() component.get("EventManager").emit(AutoaddOptionsChangedEvent()) @export def disable_watchdir(self, watchdir_id): - watchdir_id = str(watchdir_id) + w_id = str(watchdir_id) # Disable the looping call - if watchdir_id in self.update_timers: - if self.update_timers[watchdir_id].running: - self.update_timers[watchdir_id].stop() - del self.update_timers[watchdir_id] + if w_id in self.update_timers: + if self.update_timers[w_id].running: + self.update_timers[w_id].stop() + del self.update_timers[w_id] # Update the config - if self.watchdirs[watchdir_id]['enabled']: - self.watchdirs[watchdir_id]['enabled'] = False + if self.watchdirs[w_id]['enabled']: + self.watchdirs[w_id]['enabled'] = False self.config.save() component.get("EventManager").emit(AutoaddOptionsChangedEvent()) @@ -288,9 +337,24 @@ class Core(CorePluginBase): """Returns the config dictionary.""" return self.config.config - @export() + @export def get_watchdirs(self): - return self.watchdirs.keys() + rpcserver = component.get("RPCServer") + session_user = rpcserver.get_session_user() + session_auth_level = rpcserver.get_session_auth_level() + if session_auth_level == AUTH_LEVEL_ADMIN: + log.debug("Current logged in user %s is an ADMIN, send all " + "watchdirs", session_user) + return self.watchdirs + + watchdirs = {} + for watchdir_id, watchdir in self.watchdirs.iteritems(): + if watchdir.get("owner", "localclient") == session_user: + watchdirs[watchdir_id] = watchdir + + log.debug("Current logged in user %s is not an ADMIN, send only " + "his watchdirs: %s", session_user, watchdirs.keys()) + return watchdirs def _make_unicode(self, options): opts = {} @@ -300,13 +364,16 @@ class Core(CorePluginBase): opts[key] = options[key] return opts - @export() + @export def add(self, options={}): """Add a watch folder.""" options = self._make_unicode(options) abswatchdir = os.path.abspath(options['path']) CheckInput(os.path.isdir(abswatchdir) , _("Path does not exist.")) - CheckInput(os.access(abswatchdir, os.R_OK|os.W_OK), "You must have read and write access to watch folder.") + CheckInput( + os.access(abswatchdir, os.R_OK|os.W_OK), + "You must have read and write access to watch folder." + ) if abswatchdir in [wd['abspath'] for wd in self.watchdirs.itervalues()]: raise Exception("Path is already being watched.") options.setdefault('enabled', False) @@ -324,9 +391,43 @@ class Core(CorePluginBase): def remove(self, watchdir_id): """Remove a watch folder.""" watchdir_id = str(watchdir_id) - CheckInput(watchdir_id in self.watchdirs, "Unknown Watchdir: %s" % self.watchdirs) + CheckInput(watchdir_id in self.watchdirs, + "Unknown Watchdir: %s" % self.watchdirs) if self.watchdirs[watchdir_id]['enabled']: self.disable_watchdir(watchdir_id) del self.watchdirs[watchdir_id] self.config.save() component.get("EventManager").emit(AutoaddOptionsChangedEvent()) + + def __migrate_config_1_to_2(self, config): + for watchdir_id in config['watchdirs'].iterkeys(): + config['watchdirs'][watchdir_id]['owner'] = 'localclient' + return config + + def __on_pre_torrent_removed(self, torrent_id): + try: + torrent = component.get("TorrentManager")[torrent_id] + except KeyError: + log.warning("Unable to remove torrent file for torrent id %s. It" + "was already deleted from the TorrentManager", + torrent_id) + return + torrent_fname = torrent.filename + for watchdir in self.watchdirs.itervalues(): + if not watchdir.get('copy_torrent_toggle', False): + # This watchlist does copy torrents + continue + elif not watchdir.get('delete_copy_torrent_toggle', False): + # This watchlist is not set to delete finished torrents + continue + copy_torrent_path = watchdir['copy_torrent'] + torrent_fname_path = os.path.join(copy_torrent_path, torrent_fname) + if os.path.isfile(torrent_fname_path): + try: + os.remove(torrent_fname_path) + log.info("Removed torrent file \"%s\" from \"%s\"", + torrent_fname, copy_torrent_path) + break + except OSError, e: + log.info("Failed to removed torrent file \"%s\" from " + "\"%s\": %s", torrent_fname, copy_torrent_path, e) diff --git a/deluge/plugins/AutoAdd/deluge/plugins/autoadd/data/autoadd_options.glade b/deluge/plugins/AutoAdd/deluge/plugins/autoadd/data/autoadd_options.glade index 3ec79b716..ea7167e9d 100644 --- a/deluge/plugins/AutoAdd/deluge/plugins/autoadd/data/autoadd_options.glade +++ b/deluge/plugins/AutoAdd/deluge/plugins/autoadd/data/autoadd_options.glade @@ -1,80 +1,9 @@ - + - - 6 - AutoAdd Error - False - True - dialog - - - - True - - - True - - - True - gtk-dialog-error - - - False - 0 - - - - - True - 0.46000000834465027 - Error - True - - - False - False - 1 - - - - - 2 - - - - - True - end - - - gtk-ok - -5 - True - True - True - False - True - - - - False - False - 0 - - - - - False - end - 0 - - - - - + False Watch Folder Properties False True @@ -83,11 +12,75 @@ True - vertical + False + + + True + False + end + + + gtk-cancel + True + True + True + False + False + True + + + + False + False + 0 + + + + + gtk-add + True + True + True + False + False + True + + + + False + False + 1 + + + + + gtk-apply + True + True + True + False + False + True + + + + False + False + 2 + + + + + False + True + end + 0 + + True - vertical + False True @@ -95,46 +88,64 @@ True + False 6 - vertical True + False 0 none True + False 12 True - vertical + False True + False True True - + If a .torrent file is added to this directory, +it will be added to the session. + + False + False + True + True + True + True 0 True + False + If a .torrent file is added to this directory, +it will be added to the session. select-folder Select A Folder + True + True 1 + True + True 0 @@ -144,6 +155,7 @@ True True False + False True True @@ -161,6 +173,7 @@ True + False <b>Watch Folder</b> True @@ -170,53 +183,107 @@ + True False 0 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + The user selected here will be the owner of the torrent. + + + + + + + True + False + <b>Owner</b> + True + + + label_item + + + + + True + True + 1 + + True + False 0 none True + False 12 True + False True - vertical + False Delete .torrent after adding True True False - True + Once the torrent is added to the session, +the .torrent will be deleted. + False True + + True + True 0 True + False Append extension after adding: True True False + Once the torrent is added to the session, +an extension will be appended to the .torrent +and it will remain in the same directory. + False True isnt_append_extension + True + True 0 @@ -224,18 +291,112 @@ True True - + .added + False + False + True + True + True + True 1 + True + True 1 + + + True + False + 2 + 2 + + + Copy of .torrent files to: + True + True + False + Once the torrent is added to the session, +the .torrent will copied to the chosen directory +and deleted from the watch folder. + False + True + isnt_append_extension + + + + + + True + False + + + True + True + + False + False + True + True + + + True + True + 0 + + + + + True + False + select-folder + Select A Folder + + + True + True + 1 + + + + + 1 + 2 + + + + + Delete copy of torrent file on remove + True + True + False + True + Once the torrent is deleted from the session, +also delete the .torrent file used to add it. + False + True + + + 2 + 1 + 2 + 15 + + + + + True + True + 2 + + @@ -245,6 +406,7 @@ True + False <b>Torrent File Action</b> True @@ -254,28 +416,34 @@ - 1 + True + True + 2 True + False 0 none True + False 12 True - vertical + False Set download location True True False + This directory will be the download location + False True True @@ -289,28 +457,39 @@ True + False True True - + + False + False + True + True + True + True 0 True + False select-folder Select A Folder + True + True 1 + True False 1 @@ -322,6 +501,7 @@ True + False <b>Download Location</b> True @@ -331,29 +511,33 @@ + True False - 2 + 3 True + False 0 none True + False 12 True - vertical + False Set move completed location True True False + False True True @@ -367,23 +551,33 @@ True + False True True - + + False + False + True + True + True + True 0 True + False select-folder Select A Folder + True + True 1 @@ -392,6 +586,7 @@ False False False + False True True True @@ -404,6 +599,7 @@ + True False 1 @@ -415,6 +611,7 @@ True + False <b>Move Completed</b> True @@ -424,27 +621,32 @@ + True False - 3 + 4 + False 0 none True + False 12 True + False Label: True True False + False True True @@ -456,12 +658,22 @@ - + True - True - + False + + + False + False + False + True + True + + + True + True 1 @@ -472,6 +684,7 @@ True + False <b>Label</b> True @@ -481,7 +694,9 @@ - 4 + True + True + 5 @@ -489,6 +704,7 @@ True + False Main @@ -499,32 +715,38 @@ True + False 6 - vertical True + False 0 none True + False 12 True + False 3 4 3 + 2 + 4 Max Upload Speed: True True False + False True True @@ -542,6 +764,7 @@ True True False + False True True @@ -559,6 +782,7 @@ True True False + False True True @@ -574,7 +798,11 @@ True True - -1 -1 10000 1 10 0 + False + False + True + True + 0 -1 10000 1 10 0 1 1 @@ -588,7 +816,11 @@ True True - -1 -1 10000 1 10 0 + False + False + True + True + 0 -1 10000 1 10 0 1 1 @@ -604,7 +836,11 @@ True True - -1 -1 10000 1 10 0 + False + False + True + True + 0 -1 10000 1 10 0 1 @@ -619,7 +855,11 @@ True True - -1 -1 10000 1 10 0 + False + False + True + True + 0 -1 10000 1 10 0 1 @@ -633,6 +873,7 @@ True + False 0 5 KiB/s @@ -647,6 +888,7 @@ True + False 0 5 KiB/s @@ -666,6 +908,7 @@ True True False + False True True @@ -688,6 +931,7 @@ True + False <b>Bandwidth</b> True @@ -697,32 +941,41 @@ + True + True 1 True + False 0 none True + False 12 True + False 5 4 + 2 + 4 True + False Stop seed at ratio: True True False + False True True @@ -738,6 +991,7 @@ True + False 0 0 12 @@ -747,6 +1001,7 @@ True True False + False True True @@ -763,6 +1018,7 @@ True True False + False True True @@ -779,6 +1035,7 @@ False True False + False True True True @@ -797,6 +1054,7 @@ False True False + False True True True @@ -814,7 +1072,11 @@ True True - + + False + False + True + True 2 0 100 0.10000000149 10 0 1 1 @@ -830,6 +1092,7 @@ True + False True @@ -837,10 +1100,13 @@ True True False + False True True + True + True 0 @@ -850,10 +1116,13 @@ True True False + False True auto_managed + True + True 1 @@ -872,6 +1141,7 @@ False True False + False True True True @@ -891,6 +1161,7 @@ True True False + False True @@ -898,6 +1169,7 @@ True + False True @@ -905,10 +1177,13 @@ True True False + False True True + True + True 0 @@ -918,10 +1193,13 @@ True True False + False True add_paused + True + True 1 @@ -937,6 +1215,7 @@ True True False + False True @@ -948,6 +1227,7 @@ True + False True @@ -955,10 +1235,13 @@ True True False + False True True + True + True 0 @@ -968,10 +1251,13 @@ True True False + False True queue_to_top + True + True 1 @@ -1014,6 +1300,7 @@ True + False <b>Queue</b> True @@ -1023,6 +1310,8 @@ + True + True 2 @@ -1034,6 +1323,7 @@ True + False Options @@ -1044,82 +1334,29 @@ + True + True 0 True - vertical + False + True + True 1 + True + True 2 - - - True - end - - - gtk-cancel - True - True - True - False - True - - - - False - False - 0 - - - - - gtk-add - True - True - True - False - True - - - - False - False - 1 - - - - - gtk-apply - True - True - True - False - True - - - - False - False - 2 - - - - - False - end - 0 - - diff --git a/deluge/plugins/AutoAdd/deluge/plugins/autoadd/gtkui.py b/deluge/plugins/AutoAdd/deluge/plugins/autoadd/gtkui.py index 9ad27cb9f..e32d37ff6 100644 --- a/deluge/plugins/AutoAdd/deluge/plugins/autoadd/gtkui.py +++ b/deluge/plugins/AutoAdd/deluge/plugins/autoadd/gtkui.py @@ -41,6 +41,7 @@ import gtk from deluge.log import getPluginLogger from deluge.ui.client import client +from deluge.ui.gtkui import dialogs from deluge.plugins.pluginbase import GtkPluginBase import deluge.component as component import deluge.common @@ -50,12 +51,19 @@ from common import get_resource log = getPluginLogger(__name__) +class IncompatibleOption(Exception): + pass + class OptionsDialog(): spin_ids = ["max_download_speed", "max_upload_speed", "stop_ratio"] spin_int_ids = ["max_upload_slots", "max_connections"] - chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed", "add_paused", "auto_managed", "queue_to_top"] + chk_ids = ["stop_at_ratio", "remove_at_ratio", "move_completed", + "add_paused", "auto_managed", "queue_to_top"] + def __init__(self): - pass + self.accounts = gtk.ListStore(str) + self.labels = gtk.ListStore(str) + self.core_config = {} def show(self, options={}, watchdir_id=None): self.glade = gtk.glade.XML(get_resource("autoadd_options.glade")) @@ -64,14 +72,11 @@ class OptionsDialog(): "on_opts_apply":self.on_apply, "on_opts_cancel":self.on_cancel, "on_options_dialog_close":self.on_cancel, - "on_error_ok":self.on_error_ok, - "on_error_dialog_close":self.on_error_ok, "on_toggle_toggled":self.on_toggle_toggled }) self.dialog = self.glade.get_widget("options_dialog") self.dialog.set_transient_for(component.get("Preferences").pref_dialog) - self.err_dialog = self.glade.get_widget("error_dialog") - self.err_dialog.set_transient_for(self.dialog) + if watchdir_id: #We have an existing watchdir_id, we are editing self.glade.get_widget('opts_add_button').hide() @@ -87,12 +92,36 @@ class OptionsDialog(): self.dialog.run() def load_options(self, options): - self.glade.get_widget('enabled').set_active(options.get('enabled', False)) - self.glade.get_widget('append_extension_toggle').set_active(options.get('append_extension_toggle', False)) - self.glade.get_widget('append_extension').set_text(options.get('append_extension', '.added')) - self.glade.get_widget('download_location_toggle').set_active(options.get('download_location_toggle', False)) - self.glade.get_widget('label').set_text(options.get('label', '')) + self.glade.get_widget('enabled').set_active(options.get('enabled', True)) + self.glade.get_widget('append_extension_toggle').set_active( + options.get('append_extension_toggle', False) + ) + self.glade.get_widget('append_extension').set_text( + options.get('append_extension', '.added') + ) + self.glade.get_widget('download_location_toggle').set_active( + options.get('download_location_toggle', False) + ) + self.glade.get_widget('copy_torrent_toggle').set_active( + options.get('copy_torrent_toggle', False) + ) + self.glade.get_widget('delete_copy_torrent_toggle').set_active( + options.get('delete_copy_torrent_toggle', False) + ) + self.accounts.clear() + self.labels.clear() + combobox = self.glade.get_widget('OwnerCombobox') + combobox_render = gtk.CellRendererText() + combobox.pack_start(combobox_render, True) + combobox.add_attribute(combobox_render, 'text', 0) + combobox.set_model(self.accounts) + + label_widget = self.glade.get_widget('label') + label_widget.child.set_text(options.get('label', '')) + label_widget.set_model(self.labels) + label_widget.set_text_column(0) self.glade.get_widget('label_toggle').set_active(options.get('label_toggle', False)) + for id in self.spin_ids + self.spin_int_ids: self.glade.get_widget(id).set_value(options.get(id, 0)) self.glade.get_widget(id+'_toggle').set_active(options.get(id+'_toggle', False)) @@ -105,30 +134,115 @@ class OptionsDialog(): self.glade.get_widget('isnt_queue_to_top').set_active(True) if not options.get('auto_managed', True): self.glade.get_widget('isnt_auto_managed').set_active(True) - for field in ['move_completed_path', 'path', 'download_location']: + for field in ['move_completed_path', 'path', 'download_location', + 'copy_torrent']: if client.is_localhost(): - self.glade.get_widget(field+"_chooser").set_filename(options.get(field, os.path.expanduser("~"))) + self.glade.get_widget(field+"_chooser").set_current_folder( + options.get(field, os.path.expanduser("~")) + ) self.glade.get_widget(field+"_chooser").show() self.glade.get_widget(field+"_entry").hide() else: - self.glade.get_widget(field+"_entry").set_text(options.get(field, "")) + self.glade.get_widget(field+"_entry").set_text( + options.get(field, "") + ) self.glade.get_widget(field+"_entry").show() self.glade.get_widget(field+"_chooser").hide() self.set_sensitive() + def on_core_config(config): + if client.is_localhost(): + self.glade.get_widget('download_location_chooser').set_current_folder( + options.get('download_location', config["download_location"]) + ) + if options.get('move_completed_toggle', config["move_completed"]): + self.glade.get_widget('move_completed_toggle').set_active(True) + self.glade.get_widget('move_completed_path_chooser').set_current_folder( + options.get('move_completed_path', config["move_completed_path"]) + ) + if options.get('copy_torrent_toggle', config["copy_torrent_file"]): + self.glade.get_widget('copy_torrent_toggle').set_active(True) + self.glade.get_widget('copy_torrent_chooser').set_current_folder( + options.get('copy_torrent', config["torrentfiles_location"]) + ) + else: + self.glade.get_widget('download_location_entry').set_text( + options.get('download_location', config["download_location"]) + ) + if options.get('move_completed_toggle', config["move_completed"]): + self.glade.get_widget('move_completed_toggle').set_active( + options.get('move_completed_toggle', False) + ) + self.glade.get_widget('move_completed_path_entry').set_text( + options.get('move_completed_path', config["move_completed_path"]) + ) + if options.get('copy_torrent_toggle', config["copy_torrent_file"]): + self.glade.get_widget('copy_torrent_toggle').set_active(True) + self.glade.get_widget('copy_torrent_entry').set_text( + options.get('copy_torrent', config["torrentfiles_location"]) + ) + + if options.get('delete_copy_torrent_toggle', config["del_copy_torrent_file"]): + self.glade.get_widget('delete_copy_torrent_toggle').set_active(True) + + if not options: + client.core.get_config().addCallback(on_core_config) + + def on_accounts(accounts, owner): + log.debug("Got Accounts") + selected_iter = None + for account in accounts: + iter = self.accounts.append() + self.accounts.set_value( + iter, 0, account['username'] + ) + if account['username'] == owner: + selected_iter = iter + self.glade.get_widget('OwnerCombobox').set_active_iter(selected_iter) + + def on_accounts_failure(failure): + log.debug("Failed to get accounts!!! %s", failure) + iter = self.accounts.append() + self.accounts.set_value(iter, 0, client.get_auth_user()) + self.glade.get_widget('OwnerCombobox').set_active(0) + self.glade.get_widget('OwnerCombobox').set_sensitive(False) + + def on_labels(labels): + log.debug("Got Labels: %s", labels) + for label in labels: + self.labels.set_value(self.labels.append(), 0, label) + label_widget = self.glade.get_widget('label') + label_widget.set_model(self.labels) + label_widget.set_text_column(0) + + def on_failure(failure): + log.exception(failure) + def on_get_enabled_plugins(result): if 'Label' in result: self.glade.get_widget('label_frame').show() + client.label.get_labels().addCallback(on_labels).addErrback(on_failure) else: self.glade.get_widget('label_frame').hide() self.glade.get_widget('label_toggle').set_active(False) client.core.get_enabled_plugins().addCallback(on_get_enabled_plugins) + if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN: + client.core.get_known_accounts().addCallback( + on_accounts, options.get('owner', client.get_auth_user()) + ).addErrback(on_accounts_failure) + else: + iter = self.accounts.append() + self.accounts.set_value(iter, 0, client.get_auth_user()) + self.glade.get_widget('OwnerCombobox').set_active(0) + self.glade.get_widget('OwnerCombobox').set_sensitive(False) def set_sensitive(self): - maintoggles = ['download_location', 'append_extension', 'move_completed', 'label', \ - 'max_download_speed', 'max_upload_speed', 'max_connections', \ - 'max_upload_slots', 'add_paused', 'auto_managed', 'stop_at_ratio', 'queue_to_top'] + maintoggles = ['download_location', 'append_extension', + 'move_completed', 'label', 'max_download_speed', + 'max_upload_speed', 'max_connections', + 'max_upload_slots', 'add_paused', 'auto_managed', + 'stop_at_ratio', 'queue_to_top', 'copy_torrent'] [self.on_toggle_toggled(self.glade.get_widget(x+'_toggle')) for x in maintoggles] def on_toggle_toggled(self, tb): @@ -139,6 +253,10 @@ class OptionsDialog(): self.glade.get_widget('download_location_entry').set_sensitive(isactive) elif toggle == 'append_extension': self.glade.get_widget('append_extension').set_sensitive(isactive) + elif toggle == 'copy_torrent': + self.glade.get_widget('copy_torrent_entry').set_sensitive(isactive) + self.glade.get_widget('copy_torrent_chooser').set_sensitive(isactive) + self.glade.get_widget('delete_copy_torrent_toggle').set_sensitive(isactive) elif toggle == 'move_completed': self.glade.get_widget('move_completed_path_chooser').set_sensitive(isactive) self.glade.get_widget('move_completed_path_entry').set_sensitive(isactive) @@ -170,23 +288,29 @@ class OptionsDialog(): self.glade.get_widget('remove_at_ratio').set_sensitive(isactive) def on_apply(self, Event=None): - client.autoadd.set_options(str(self.watchdir_id), self.generate_opts()).addCallbacks(self.on_added, self.on_error_show) + try: + options = self.generate_opts() + client.autoadd.set_options( + str(self.watchdir_id), options + ).addCallbacks(self.on_added, self.on_error_show) + except IncompatibleOption, err: + dialogs.ErrorDialog(_("Incompatible Option"), str(err), self.dialog).run() + def on_error_show(self, result): - self.glade.get_widget('error_label').set_text(result.value.exception_msg) - self.err_dialog = self.glade.get_widget('error_dialog') - self.err_dialog.set_transient_for(self.dialog) + d = dialogs.ErrorDialog(_("Error"), result.value.exception_msg, self.dialog) result.cleanFailure() - self.err_dialog.show() + d.run() def on_added(self, result): self.dialog.destroy() - def on_error_ok(self, Event=None): - self.err_dialog.hide() - def on_add(self, Event=None): - client.autoadd.add(self.generate_opts()).addCallbacks(self.on_added, self.on_error_show) + try: + options = self.generate_opts() + client.autoadd.add(options).addCallbacks(self.on_added, self.on_error_show) + except IncompatibleOption, err: + dialogs.ErrorDialog(_("Incompatible Option"), str(err), self.dialog).run() def on_cancel(self, Event=None): self.dialog.destroy() @@ -197,17 +321,30 @@ class OptionsDialog(): options['enabled'] = self.glade.get_widget('enabled').get_active() if client.is_localhost(): options['path'] = self.glade.get_widget('path_chooser').get_filename() - options['download_location'] = self.glade.get_widget('download_location_chooser').get_filename() - options['move_completed_path'] = self.glade.get_widget('move_completed_path_chooser').get_filename() + options['download_location'] = self.glade.get_widget( + 'download_location_chooser').get_filename() + options['move_completed_path'] = self.glade.get_widget( + 'move_completed_path_chooser').get_filename() + options['copy_torrent'] = self.glade.get_widget( + 'copy_torrent_chooser').get_filename() else: options['path'] = self.glade.get_widget('path_entry').get_text() - options['download_location'] = self.glade.get_widget('download_location_entry').get_text() - options['move_completed_path'] = self.glade.get_widget('move_completed_path_entry').get_text() - options['append_extension_toggle'] = self.glade.get_widget('append_extension_toggle').get_active() + options['download_location'] = self.glade.get_widget( + 'download_location_entry').get_text() + options['move_completed_path'] = self.glade.get_widget( + 'move_completed_path_entry').get_text() + options['copy_torrent'] = self.glade.get_widget( + 'copy_torrent_entry').get_text() + + options['label'] = self.glade.get_widget('label').child.get_text().lower() options['append_extension'] = self.glade.get_widget('append_extension').get_text() - options['download_location_toggle'] = self.glade.get_widget('download_location_toggle').get_active() - options['label'] = self.glade.get_widget('label').get_text().lower() - options['label_toggle'] = self.glade.get_widget('label_toggle').get_active() + options['owner'] = self.accounts[ + self.glade.get_widget('OwnerCombobox').get_active()][0] + + for key in ['append_extension_toggle', 'download_location_toggle', + 'label_toggle', 'copy_torrent_toggle', + 'delete_copy_torrent_toggle']: + options[key] = self.glade.get_widget(key).get_active() for id in self.spin_ids: options[id] = self.glade.get_widget(id).get_value() @@ -218,6 +355,10 @@ class OptionsDialog(): for id in self.chk_ids: options[id] = self.glade.get_widget(id).get_active() options[id+'_toggle'] = self.glade.get_widget(id+'_toggle').get_active() + + if options['copy_torrent_toggle'] and options['path'] == options['copy_torrent']: + raise IncompatibleOption(_("\"Watch Folder\" directory and \"Copy of .torrent" + " files to\" directory cannot be the same!")) return options @@ -232,9 +373,15 @@ class GtkUI(GtkPluginBase): }) self.opts_dialog = OptionsDialog() - component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) - component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) - client.register_event_handler("AutoaddOptionsChangedEvent", self.on_options_changed_event) + component.get("PluginManager").register_hook( + "on_apply_prefs", self.on_apply_prefs + ) + component.get("PluginManager").register_hook( + "on_show_prefs", self.on_show_prefs + ) + client.register_event_handler( + "AutoaddOptionsChangedEvent", self.on_options_changed_event + ) self.watchdirs = {} @@ -255,38 +402,55 @@ class GtkUI(GtkPluginBase): self.create_columns(self.treeView) sw.add(self.treeView) sw.show_all() - component.get("Preferences").add_page("AutoAdd", self.glade.get_widget("prefs_box")) + component.get("Preferences").add_page( + "AutoAdd", self.glade.get_widget("prefs_box") + ) self.on_show_prefs() def disable(self): component.get("Preferences").remove_page("AutoAdd") - component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_prefs) - component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs) + component.get("PluginManager").deregister_hook( + "on_apply_prefs", self.on_apply_prefs + ) + component.get("PluginManager").deregister_hook( + "on_show_prefs", self.on_show_prefs + ) def create_model(self): - - store = gtk.ListStore(str, bool, str) + store = gtk.ListStore(str, bool, str, str) for watchdir_id, watchdir in self.watchdirs.iteritems(): - store.append([watchdir_id, watchdir['enabled'], watchdir['path']]) + store.append([ + watchdir_id, watchdir['enabled'], + watchdir.get('owner', 'localclient'), watchdir['path'] + ]) return store def create_columns(self, treeView): rendererToggle = gtk.CellRendererToggle() - column = gtk.TreeViewColumn("On", rendererToggle, activatable=True, active=1) + column = gtk.TreeViewColumn( + _("Active"), rendererToggle, activatable=1, active=1 + ) column.set_sort_column_id(1) treeView.append_column(column) tt = gtk.Tooltip() - tt.set_text('Double-click to toggle') + tt.set_text(_('Double-click to toggle')) treeView.set_tooltip_cell(tt, None, None, rendererToggle) rendererText = gtk.CellRendererText() - column = gtk.TreeViewColumn("Path", rendererText, text=2) + column = gtk.TreeViewColumn(_("Owner"), rendererText, text=2) column.set_sort_column_id(2) treeView.append_column(column) tt2 = gtk.Tooltip() - tt2.set_text('Double-click to edit') - #treeView.set_tooltip_cell(tt2, None, column, None) + tt2.set_text(_('Double-click to edit')) + treeView.set_has_tooltip(True) + + rendererText = gtk.CellRendererText() + column = gtk.TreeViewColumn(_("Path"), rendererText, text=3) + column.set_sort_column_id(3) + treeView.append_column(column) + tt2 = gtk.Tooltip() + tt2.set_text(_('Double-click to edit')) treeView.set_has_tooltip(True) def load_watchdir_list(self): @@ -309,7 +473,7 @@ class GtkUI(GtkPluginBase): tree, tree_id = self.treeView.get_selection().get_selected() watchdir_id = str(self.store.get_value(tree_id, 0)) if watchdir_id: - if col and col.get_title() == 'On': + if col and col.get_title() == _("Active"): if self.watchdirs[watchdir_id]['enabled']: client.autoadd.disable_watchdir(watchdir_id) else: @@ -332,17 +496,21 @@ class GtkUI(GtkPluginBase): client.autoadd.set_options(watchdir_id, watchdir) def on_show_prefs(self): - client.autoadd.get_config().addCallback(self.cb_get_config) + client.autoadd.get_watchdirs().addCallback(self.cb_get_config) - def on_options_changed_event(self, event): - client.autoadd.get_config().addCallback(self.cb_get_config) + def on_options_changed_event(self): + client.autoadd.get_watchdirs().addCallback(self.cb_get_config) - def cb_get_config(self, config): + def cb_get_config(self, watchdirs): """callback for on show_prefs""" - self.watchdirs = config.get('watchdirs', {}) + log.trace("Got whatchdirs from core: %s", watchdirs) + self.watchdirs = watchdirs or {} self.store.clear() for watchdir_id, watchdir in self.watchdirs.iteritems(): - self.store.append([watchdir_id, watchdir['enabled'], watchdir['path']]) + self.store.append([ + watchdir_id, watchdir['enabled'], + watchdir.get('owner', 'localclient'), watchdir['path'] + ]) # Disable the remove and edit buttons, because nothing in the store is selected self.glade.get_widget('remove_button').set_sensitive(False) self.glade.get_widget('edit_button').set_sensitive(False) diff --git a/deluge/plugins/AutoAdd/setup.py b/deluge/plugins/AutoAdd/setup.py index 7763a9270..2ffe54898 100644 --- a/deluge/plugins/AutoAdd/setup.py +++ b/deluge/plugins/AutoAdd/setup.py @@ -2,6 +2,7 @@ # setup.py # # Copyright (C) 2009 GazpachoKing +# Copyright (C) 2011 Pedro Algarvio # # Basic plugin template created by: # Copyright (C) 2008 Martijn Voncken @@ -40,10 +41,10 @@ from setuptools import setup, find_packages __plugin_name__ = "AutoAdd" -__author__ = "Chase Sterling" -__author_email__ = "chase.sterling@gmail.com" +__author__ = "Chase Sterling, Pedro Algarvio" +__author_email__ = "chase.sterling@gmail.com, pedro@algarvio.me" __version__ = "1.02" -__url__ = "http://forum.deluge-torrent.org/viewtopic.php?f=9&t=26775" +__url__ = "http://dev.deluge-torrent.org/wiki/Plugins/AutoAdd" __license__ = "GPLv3" __description__ = "Monitors folders for .torrent files." __long_description__ = """""" diff --git a/deluge/plugins/Execute/deluge/plugins/execute/core.py b/deluge/plugins/Execute/deluge/plugins/execute/core.py index 1dda1c802..54362b1c7 100644 --- a/deluge/plugins/Execute/deluge/plugins/execute/core.py +++ b/deluge/plugins/Execute/deluge/plugins/execute/core.py @@ -64,19 +64,15 @@ class ExecuteCommandAddedEvent(DelugeEvent): """ Emitted when a new command is added. """ - __slots__ = ('command_id', 'event', 'command') def __init__(self, command_id, event, command): - self.command_id = command_id - self.event = event - self.command = command + self._args = [command_id, event, command] class ExecuteCommandRemovedEvent(DelugeEvent): """ Emitted when a command is removed. """ - __slots__ = ('command_id',) def __init__(self, command_id): - self.command_id = command_id + self._args = [command_id] class Core(CorePluginBase): def enable(self): @@ -86,17 +82,17 @@ class Core(CorePluginBase): # Go through the commands list and register event handlers for command in self.config["commands"]: - event_name = command[EXECUTE_EVENT] - if event_name in self.registered_events: + event = command[EXECUTE_EVENT] + if event in self.registered_events: continue - def create_event_handler(event_name): - def event_handler(event): - self.execute_commands(event.torrent_id, event_name) + def create_event_handler(event): + def event_handler(torrent_id): + self.execute_commands(torrent_id, event) return event_handler - event_handler = create_event_handler(event_name) - event_manager.register_event_handler(EVENT_MAP[event_name], event_handler) - self.registered_events[event_name] = event_handler + event_handler = create_event_handler(event) + event_manager.register_event_handler(EVENT_MAP[event], event_handler) + self.registered_events[event] = event_handler log.debug("Execute core plugin enabled!") diff --git a/deluge/plugins/Execute/deluge/plugins/execute/gtkui.py b/deluge/plugins/Execute/deluge/plugins/execute/gtkui.py index c1102a76e..3cd148a55 100644 --- a/deluge/plugins/Execute/deluge/plugins/execute/gtkui.py +++ b/deluge/plugins/Execute/deluge/plugins/execute/gtkui.py @@ -161,13 +161,13 @@ class ExecutePreferences(object): command = widget.get_text() client.execute.save_command(command_id, event, command) - def on_command_added_event(self, event): - log.debug("Adding command %s: %s", event.event, event.command) - self.add_command(event.command_id, event.event, event.command) + def on_command_added_event(self, command_id, event, command): + log.debug("Adding command %s: %s", event, command) + self.add_command(command_id, event, command) - def on_command_removed_event(self, event): - log.debug("Removing command %s", event.command_id) - self.remove_command(event.command_id) + def on_command_removed_event(self, command_id): + log.debug("Removing command %s", command_id) + self.remove_command(command_id) class GtkUI(GtkPluginBase): diff --git a/deluge/plugins/Extractor/deluge/plugins/extractor/core.py b/deluge/plugins/Extractor/deluge/plugins/extractor/core.py index d8d5d8fd7..03eeda1e7 100644 --- a/deluge/plugins/Extractor/deluge/plugins/extractor/core.py +++ b/deluge/plugins/Extractor/deluge/plugins/extractor/core.py @@ -77,14 +77,14 @@ class Core(CorePluginBase): def update(self): pass - def _on_torrent_finished(self, event): + def _on_torrent_finished(self, torrent_id): """ This is called when a torrent finishes. We need to check to see if there are any files to extract. """ # Get the save path - save_path = component.get("TorrentManager")[event.torrent_id].get_status(["save_path"])["save_path"] - files = component.get("TorrentManager")[event.torrent_id].get_files() + save_path = component.get("TorrentManager")[torrent_id].get_status(["save_path"])["save_path"] + files = component.get("TorrentManager")[torrent_id].get_files() for f in files: ext = os.path.splitext(f["path"]) if ext[1] in (".gz", ".bz2", ".lzma"): @@ -100,22 +100,22 @@ class Core(CorePluginBase): # Now that we have the cmd, lets run it to extract the files fp = os.path.join(save_path, f["path"]) - + # Get the destination path dest = self.config["extract_path"] if self.config["use_name_folder"]: - name = component.get("TorrentManager")[event.torrent_id].get_status(["name"])["name"] + name = component.get("TorrentManager")[torrent_id].get_status(["name"])["name"] dest = os.path.join(dest, name) - # Create the destination folder if it doesn't exist + # Create the destination folder if it doesn't exist if not os.path.exists(dest): try: os.makedirs(dest) except Exception, e: log.error("Error creating destination folder: %s", e) return - - log.debug("Extracting to %s", dest) + + log.debug("Extracting to %s", dest) def on_extract_success(result, torrent_id): # XXX: Emit an event log.debug("Extract was successful for %s", torrent_id) @@ -126,8 +126,8 @@ class Core(CorePluginBase): # Run the command and add some callbacks d = getProcessValue(cmd[0], cmd[1].split() + [str(fp)], {}, str(dest)) - d.addCallback(on_extract_success, event.torrent_id) - d.addErrback(on_extract_failed, event.torrent_id) + d.addCallback(on_extract_success, torrent_id) + d.addErrback(on_extract_failed, torrent_id) @export def set_config(self, config): diff --git a/deluge/plugins/FreeSpace/deluge/plugins/freespace/core.py b/deluge/plugins/FreeSpace/deluge/plugins/freespace/core.py index 0e4846a3d..eb90ecb68 100644 --- a/deluge/plugins/FreeSpace/deluge/plugins/freespace/core.py +++ b/deluge/plugins/FreeSpace/deluge/plugins/freespace/core.py @@ -55,14 +55,12 @@ class LowDiskSpaceEvent(DelugeEvent): """Triggered when the available space for a specific path is getting too low. """ - __slots__ = ('percents_dict',) - def __init__(self, percents_dict): """ :param percents: dictionary of path keys with their respecive occupation percentages. """ - self.percents_dict = percents_dict + self._args = [percents_dict] DEFAULT_PREFS = { "enabled": False, @@ -174,25 +172,25 @@ class Core(CorePluginBase): free_percent = free_blocks * 100 / total_blocks return free_percent - def __custom_email_notification(self, event): + def __custom_email_notification(self, ocupied_percents): subject = _("Low Disk Space Warning") message = _("You're running low on disk space:\n") - for path, ocupied_percent in event.percents_dict.iteritems(): + for path, ocupied_percent in ocupied_percents.iteritems(): message += _(' %s%% ocupation in %s\n') % (ocupied_percent, path) # "\"%s\"%% space occupation on %s") % (ocupied_percent, path) return subject, message - def __on_plugin_enabled(self, event): - if event.plugin_name == 'Notifications': + def __on_plugin_enabled(self, plugin_name): + if plugin_name == 'Notifications': component.get("CorePlugin.Notifications"). \ register_custom_email_notification( "LowDiskSpaceEvent", self.__custom_email_notification ) - def __on_plugin_disabled(self, event): - if event.plugin_name == 'Notifications': + def __on_plugin_disabled(self, plugin_name): + if plugin_name == 'Notifications': component.get("CorePlugin.Notifications"). \ deregister_custom_email_notification("LowDiskSpaceEvent") diff --git a/deluge/plugins/FreeSpace/deluge/plugins/freespace/gtkui.py b/deluge/plugins/FreeSpace/deluge/plugins/freespace/gtkui.py index 83d94b3b2..21405ccda 100644 --- a/deluge/plugins/FreeSpace/deluge/plugins/freespace/gtkui.py +++ b/deluge/plugins/FreeSpace/deluge/plugins/freespace/gtkui.py @@ -134,22 +134,22 @@ class GtkUI(GtkPluginBase): self.glade.get_widget('enabled').set_active(config['enabled']) self.glade.get_widget('percent').set_value(config['percent']) - def __custom_popup_notification(self, event): + def __custom_popup_notification(self, ocupied_percents): title = _("Low Free Space") message = '' - for path, percent in event.percents_dict.iteritems(): + for path, percent in ocupied_percents.iteritems(): message += '%s%% %s\n' % (percent, path) message += '\n' return title, message - def __custom_blink_notification(self, event): + def __custom_blink_notification(self, ocupied_percents): return True # Yes, do blink - def __custom_sound_notification(self, event): + def __custom_sound_notification(self, ocupied_percents): return '' # Use default sound - def __on_plugin_enabled(self, event): - if event.plugin_name == 'Notifications': + def __on_plugin_enabled(self, plugin_name): + if plugin_name == 'Notifications': notifications = component.get("GtkPlugin.Notifications") notifications.register_custom_popup_notification( "LowDiskSpaceEvent", self.__custom_popup_notification @@ -161,7 +161,7 @@ class GtkUI(GtkPluginBase): "LowDiskSpaceEvent", self.__custom_sound_notification ) - def __on_plugin_disabled(self, event): + def __on_plugin_disabled(self, plugin_name): pass # if plugin_name == 'Notifications': # notifications = component.get("GtkPlugin.Notifications") diff --git a/deluge/plugins/Label/deluge/plugins/label/core.py b/deluge/plugins/Label/deluge/plugins/label/core.py index cf6c7b970..5c76f5d19 100644 --- a/deluge/plugins/Label/deluge/plugins/label/core.py +++ b/deluge/plugins/Label/deluge/plugins/label/core.py @@ -133,20 +133,20 @@ class Core(CorePluginBase): return dict( [(label, 0) for label in self.labels.keys()]) ## Plugin hooks ## - def post_torrent_add(self, event): + def post_torrent_add(self, torrent_id, from_state): log.debug("post_torrent_add") - torrent = self.torrents[event.torrent_id] + torrent = self.torrents[torrent_id] for label_id, options in self.labels.iteritems(): if options["auto_add"]: if self._has_auto_match(torrent, options): - self.set_torrent(event.torrent_id, label_id) + self.set_torrent(torrent_id, label_id) return - def post_torrent_remove(self, event): + def post_torrent_remove(self, torrent_id): log.debug("post_torrent_remove") - if event.torrent_id in self.torrent_labels: - del self.torrent_labels[event.torrent_id] + if torrent_id in self.torrent_labels: + del self.torrent_labels[torrent_id] ## Utils ## def clean_config(self): diff --git a/deluge/plugins/Label/deluge/plugins/label/data/label.js b/deluge/plugins/Label/deluge/plugins/label/data/label.js index 57d0dda0d..8ae0584bc 100644 --- a/deluge/plugins/Label/deluge/plugins/label/data/label.js +++ b/deluge/plugins/Label/deluge/plugins/label/data/label.js @@ -330,6 +330,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, { onOkClick: function() { var values = this.form.getForm().getFieldValues(); + values['auto_add_trackers'] = values['auto_add_trackers'].split('\n'); deluge.client.label.set_options(this.label, values); this.hide(); }, diff --git a/deluge/plugins/Label/setup.py b/deluge/plugins/Label/setup.py index ef710fc25..65d36bf53 100644 --- a/deluge/plugins/Label/setup.py +++ b/deluge/plugins/Label/setup.py @@ -39,15 +39,11 @@ __author_email__ = "mvoncken@gmail.com" __version__ = "0.1" __url__ = "http://deluge-torrent.org" __license__ = "GPLv3" -__description__ = "Label plugin." +__description__ = "Allows labels to be assigned to torrents" __long_description__ = """ -Label plugin. - -Offers filters on state,tracker and keyword. -adds a tracker column. - -future: Real labels. +Allows labels to be assigned to torrents +Also offers filters on state, tracker and keywords """ __pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]} diff --git a/deluge/plugins/Notifications/deluge/plugins/notifications/core.py b/deluge/plugins/Notifications/deluge/plugins/notifications/core.py index 2377d595e..5b01ca8b4 100644 --- a/deluge/plugins/Notifications/deluge/plugins/notifications/core.py +++ b/deluge/plugins/Notifications/deluge/plugins/notifications/core.py @@ -38,6 +38,7 @@ # import smtplib +from email.utils import formatdate from twisted.internet import defer, threads from deluge import component from deluge.event import known_events @@ -118,11 +119,14 @@ class CoreNotifications(CustomNotifications): From: %(smtp_from)s To: %(smtp_recipients)s Subject: %(subject)s +Date: %(date)s """ % {'smtp_from': self.config['smtp_from'], 'subject': subject, - 'smtp_recipients': to_addrs} + 'smtp_recipients': to_addrs, + 'date': formatdate() + } message = '\r\n'.join((headers + message).splitlines()) @@ -188,9 +192,9 @@ Subject: %(subject)s return _("Notification email sent.") - def _on_torrent_finished_event(self, event): + def _on_torrent_finished_event(self, torrent_id): log.debug("Handler for TorrentFinishedEvent called for CORE") - torrent = component.get("TorrentManager")[event.torrent_id] + torrent = component.get("TorrentManager")[torrent_id] torrent_status = torrent.get_status({}) # Email subject = _("Finished Torrent \"%(name)s\"") % torrent_status diff --git a/deluge/plugins/Notifications/deluge/plugins/notifications/data/config.glade b/deluge/plugins/Notifications/deluge/plugins/notifications/data/config.glade index f1603505d..4da1de6bd 100644 --- a/deluge/plugins/Notifications/deluge/plugins/notifications/data/config.glade +++ b/deluge/plugins/Notifications/deluge/plugins/notifications/data/config.glade @@ -213,7 +213,7 @@ 65535 5 - 25 1 100 1 10 0 + 25 1 65535 0 10 0 1 True True @@ -292,6 +292,7 @@ True automatic automatic + in True diff --git a/deluge/plugins/Notifications/setup.py b/deluge/plugins/Notifications/setup.py index 2e4797364..d8c566153 100755 --- a/deluge/plugins/Notifications/setup.py +++ b/deluge/plugins/Notifications/setup.py @@ -46,10 +46,13 @@ __version__ = "0.1" __url__ = "http://dev.deluge-torrent.org/" __license__ = "GPLv3" __description__ = "Plugin which provides notifications to Deluge." -__long_description__ = __description__ + """\ - Email, Popup, Blink and Sound notifications are supported. -The plugin also allows other plugins to make use of itself for their own custom -notifications. +__long_description__ = """ +Plugin which provides notifications to Deluge + +Email, Popup, Blink and Sound notifications + +The plugin also allows other plugins to make + use of itself for their own custom notifications """ __pkg_data__ = {"deluge.plugins."+__plugin_name__.lower(): ["template/*", "data/*"]} diff --git a/deluge/plugins/Scheduler/deluge/plugins/scheduler/core.py b/deluge/plugins/Scheduler/deluge/plugins/scheduler/core.py index f810fff71..c4b13d492 100644 --- a/deluge/plugins/Scheduler/deluge/plugins/scheduler/core.py +++ b/deluge/plugins/Scheduler/deluge/plugins/scheduler/core.py @@ -66,7 +66,7 @@ STATES = { CONTROLLED_SETTINGS = [ "max_download_speed", - "max_download_speed", + "max_upload_speed", "max_active_limit", "max_active_downloading", "max_active_seeding" @@ -76,12 +76,11 @@ class SchedulerEvent(DelugeEvent): """ Emitted when a schedule state changes. """ - __slots__ = ('colour',) def __init__(self, colour): """ :param colour: str, the current scheduler state """ - self.colour = colour + self._args = [colour] class Core(CorePluginBase): def enable(self): @@ -120,8 +119,8 @@ class Core(CorePluginBase): pass - def on_config_value_changed(self, event): - if event.key in CONTROLLED_SETTINGS: + def on_config_value_changed(self, key, value): + if key in CONTROLLED_SETTINGS: self.do_schedule(False) def __apply_set_functions(self): diff --git a/deluge/plugins/Scheduler/deluge/plugins/scheduler/gtkui.py b/deluge/plugins/Scheduler/deluge/plugins/scheduler/gtkui.py index 2bf73cbd3..739f29ab5 100644 --- a/deluge/plugins/Scheduler/deluge/plugins/scheduler/gtkui.py +++ b/deluge/plugins/Scheduler/deluge/plugins/scheduler/gtkui.py @@ -203,9 +203,9 @@ class GtkUI(GtkPluginBase): client.scheduler.get_config().addCallback(on_get_config) - def on_scheduler_event(self, event): + def on_scheduler_event(self, state): def on_state_deferred(s): - self.status_item.set_image_from_file(get_resource(event.colour.lower() + ".png")) + self.status_item.set_image_from_file(get_resource(state.lower() + ".png")) self.state_deferred.addCallback(on_state_deferred) diff --git a/deluge/rencode.py b/deluge/rencode.py index 2b32c001b..1172df2e5 100644 --- a/deluge/rencode.py +++ b/deluge/rencode.py @@ -63,7 +63,6 @@ __all__ = ['dumps', 'loads'] # import struct -import string from threading import Lock # Default number of bits for serialized floats, either 32 or 64 (also a parameter for dumps()). diff --git a/deluge/tests/common.py b/deluge/tests/common.py index 4c9f1769e..c24cd4c60 100644 --- a/deluge/tests/common.py +++ b/deluge/tests/common.py @@ -1,4 +1,8 @@ +import os +import sys +import time import tempfile +from subprocess import Popen, PIPE import deluge.configmanager import deluge.log @@ -10,6 +14,9 @@ def set_tmp_config_dir(): deluge.configmanager.set_config_dir(config_directory) return config_directory +def rpath(*args): + return os.path.join(os.path.dirname(__file__), *args) + import gettext import locale import pkg_resources @@ -26,3 +33,38 @@ try: gettext.install("deluge", pkg_resources.resource_filename("deluge", "i18n")) except Exception, e: print e + +def start_core(): + CWD = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + DAEMON_SCRIPT = """ +import sys +import deluge.main + +sys.argv.extend(['-d', '-c', '%s', '-L', 'info']) + +deluge.main.start_daemon() +""" + config_directory = set_tmp_config_dir() + fp = tempfile.TemporaryFile() + fp.write(DAEMON_SCRIPT % config_directory) + fp.seek(0) + + core = Popen([sys.executable], cwd=CWD, stdin=fp, stdout=PIPE, stderr=PIPE) + while True: + line = core.stderr.readline() + if "Factory starting on 58846" in line: + time.sleep(0.3) # Slight pause just incase + break + elif "Couldn't listen on localhost:58846" in line: + raise SystemExit("Could not start deluge test client. %s" % line) + elif 'Traceback' in line: + raise SystemExit( + "Failed to start core daemon. Do \"\"\" %s \"\"\" to see what's " + "happening" % + "python -c \"import sys; import tempfile; import deluge.main; " + "import deluge.configmanager; config_directory = tempfile.mkdtemp(); " + "deluge.configmanager.set_config_dir(config_directory); " + "sys.argv.extend(['-d', '-c', config_directory, '-L', 'info']); " + "deluge.main.start_daemon()\"" + ) + return core diff --git a/deluge/tests/test_authmanager.py b/deluge/tests/test_authmanager.py index 617984664..08bbab662 100644 --- a/deluge/tests/test_authmanager.py +++ b/deluge/tests/test_authmanager.py @@ -2,7 +2,7 @@ from twisted.trial import unittest import common -from deluge.core.authmanager import AuthManager +from deluge.core.authmanager import AuthManager, AUTH_LEVEL_ADMIN class AuthManagerTestCase(unittest.TestCase): def setUp(self): @@ -11,4 +11,7 @@ class AuthManagerTestCase(unittest.TestCase): def test_authorize(self): from deluge.ui import common - self.assertEquals(self.auth.authorize(*common.get_localhost_auth()), 10) + self.assertEquals( + self.auth.authorize(*common.get_localhost_auth()), + AUTH_LEVEL_ADMIN + ) diff --git a/deluge/tests/test_client.py b/deluge/tests/test_client.py index 79ff38376..0e9bf5c1b 100644 --- a/deluge/tests/test_client.py +++ b/deluge/tests/test_client.py @@ -1,27 +1,152 @@ -import tempfile -import os -import signal import common +from twisted.internet import defer from twisted.trial import unittest -from deluge.ui.client import client +from deluge import error +from deluge.core.authmanager import AUTH_LEVEL_ADMIN +from deluge.ui.client import client, Client, DaemonSSLProxy -# Start a daemon to test with and wait a couple seconds to make sure it's started -config_directory = common.set_tmp_config_dir() -client.start_daemon(58847, config_directory) -import time -time.sleep(2) +class NoVersionSendingDaemonSSLProxy(DaemonSSLProxy): + def authenticate(self, username, password): + self.login_deferred = defer.Deferred() + d = self.call("daemon.login", username, password) + d.addCallback(self.__on_login, username) + d.addErrback(self.__on_login_fail) + return self.login_deferred + + def __on_login(self, result, username): + self.login_deferred.callback(result) + + def __on_login_fail(self, result): + self.login_deferred.errback(result) + +class NoVersionSendingClient(Client): + + def connect(self, host="127.0.0.1", port=58846, username="", password="", + skip_authentication=False): + self._daemon_proxy = NoVersionSendingDaemonSSLProxy() + self._daemon_proxy.set_disconnect_callback(self.__on_disconnect) + + d = self._daemon_proxy.connect(host, port) + + def on_connect_fail(reason): + self.disconnect() + return reason + + def on_authenticate(result, daemon_info): + return result + + def on_authenticate_fail(reason): + return reason + + def on_connected(daemon_version): + return daemon_version + + def authenticate(daemon_version, username, password): + d = self._daemon_proxy.authenticate(username, password) + d.addCallback(on_authenticate, daemon_version) + d.addErrback(on_authenticate_fail) + return d + + d.addCallback(on_connected) + d.addErrback(on_connect_fail) + if not skip_authentication: + d.addCallback(authenticate, username, password) + return d + + def __on_disconnect(self): + if self.disconnect_callback: + self.disconnect_callback() class ClientTestCase(unittest.TestCase): + + def setUp(self): + self.core = common.start_core() + + def tearDown(self): + self.core.terminate() + def test_connect_no_credentials(self): - d = client.connect("localhost", 58847) - d.addCallback(self.assertEquals, 10) + + d = client.connect("localhost", 58846) + + def on_failure(failure): + self.assertEqual( + failure.trap(error.AuthenticationRequired), + error.AuthenticationRequired + ) + self.addCleanup(client.disconnect) + + d.addErrback(on_failure) + return d + + def test_connect_localclient(self): + from deluge.ui import common + username, password = common.get_localhost_auth() + d = client.connect( + "localhost", 58846, username=username, password=password + ) def on_connect(result): + self.assertEqual(client.get_auth_level(), AUTH_LEVEL_ADMIN) self.addCleanup(client.disconnect) return result + d.addCallback(on_connect) return d + + def test_connect_bad_password(self): + from deluge.ui import common + username, password = common.get_localhost_auth() + d = client.connect( + "localhost", 58846, username=username, password=password+'1' + ) + + def on_failure(failure): + self.assertEqual( + failure.trap(error.BadLoginError), + error.BadLoginError + ) + self.addCleanup(client.disconnect) + + d.addErrback(on_failure) + return d + + def test_connect_without_password(self): + from deluge.ui import common + username, password = common.get_localhost_auth() + d = client.connect( + "localhost", 58846, username=username + ) + + def on_failure(failure): + self.assertEqual( + failure.trap(error.AuthenticationRequired), + error.AuthenticationRequired + ) + self.assertEqual(failure.value.username, username) + self.addCleanup(client.disconnect) + + d.addErrback(on_failure) + return d + + def test_connect_without_sending_client_version_fails(self): + from deluge.ui import common + username, password = common.get_localhost_auth() + no_version_sending_client = NoVersionSendingClient() + d = no_version_sending_client.connect( + "localhost", 58846, username=username, password=password + ) + + def on_failure(failure): + self.assertEqual( + failure.trap(error.IncompatibleClient), + error.IncompatibleClient + ) + self.addCleanup(no_version_sending_client.disconnect) + + d.addErrback(on_failure) + return d diff --git a/deluge/tests/test_core.py b/deluge/tests/test_core.py index 1e74f30cf..751851325 100644 --- a/deluge/tests/test_core.py +++ b/deluge/tests/test_core.py @@ -1,5 +1,10 @@ from twisted.trial import unittest +from twisted.internet import reactor from twisted.python.failure import Failure +from twisted.web.http import FORBIDDEN +from twisted.web.resource import Resource +from twisted.web.server import Site +from twisted.web.static import File try: from hashlib import sha1 as sha @@ -8,19 +13,65 @@ except ImportError: import os import common +import warnings +rpath = common.rpath from deluge.core.rpcserver import RPCServer from deluge.core.core import Core +warnings.filterwarnings("ignore", category=RuntimeWarning) +from deluge.ui.web.common import compress +warnings.resetwarnings() import deluge.component as component import deluge.error +class TestCookieResource(Resource): + + def render(self, request): + if request.getCookie("password") != "deluge": + request.setResponseCode(FORBIDDEN) + return + + request.setHeader("Content-Type", "application/x-bittorrent") + return open(rpath("ubuntu-9.04-desktop-i386.iso.torrent")).read() + +class TestPartialDownload(Resource): + + def render(self, request): + data = open(rpath("ubuntu-9.04-desktop-i386.iso.torrent")).read() + request.setHeader("Content-Type", len(data)) + request.setHeader("Content-Type", "application/x-bittorrent") + if request.requestHeaders.hasHeader("accept-encoding"): + return compress(data, request) + return data + +class TestRedirectResource(Resource): + + def render(self, request): + request.redirect("/ubuntu-9.04-desktop-i386.iso.torrent") + return "" + +class TopLevelResource(Resource): + + addSlash = True + + def __init__(self): + Resource.__init__(self) + self.putChild("cookie", TestCookieResource()) + self.putChild("partial", TestPartialDownload()) + self.putChild("redirect", TestRedirectResource()) + self.putChild("ubuntu-9.04-desktop-i386.iso.torrent", File(common.rpath("ubuntu-9.04-desktop-i386.iso.torrent"))) + class CoreTestCase(unittest.TestCase): def setUp(self): common.set_tmp_config_dir() self.rpcserver = RPCServer(listen=False) self.core = Core() - d = component.start() - return d + return component.start().addCallback(self.startWebserver) + + def startWebserver(self, result): + self.website = Site(TopLevelResource()) + self.webserver = reactor.listenTCP(51242, self.website) + return result def tearDown(self): @@ -28,6 +79,7 @@ class CoreTestCase(unittest.TestCase): component._ComponentRegistry.components = {} del self.rpcserver del self.core + return self.webserver.stopListening() return component.shutdown().addCallback(on_shutdown) @@ -44,7 +96,7 @@ class CoreTestCase(unittest.TestCase): self.assertEquals(torrent_id, info_hash) def test_add_torrent_url(self): - url = "http://deluge-torrent.org/ubuntu-9.04-desktop-i386.iso.torrent" + url = "http://localhost:51242/ubuntu-9.04-desktop-i386.iso.torrent" options = {} info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00" @@ -53,7 +105,7 @@ class CoreTestCase(unittest.TestCase): return d def test_add_torrent_url_with_cookie(self): - url = "http://deluge-torrent.org/test_torrent.php" + url = "http://localhost:51242/cookie" options = {} headers = { "Cookie" : "password=deluge" } info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00" @@ -66,6 +118,26 @@ class CoreTestCase(unittest.TestCase): return d + def test_add_torrent_url_with_redirect(self): + url = "http://localhost:51242/redirect" + options = {} + info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00" + + d = self.core.add_torrent_url(url, options) + d.addCallback(self.assertEquals, info_hash) + + return d + + def test_add_torrent_url_with_partial_download(self): + url = "http://localhost:51242/partial" + options = {} + info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00" + + d = self.core.add_torrent_url(url, options) + d.addCallback(self.assertEquals, info_hash) + + return d + def test_add_magnet(self): info_hash = "60d5d82328b4547511fdeac9bf4d0112daa0ce00" import deluge.common diff --git a/deluge/tests/test_httpdownloader.py b/deluge/tests/test_httpdownloader.py index 54b4c053d..76d89cd6e 100644 --- a/deluge/tests/test_httpdownloader.py +++ b/deluge/tests/test_httpdownloader.py @@ -1,17 +1,89 @@ +import os +import warnings + from twisted.trial import unittest +from twisted.internet import reactor from twisted.python.failure import Failure +from twisted.web.http import FORBIDDEN, NOT_MODIFIED +from twisted.web.resource import Resource, ForbiddenResource +from twisted.web.server import Site from deluge.httpdownloader import download_file from deluge.log import setupLogger +warnings.filterwarnings("ignore", category=RuntimeWarning) +from deluge.ui.web.common import compress +warnings.resetwarnings() + from email.utils import formatdate +import common +rpath = common.rpath + +class TestRedirectResource(Resource): + + def render(self, request): + request.redirect("http://localhost:51242/") + +class TestRenameResource(Resource): + + def render(self, request): + filename = request.args.get("filename", ["renamed_file"])[0] + request.setHeader("Content-Type", "text/plain") + request.setHeader("Content-Disposition", "attachment; filename=" + + filename) + return "This file should be called " + filename + +class TestCookieResource(Resource): + + def render(self, request): + request.setHeader("Content-Type", "text/plain") + if request.getCookie("password") is None: + return "Password cookie not set!" + + if request.getCookie("password") == "deluge": + return "COOKIE MONSTER!" + + return request.getCookie("password") + +class TestGzipResource(Resource): + + def render(self, request): + message = request.args.get("msg", ["EFFICIENCY!"])[0] + request.setHeader("Content-Type", "text/plain") + return compress(message, request) + +class TopLevelResource(Resource): + + addSlash = True + + def __init__(self): + Resource.__init__(self) + self.putChild("cookie", TestCookieResource()) + self.putChild("gzip", TestGzipResource()) + self.putChild("redirect", TestRedirectResource()) + self.putChild("rename", TestRenameResource()) + + def getChild(self, path, request): + if path == "": + return self + else: + return Resource.getChild(self, path, request) + + def render(self, request): + if request.getHeader("If-Modified-Since"): + request.setResponseCode(NOT_MODIFIED) + return "

Deluge HTTP Downloader tests webserver here

" + class DownloadFileTestCase(unittest.TestCase): + def setUp(self): setupLogger("warning", "log_file") + self.website = Site(TopLevelResource()) + self.webserver = reactor.listenTCP(51242, self.website) def tearDown(self): - pass + return self.webserver.stopListening() def assertContains(self, filename, contents): f = open(filename) @@ -34,19 +106,19 @@ class DownloadFileTestCase(unittest.TestCase): return filename def test_download(self): - d = download_file("http://deluge-torrent.org", "index.html") + d = download_file("http://localhost:51242/", "index.html") d.addCallback(self.assertEqual, "index.html") return d def test_download_without_required_cookies(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=cookie" + url = "http://localhost:51242/cookie" d = download_file(url, "none") d.addCallback(self.fail) d.addErrback(self.assertIsInstance, Failure) return d def test_download_with_required_cookies(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=cookie" + url = "http://localhost:51242/cookie" cookie = { "cookie" : "password=deluge" } d = download_file(url, "monster", headers=cookie) d.addCallback(self.assertEqual, "monster") @@ -54,61 +126,61 @@ class DownloadFileTestCase(unittest.TestCase): return d def test_download_with_rename(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=renamed" + url = "http://localhost:51242/rename?filename=renamed" d = download_file(url, "original") d.addCallback(self.assertEqual, "renamed") d.addCallback(self.assertContains, "This file should be called renamed") return d def test_download_with_rename_fail(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=renamed" + url = "http://localhost:51242/rename?filename=renamed" d = download_file(url, "original") d.addCallback(self.assertEqual, "original") d.addCallback(self.assertContains, "This file should be called renamed") return d def test_download_with_rename_sanitised(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=/etc/passwd" + url = "http://localhost:51242/rename?filename=/etc/passwd" d = download_file(url, "original") d.addCallback(self.assertEqual, "passwd") d.addCallback(self.assertContains, "This file should be called /etc/passwd") return d def test_download_with_rename_prevented(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=rename&filename=spam" + url = "http://localhost:51242/rename?filename=spam" d = download_file(url, "forced", force_filename=True) d.addCallback(self.assertEqual, "forced") d.addCallback(self.assertContains, "This file should be called spam") return d def test_download_with_gzip_encoding(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=gzip&msg=success" + url = "http://localhost:51242/gzip?msg=success" d = download_file(url, "gzip_encoded") d.addCallback(self.assertContains, "success") return d def test_download_with_gzip_encoding_disabled(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=gzip&msg=fail" + url = "http://localhost:51242/gzip?msg=fail" d = download_file(url, "gzip_encoded", allow_compression=False) d.addCallback(self.failIfContains, "fail") return d def test_page_redirect(self): - url = "http://damoxc.net/deluge/httpdownloader.php?test=redirect" + url = 'http://localhost:51242/redirect' d = download_file(url, "none") d.addCallback(self.fail) d.addErrback(self.assertIsInstance, Failure) return d def test_page_not_found(self): - d = download_file("http://does.not.exist", "none") + d = download_file("http://localhost:51242/page/not/found", "none") d.addCallback(self.fail) d.addErrback(self.assertIsInstance, Failure) return d def test_page_not_modified(self): headers = { 'If-Modified-Since' : formatdate(usegmt=True) } - d = download_file("http://deluge-torrent.org", "index.html", headers=headers) + d = download_file("http://localhost:51242/", "index.html", headers=headers) d.addCallback(self.fail) d.addErrback(self.assertIsInstance, Failure) return d diff --git a/deluge/tests/ubuntu-9.04-desktop-i386.iso.torrent b/deluge/tests/ubuntu-9.04-desktop-i386.iso.torrent new file mode 100644 index 000000000..b55c9aec1 Binary files /dev/null and b/deluge/tests/ubuntu-9.04-desktop-i386.iso.torrent differ diff --git a/deluge/ui/client.py b/deluge/ui/client.py index 9852b2998..31fb305a3 100644 --- a/deluge/ui/client.py +++ b/deluge/ui/client.py @@ -2,6 +2,7 @@ # client.py # # Copyright (C) 2009 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -44,7 +45,7 @@ except ImportError: import zlib import deluge.common -import deluge.component as component +from deluge import error from deluge.event import known_events if deluge.common.windows_check(): @@ -61,18 +62,6 @@ log = logging.getLogger(__name__) def format_kwargs(kwargs): return ", ".join([key + "=" + str(value) for key, value in kwargs.items()]) -class DelugeRPCError(object): - """ - This object is passed to errback handlers in the event of a RPCError from the - daemon. - """ - def __init__(self, method, args, kwargs, exception_type, exception_msg, traceback): - self.method = method - self.args = args - self.kwargs = kwargs - self.exception_type = exception_type - self.exception_msg = exception_msg - self.traceback = traceback class DelugeRPCRequest(object): """ @@ -108,12 +97,15 @@ class DelugeRPCRequest(object): :returns: a properly formated RPCRequest """ - if self.request_id is None or self.method is None or self.args is None or self.kwargs is None: - raise TypeError("You must set the properties of this object before calling format_message!") + if self.request_id is None or self.method is None or self.args is None \ + or self.kwargs is None: + raise TypeError("You must set the properties of this object " + "before calling format_message!") return (self.request_id, self.method, self.args, self.kwargs) class DelugeRPCProtocol(Protocol): + def connectionMade(self): self.__rpc_requests = {} self.__buffer = None @@ -160,20 +152,20 @@ class DelugeRPCProtocol(Protocol): log.debug("Received invalid message: type is not tuple") return if len(request) < 3: - log.debug("Received invalid message: number of items in response is %s", len(3)) + log.debug("Received invalid message: number of items in " + "response is %s", len(3)) return message_type = request[0] if message_type == RPC_EVENT: - event_name = request[1] + event = request[1] #log.debug("Received RPCEvent: %s", event) # A RPCEvent was received from the daemon so run any handlers # associated with it. - if event_name in self.factory.event_handlers: - event = known_events[event_name](*request[2]) - for handler in self.factory.event_handlers[event_name]: - reactor.callLater(0, handler, event.copy()) + if event in self.factory.event_handlers: + for handler in self.factory.event_handlers[event]: + reactor.callLater(0, handler, *request[2]) continue request_id = request[1] @@ -186,12 +178,35 @@ class DelugeRPCProtocol(Protocol): # Run the callbacks registered with this Deferred object d.callback(request[2]) elif message_type == RPC_ERROR: - # Create the DelugeRPCError to pass to the errback - r = self.__rpc_requests[request_id] - e = DelugeRPCError(r.method, r.args, r.kwargs, request[2][0], request[2][1], request[2][2]) - # Run the errbacks registered with this Deferred object - d.errback(e) + # Recreate exception and errback'it + exception_cls = getattr(error, request[2]) + exception = exception_cls(*request[3], **request[4]) + # Ideally we would chain the deferreds instead of instance + # checking just to log them. But, that would mean that any + # errback on the fist deferred should returns it's failure + # so it could pass back to the 2nd deferred on the chain. But, + # that does not always happen. + # So, just do some instance checking and just log rpc error at + # diferent levels. + r = self.__rpc_requests[request_id] + msg = "RPCError Message Received!" + msg += "\n" + "-" * 80 + msg += "\n" + "RPCRequest: " + r.__repr__() + msg += "\n" + "-" * 80 + msg += "\n" + request[5] + "\n" + request[2] + ": " + msg += str(exception) + msg += "\n" + "-" * 80 + + if not isinstance(exception, error._ClientSideRecreateError): + # Let's log these as errors + log.error(msg) + else: + # The rest just get's logged in debug level, just to log + # what's happending + log.debug(msg) + + d.errback(exception) del self.__rpc_requests[request_id] def send_request(self, request): @@ -222,14 +237,17 @@ class DelugeRPCClientFactory(ClientFactory): self.bytes_sent = 0 def startedConnecting(self, connector): - log.info("Connecting to daemon at %s:%s..", connector.host, connector.port) + log.info("Connecting to daemon at \"%s:%s\"...", + connector.host, connector.port) def clientConnectionFailed(self, connector, reason): - log.warning("Connection to daemon at %s:%s failed: %s", connector.host, connector.port, reason.value) + log.warning("Connection to daemon at \"%s:%s\" failed: %s", + connector.host, connector.port, reason.value) self.daemon.connect_deferred.errback(reason) def clientConnectionLost(self, connector, reason): - log.info("Connection lost to daemon at %s:%s reason: %s", connector.host, connector.port, reason.value) + log.info("Connection lost to daemon at \"%s:%s\" reason: %s", + connector.host, connector.port, reason.value) self.daemon.host = None self.daemon.port = None self.daemon.username = None @@ -256,37 +274,43 @@ class DaemonSSLProxy(DaemonProxy): self.host = None self.port = None self.username = None + self.authentication_level = 0 self.connected = False self.disconnect_deferred = None self.disconnect_callback = None - def connect(self, host, port, username, password): + self.auth_levels_mapping = None + self.auth_levels_mapping_reverse = None + + def connect(self, host, port): """ Connects to a daemon at host:port :param host: str, the host to connect to :param port: int, the listening port on the daemon - :param username: str, the username to login as - :param password: str, the password to login with :returns: twisted.Deferred """ + log.debug("sslproxy.connect()") self.host = host self.port = port - self.__connector = reactor.connectSSL(self.host, self.port, self.__factory, ssl.ClientContextFactory()) + self.__connector = reactor.connectSSL(self.host, self.port, + self.__factory, + ssl.ClientContextFactory()) self.connect_deferred = defer.Deferred() - self.login_deferred = defer.Deferred() + self.daemon_info_deferred = defer.Deferred() # Upon connect we do a 'daemon.login' RPC - self.connect_deferred.addCallback(self.__on_connect, username, password) + self.connect_deferred.addCallback(self.__on_connect) self.connect_deferred.addErrback(self.__on_connect_fail) - return self.login_deferred + return self.daemon_info_deferred def disconnect(self): + log.debug("sslproxy.disconnect()") self.disconnect_deferred = defer.Deferred() self.__connector.disconnect() return self.disconnect_deferred @@ -315,7 +339,6 @@ class DaemonSSLProxy(DaemonProxy): # Create a Deferred object to return and add a default errback to print # the error. d = defer.Deferred() - d.addErrback(self.__rpcError) # Store the Deferred until we receive a response from the daemon self.__deferred[self.__request_counter] = d @@ -373,50 +396,58 @@ class DaemonSSLProxy(DaemonProxy): if event in self.__factory.event_handlers and handler in self.__factory.event_handlers[event]: self.__factory.event_handlers[event].remove(handler) - def __rpcError(self, error_data): - """ - Prints out a RPCError message to the error log. This includes the daemon - traceback. + def __on_connect(self, result): + log.debug("__on_connect called") - :param error_data: this is passed from the deferred errback with error.value - containing a `:class:DelugeRPCError` object. - """ - # Get the DelugeRPCError object from the error_data - error = error_data.value - # Create a delugerpcrequest to print out a nice RPCRequest string - r = DelugeRPCRequest() - r.method = error.method - r.args = error.args - r.kwargs = error.kwargs - msg = "RPCError Message Received!" - msg += "\n" + "-" * 80 - msg += "\n" + "RPCRequest: " + r.__repr__() - msg += "\n" + "-" * 80 - msg += "\n" + error.traceback + "\n" + error.exception_type + ": " + error.exception_msg - msg += "\n" + "-" * 80 - log.error(msg) - return error_data + def on_info(daemon_info): + self.daemon_info = daemon_info + log.debug("Got info from daemon: %s", daemon_info) + self.daemon_info_deferred.callback(daemon_info) - def __on_connect(self, result, username, password): - self.__login_deferred = self.call("daemon.login", username, password) - self.__login_deferred.addCallback(self.__on_login, username) - self.__login_deferred.addErrback(self.__on_login_fail) + def on_info_fail(reason): + log.debug("Failed to get info from daemon") + log.exception(reason) + self.daemon_info_deferred.errback(reason) + + self.call("daemon.info").addCallback(on_info).addErrback(on_info_fail) + return self.daemon_info_deferred def __on_connect_fail(self, reason): - log.debug("connect_fail: %s", reason) - self.login_deferred.errback(reason) + self.daemon_info_deferred.errback(reason) + + def authenticate(self, username, password): + log.debug("%s.authenticate: %s", self.__class__.__name__, username) + self.login_deferred = defer.Deferred() + d = self.call("daemon.login", username, password, + client_version=deluge.common.get_version()) + d.addCallback(self.__on_login, username) + d.addErrback(self.__on_login_fail) + return self.login_deferred def __on_login(self, result, username): + log.debug("__on_login called: %s %s", username, result) self.username = username + self.authentication_level = result # We need to tell the daemon what events we're interested in receiving if self.__factory.event_handlers: - self.call("daemon.set_event_interest", self.__factory.event_handlers.keys()) + self.call("daemon.set_event_interest", + self.__factory.event_handlers.keys()) + + self.call("core.get_auth_levels_mappings").addCallback( + self.__on_auth_levels_mappings + ) + self.login_deferred.callback(result) def __on_login_fail(self, result): - log.debug("_on_login_fail(): %s", result) + log.debug("_on_login_fail(): %s", result.value) self.login_deferred.errback(result) + def __on_auth_levels_mappings(self, result): + auth_levels_mapping, auth_levels_mapping_reverse = result + self.auth_levels_mapping = auth_levels_mapping + self.auth_levels_mapping_reverse = auth_levels_mapping_reverse + def set_disconnect_callback(self, cb): """ Set a function to be called when the connection to the daemon is lost @@ -438,13 +469,21 @@ class DaemonClassicProxy(DaemonProxy): self.connected = True self.host = "localhost" self.port = 58846 + # Running in classic mode, it's safe to import auth level + from deluge.core.authmanager import (AUTH_LEVEL_ADMIN, + AUTH_LEVELS_MAPPING, + AUTH_LEVELS_MAPPING_REVERSE) self.username = "localclient" + self.authentication_level = AUTH_LEVEL_ADMIN + self.auth_levels_mapping = AUTH_LEVELS_MAPPING + self.auth_levels_mapping_reverse = AUTH_LEVELS_MAPPING_REVERSE # Register the event handlers for event in event_handlers: for handler in event_handlers[event]: self.__daemon.core.eventmanager.register_event_handler(event, handler) def disconnect(self): + self.connected = False self.__daemon = None def call(self, method, *args, **kwargs): @@ -458,12 +497,14 @@ class DaemonClassicProxy(DaemonProxy): log.exception(e) return defer.fail(e) else: - return defer.maybeDeferred(m, *copy.deepcopy(args), **copy.deepcopy(kwargs)) + return defer.maybeDeferred( + m, *copy.deepcopy(args), **copy.deepcopy(kwargs) + ) def register_event_handler(self, event, handler): """ - Registers a handler function to be called when `:param:event` is received - from the daemon. + Registers a handler function to be called when `:param:event` is + received from the daemon. :param event: the name of the event to handle :type event: str @@ -520,7 +561,8 @@ class Client(object): self.disconnect_callback = None self.__started_in_classic = False - def connect(self, host="127.0.0.1", port=58846, username="", password=""): + def connect(self, host="127.0.0.1", port=58846, username="", password="", + skip_authentication=False): """ Connects to a daemon process. @@ -532,27 +574,50 @@ class Client(object): :returns: a Deferred object that will be called once the connection has been established or fails """ - if not username and host in ("127.0.0.1", "localhost"): - # No username was provided and it's the localhost, so we can try - # to grab the credentials from the auth file. - import common - username, password = common.get_localhost_auth() self._daemon_proxy = DaemonSSLProxy(dict(self.__event_handlers)) self._daemon_proxy.set_disconnect_callback(self.__on_disconnect) - d = self._daemon_proxy.connect(host, port, username, password) - def on_connect_fail(result): - log.debug("on_connect_fail: %s", result) + + d = self._daemon_proxy.connect(host, port) + + def on_connect_fail(reason): self.disconnect() + return reason + + def on_authenticate(result, daemon_info): + log.debug("Authentication sucessfull: %s", result) return result + def on_authenticate_fail(reason): + log.debug("Failed to authenticate: %s", reason.value) + return reason + + def on_connected(daemon_version): + log.debug("Client.connect.on_connected. Daemon version: %s", + daemon_version) + return daemon_version + + def authenticate(daemon_version, username, password): + d = self._daemon_proxy.authenticate(username, password) + d.addCallback(on_authenticate, daemon_version) + d.addErrback(on_authenticate_fail) + return d + + d.addCallback(on_connected) d.addErrback(on_connect_fail) + if not skip_authentication: + d.addCallback(authenticate, username, password) return d def disconnect(self): """ Disconnects from the daemon. """ + if self.is_classicmode(): + self._daemon_proxy.disconnect() + self.stop_classic_mode() + return + if self._daemon_proxy: return self._daemon_proxy.disconnect() @@ -563,6 +628,13 @@ class Client(object): self._daemon_proxy = DaemonClassicProxy(self.__event_handlers) self.__started_in_classic = True + def stop_classic_mode(self): + """ + Stops the daemon process in the client. + """ + self._daemon_proxy = None + self.__started_in_classic = False + def start_daemon(self, port, config): """ Starts a daemon process. @@ -698,5 +770,31 @@ class Client(object): """ return self._daemon_proxy.get_bytes_sent() + def get_auth_user(self): + """ + Returns the current authenticated username. + + :returns: the authenticated username + :rtype: str + """ + return self._daemon_proxy.username + + def get_auth_level(self): + """ + Returns the authentication level the daemon returned upon authentication. + + :returns: the authentication level + :rtype: int + """ + return self._daemon_proxy.authentication_level + + @property + def auth_levels_mapping(self): + return self._daemon_proxy.auth_levels_mapping + + @property + def auth_levels_mapping_reverse(self): + return self._daemon_proxy.auth_levels_mapping_reverse + # This is the object clients will use client = Client() diff --git a/deluge/ui/common.py b/deluge/ui/common.py index fb9048d01..239bb84c9 100644 --- a/deluge/ui/common.py +++ b/deluge/ui/common.py @@ -407,26 +407,28 @@ def get_localhost_auth(): :rtype: tuple """ auth_file = deluge.configmanager.get_config_dir("auth") - if os.path.exists(auth_file): - for line in open(auth_file): - if line.startswith("#"): - # This is a comment line - continue - line = line.strip() - try: - lsplit = line.split(":") - except Exception, e: - log.error("Your auth file is malformed: %s", e) - continue + if not os.path.exists(auth_file): + from deluge.common import create_localclient_account + create_localclient_account() - if len(lsplit) == 2: - username, password = lsplit - elif len(lsplit) == 3: - username, password, level = lsplit - else: - log.error("Your auth file is malformed: Incorrect number of fields!") - continue + for line in open(auth_file): + if line.startswith("#"): + # This is a comment line + continue + line = line.strip() + try: + lsplit = line.split(":") + except Exception, e: + log.error("Your auth file is malformed: %s", e) + continue - if username == "localclient": - return (username, password) - return ("", "") + if len(lsplit) == 2: + username, password = lsplit + elif len(lsplit) == 3: + username, password, level = lsplit + else: + log.error("Your auth file is malformed: Incorrect number of fields!") + continue + + if username == "localclient": + return (username, password) diff --git a/deluge/ui/console/colors.py b/deluge/ui/console/colors.py index 83465737d..3df92294e 100644 --- a/deluge/ui/console/colors.py +++ b/deluge/ui/console/colors.py @@ -57,11 +57,17 @@ color_pairs = { # Some default color schemes schemes = { "input": ("white", "black"), + "normal": ("white","black"), "status": ("yellow", "blue", "bold"), "info": ("white", "black", "bold"), "error": ("red", "black", "bold"), "success": ("green", "black", "bold"), - "event": ("magenta", "black", "bold") + "event": ("magenta", "black", "bold"), + "selected": ("black", "white", "bold"), + "marked": ("white","blue","bold"), + "selectedmarked": ("blue","white","bold"), + "header": ("green","black","bold"), + "filterstatus": ("green", "blue", "bold") } # Colors for various torrent states @@ -94,6 +100,14 @@ def init_colors(): curses.init_pair(counter, getattr(curses, fg), getattr(curses, bg)) counter += 1 + # try to redefine white/black as it makes underlining work for some terminals + # but can also fail on others, so we try/except + try: + curses.init_pair(counter, curses.COLOR_WHITE, curses.COLOR_BLACK) + color_pairs[("white","black")] = counter + except: + pass + class BadColorString(Exception): pass diff --git a/deluge/ui/console/commander.py b/deluge/ui/console/commander.py new file mode 100644 index 000000000..21680c06f --- /dev/null +++ b/deluge/ui/console/commander.py @@ -0,0 +1,146 @@ +# +# commander.py +# +# Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from twisted.internet import defer, reactor +import deluge.component as component +from deluge.error import DelugeError +from deluge.ui.client import client +from deluge.ui.console import UI_PATH +from colors import strip_colors + +import logging +log = logging.getLogger(__name__) + +class Commander: + def __init__(self, cmds, interactive=False): + self._commands = cmds + self.console = component.get("ConsoleUI") + self.interactive = interactive + + def write(self,line): + print(strip_colors(line)) + + def do_command(self, cmd): + """ + Processes a command. + + :param cmd: str, the command string + + """ + if not cmd: + return + cmd, _, line = cmd.partition(' ') + try: + parser = self._commands[cmd].create_parser() + except KeyError: + self.write("{!error!}Unknown command: %s" % cmd) + return + args = self._commands[cmd].split(line) + + # Do a little hack here to print 'command --help' properly + parser._print_help = parser.print_help + def print_help(f=None): + if self.interactive: + self.write(parser.format_help()) + else: + parser._print_help(f) + parser.print_help = print_help + + # Only these commands can be run when not connected to a daemon + not_connected_cmds = ["help", "connect", "quit"] + aliases = [] + for c in not_connected_cmds: + aliases.extend(self._commands[c].aliases) + not_connected_cmds.extend(aliases) + + if not client.connected() and cmd not in not_connected_cmds: + self.write("{!error!}Not connected to a daemon, please use the connect command first.") + return + + try: + options, args = parser.parse_args(args) + except Exception, e: + self.write("{!error!}Error parsing options: %s" % e) + return + + if not getattr(options, '_exit', False): + try: + ret = self._commands[cmd].handle(*args, **options.__dict__) + except Exception, e: + self.write("{!error!}" + str(e)) + log.exception(e) + import traceback + self.write("%s" % traceback.format_exc()) + return defer.succeed(True) + else: + return ret + + def exec_args(self,args,host,port,username,password): + def on_connect(result): + def on_started(result): + def on_started(result): + deferreds = [] + # If we have args, lets process them and quit + # allow multiple commands split by ";" + for arg in args.split(";"): + deferreds.append(defer.maybeDeferred(self.do_command, arg.strip())) + + def on_complete(result): + self.do_command("quit") + + dl = defer.DeferredList(deferreds).addCallback(on_complete) + + # We need to wait for the rpcs in start() to finish before processing + # any of the commands. + self.console.started_deferred.addCallback(on_started) + component.start().addCallback(on_started) + + def on_connect_fail(reason): + if reason.check(DelugeError): + rm = reason.value.message + else: + rm = reason.getErrorMessage() + print "Could not connect to: %s:%d\n %s"%(host,port,rm) + self.do_command("quit") + + if host: + d = client.connect(host,port,username,password) + else: + d = client.connect() + d.addCallback(on_connect) + d.addErrback(on_connect_fail) + diff --git a/deluge/ui/console/commands/add.py b/deluge/ui/console/commands/add.py index 8d688187c..37a3eb2aa 100644 --- a/deluge/ui/console/commands/add.py +++ b/deluge/ui/console/commands/add.py @@ -39,6 +39,7 @@ from deluge.ui.console.main import BaseCommand import deluge.ui.console.colors as colors from deluge.ui.client import client import deluge.component as component +import deluge.common from optparse import make_option import os @@ -49,36 +50,52 @@ class Command(BaseCommand): option_list = BaseCommand.option_list + ( make_option('-p', '--path', dest='path', help='save path for torrent'), + make_option('-u', '--urls', action='store_true', default=False, dest='force_url', + help='Interpret all given torrent-file arguments as URLs'), + make_option('-f', '--files', action='store_true', default=False, dest='force_file', + help='Interpret all given torrent-file arguments as files'), ) - usage = "Usage: add [-p ] [ ...]" + usage = "Usage: add [-p ] [-u | --urls] [-f | --files] [ ...]\n"\ + " arguments can be file paths, URLs or magnet uris" def handle(self, *args, **options): self.console = component.get("ConsoleUI") + if options["force_file"] and options["force_url"]: + self.console.write("{!error!}Cannot specify --urls and --files at the same time") + return + t_options = {} if options["path"]: t_options["download_location"] = os.path.expanduser(options["path"]) + def on_success(result): + self.console.write("{!success!}Torrent added!") + def on_fail(result): + self.console.write("{!error!}Torrent was not added! %s" % result) + # Keep a list of deferreds to make a DeferredList deferreds = [] for arg in args: - if not os.path.exists(arg): - self.console.write("{!error!}%s doesn't exist!" % arg) - continue - if not os.path.isfile(arg): - self.console.write("{!error!}This is a directory!") - continue - self.console.write("{!info!}Attempting to add torrent: %s" % arg) - filename = os.path.split(arg)[-1] - filedump = base64.encodestring(open(arg).read()) + if not options["force_file"] and (deluge.common.is_url(arg) or options["force_url"]): + self.console.write("{!info!}Attempting to add torrent from url: %s" % arg) + deferreds.append(client.core.add_torrent_url(arg, t_options).addCallback(on_success).addErrback(on_fail)) + elif not options["force_file"] and (deluge.common.is_magnet(arg)): + self.console.write("{!info!}Attempting to add torrent from magnet uri: %s" % arg) + deferreds.append(client.core.add_torrent_magnet(arg, t_options).addCallback(on_success).addErrback(on_fail)) + else: + if not os.path.exists(arg): + self.console.write("{!error!}%s doesn't exist!" % arg) + continue + if not os.path.isfile(arg): + self.console.write("{!error!}This is a directory!") + continue + self.console.write("{!info!}Attempting to add torrent: %s" % arg) + filename = os.path.split(arg)[-1] + filedump = base64.encodestring(open(arg, "rb").read()) - def on_success(result): - self.console.write("{!success!}Torrent added!") - def on_fail(result): - self.console.write("{!error!}Torrent was not added! %s" % result) - - deferreds.append(client.core.add_torrent_file(filename, filedump, t_options).addCallback(on_success).addErrback(on_fail)) + deferreds.append(client.core.add_torrent_file(filename, filedump, t_options).addCallback(on_success).addErrback(on_fail)) return defer.DeferredList(deferreds) diff --git a/deluge/ui/console/commands/cache.py b/deluge/ui/console/commands/cache.py index eee7e049f..92c016d9f 100644 --- a/deluge/ui/console/commands/cache.py +++ b/deluge/ui/console/commands/cache.py @@ -47,4 +47,6 @@ class Command(BaseCommand): for key, value in status.items(): self.console.write("{!info!}%s: {!input!}%s" % (key, value)) - client.core.get_cache_status().addCallback(on_cache_status) + d = client.core.get_cache_status() + d.addCallback(on_cache_status) + return d diff --git a/deluge/ui/console/commands/config.py b/deluge/ui/console/commands/config.py index e74ad344e..3c9ddb921 100644 --- a/deluge/ui/console/commands/config.py +++ b/deluge/ui/console/commands/config.py @@ -62,7 +62,10 @@ def atom(next, token): return tuple(out) elif token[0] is tokenize.NUMBER or token[1] == "-": try: - return int(token[-1], 0) + if token[1] == "-": + return int(token[-1], 0) + else: + return int(token[1], 0) except ValueError: return float(token[-1]) elif token[1].lower() == 'true': @@ -133,7 +136,7 @@ class Command(BaseCommand): deferred = defer.Deferred() config = component.get("CoreConfig") key = options["set"][0] - val = simple_eval(options["set"][1] + " " + " ".join(args)) + val = simple_eval(options["set"][1] + " " .join(args)) if key not in config.keys(): self.console.write("{!error!}The key '%s' is invalid!" % key) diff --git a/deluge/ui/console/commands/debug.py b/deluge/ui/console/commands/debug.py index 118f9542b..40e87eda2 100644 --- a/deluge/ui/console/commands/debug.py +++ b/deluge/ui/console/commands/debug.py @@ -44,7 +44,7 @@ import deluge.component as component class Command(BaseCommand): """Enable and disable debugging""" - usage = 'debug [on|off]' + usage = 'Usage: debug [on|off]' def handle(self, state='', **options): if state == 'on': deluge.log.setLoggerLevel("debug") diff --git a/deluge/ui/console/commands/gui.py b/deluge/ui/console/commands/gui.py new file mode 100644 index 000000000..35335e9fb --- /dev/null +++ b/deluge/ui/console/commands/gui.py @@ -0,0 +1,51 @@ +# +# status.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + +from deluge.ui.console.main import BaseCommand +import deluge.common +import deluge.component as component +from deluge.ui.console.modes.alltorrents import AllTorrents + +class Command(BaseCommand): + """Exit this mode and go into the more 'gui' like mode""" + usage = "Usage: gui" + interactive_only = True + def handle(self, *args, **options): + console = component.get("ConsoleUI") + try: + at = component.get("AllTorrents") + except KeyError: + at = AllTorrents(console.stdscr,console.encoding) + console.set_mode(at) + at.resume() diff --git a/deluge/ui/console/commands/halt.py b/deluge/ui/console/commands/halt.py index 0a4de27a9..1c239dec4 100644 --- a/deluge/ui/console/commands/halt.py +++ b/deluge/ui/console/commands/halt.py @@ -39,7 +39,8 @@ from deluge.ui.client import client import deluge.component as component class Command(BaseCommand): - "Shutdown the deluge server." + "Shutdown the deluge server" + usage = "Usage: halt" def handle(self, **options): self.console = component.get("ConsoleUI") diff --git a/deluge/ui/console/commands/info.py b/deluge/ui/console/commands/info.py index 8a478a87d..7e6ed99ac 100644 --- a/deluge/ui/console/commands/info.py +++ b/deluge/ui/console/commands/info.py @@ -70,6 +70,8 @@ status_keys = ["state", "is_finished" ] +states = ["Active", "Downloading", "Seeding", "Paused", "Checking", "Error", "Queued"] + def format_progressbar(progress, width): """ @@ -85,7 +87,7 @@ def format_progressbar(progress, width): s = "[" p = int(round((progress/100) * w)) s += "#" * p - s += "~" * (w - p) + s += "-" * (w - p) s += "]" return s @@ -98,11 +100,17 @@ class Command(BaseCommand): help='shows more information per torrent'), make_option('-i', '--id', action='store_true', default=False, dest='tid', help='use internal id instead of torrent name'), + make_option('-s', '--state', action='store', dest='state', + help="Only retrieve torrents in state STATE. " + "Allowable values are: %s "%(", ".join(states))), ) - usage = "Usage: info [ [ ...]]\n"\ + usage = "Usage: info [-v | -i | -s ] [ [ ...]]\n"\ + " info -s will show only torrents in state \n"\ " You can give the first few characters of a torrent-id to identify the torrent." + + def handle(self, *args, **options): self.console = component.get("ConsoleUI") # Compile a list of torrent_ids to request the status of @@ -121,7 +129,17 @@ class Command(BaseCommand): def on_torrents_status_fail(reason): self.console.write("{!error!}Error getting torrent info: %s" % reason) - d = client.core.get_torrents_status({"id": torrent_ids}, status_keys) + status_dict = {"id": torrent_ids} + + if options["state"]: + if options["state"] not in states: + self.console.write("Invalid state: %s"%options["state"]) + self.console.write("Allowble values are: %s."%(", ".join(states))) + return + else: + status_dict["state"] = options["state"] + + d = client.core.get_torrents_status(status_dict, status_keys) d.addCallback(on_torrents_status) d.addErrback(on_torrents_status_fail) return d @@ -189,10 +207,11 @@ class Command(BaseCommand): s += " %s" % (fp) # Check if this is too long for the screen and reduce the path # if necessary - cols = self.console.screen.cols - slen = colors.get_line_length(s, self.console.screen.encoding) - if slen > cols: - s = s.replace(f["path"], f["path"][slen - cols + 1:]) + if hasattr(self.console, "screen"): + cols = self.console.screen.cols + slen = colors.get_line_length(s, self.console.screen.encoding) + if slen > cols: + s = s.replace(f["path"], f["path"][slen - cols + 1:]) self.console.write(s) self.console.write(" {!info!}::Peers") diff --git a/deluge/ui/console/commands/move.py b/deluge/ui/console/commands/move.py new file mode 100644 index 000000000..011031417 --- /dev/null +++ b/deluge/ui/console/commands/move.py @@ -0,0 +1,111 @@ +# +# move.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from deluge.ui.console.main import BaseCommand +from deluge.ui.client import client +import deluge.component as component + +import os.path + + +class Command(BaseCommand): + """Move torrents' storage location""" + usage = "Usage: move [ ...] " + + def handle(self, *args, **options): + self.console = component.get("ConsoleUI") + + if len(args) < 2: + self.console.write(self.usage) + return + + path = args[-1] + + if os.path.exists(path) and not os.path.isdir(path): + self.console.write("{!error!}Cannot Move Storage: %s exists and is not a directory"%path) + return + + ids = [] + for i in args[:-1]: + ids.extend(self.console.match_torrent(i)) + + names = [] + for i in ids: + names.append(self.console.get_torrent_name(i)) + namestr = ", ".join(names) + + def on_move(res): + self.console.write("Moved \"%s\" to %s"%(namestr,path)) + + d = client.core.move_storage(ids,path) + d.addCallback(on_move) + return d + + def complete(self, line): + line = os.path.abspath(os.path.expanduser(line)) + ret = [] + if os.path.exists(line): + # This is a correct path, check to see if it's a directory + if os.path.isdir(line): + # Directory, so we need to show contents of directory + #ret.extend(os.listdir(line)) + for f in os.listdir(line): + # Skip hidden + if f.startswith("."): + continue + f = os.path.join(line, f) + if os.path.isdir(f): + f += "/" + ret.append(f) + else: + # This is a file, but we could be looking for another file that + # shares a common prefix. + for f in os.listdir(os.path.dirname(line)): + if f.startswith(os.path.split(line)[1]): + ret.append(os.path.join( os.path.dirname(line), f)) + else: + # This path does not exist, so lets do a listdir on it's parent + # and find any matches. + ret = [] + if os.path.isdir(os.path.dirname(line)): + for f in os.listdir(os.path.dirname(line)): + if f.startswith(os.path.split(line)[1]): + p = os.path.join(os.path.dirname(line), f) + + if os.path.isdir(p): + p += "/" + ret.append(p) + + return ret diff --git a/deluge/ui/console/commands/quit.py b/deluge/ui/console/commands/quit.py index f0e2fae22..1c95f9461 100644 --- a/deluge/ui/console/commands/quit.py +++ b/deluge/ui/console/commands/quit.py @@ -40,6 +40,7 @@ from twisted.internet import reactor class Command(BaseCommand): """Exit from the client.""" aliases = ['exit'] + interactive_only = True def handle(self, *args, **options): if client.connected(): def on_disconnect(result): diff --git a/deluge/ui/console/commands/status.py b/deluge/ui/console/commands/status.py new file mode 100644 index 000000000..9dc6b29db --- /dev/null +++ b/deluge/ui/console/commands/status.py @@ -0,0 +1,126 @@ +# +# status.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# + +from optparse import make_option +from twisted.internet import defer +from deluge.ui.console.main import BaseCommand +from deluge.ui.client import client +import deluge.common +import deluge.component as component + +class Command(BaseCommand): + """Shows a various status information from the daemon.""" + option_list = BaseCommand.option_list + ( + make_option('-r', '--raw', action='store_true', default=False, dest='raw', + help='Don\'t format upload/download rates in KiB/s (useful for scripts that want to do their own parsing)'), + make_option('-n', '--no-torrents', action='store_false', default=True, dest='show_torrents', + help='Don\'t show torrent status (this will make the command a bit faster)'), + ) + + + usage = "Usage: status [-r] [-n]" + def handle(self, *args, **options): + self.console = component.get("ConsoleUI") + self.status = None + self.connections = None + if options["show_torrents"]: + self.torrents = None + else: + self.torrents = -2 + + self.raw = options["raw"] + + def on_session_status(status): + self.status = status + if self.status != None and self.connections != None and self.torrents != None: + self.print_status() + + def on_num_connections(conns): + self.connections = conns + if self.status != None and self.connections != None and self.torrents != None: + self.print_status() + + def on_torrents_status(status): + self.torrents = status + if self.status != None and self.connections != None and self.torrents != None: + self.print_status() + + def on_torrents_status_fail(reason): + self.torrents = -1 + if self.status != None and self.connections != None and self.torrents != None: + self.print_status() + + deferreds = [] + + ds = client.core.get_session_status(["payload_upload_rate","payload_download_rate","dht_nodes"]) + ds.addCallback(on_session_status) + deferreds.append(ds) + + dc = client.core.get_num_connections() + dc.addCallback(on_num_connections) + deferreds.append(dc) + + if options["show_torrents"]: + dt = client.core.get_torrents_status({}, ["state"]) + dt.addCallback(on_torrents_status) + dt.addErrback(on_torrents_status_fail) + deferreds.append(dt) + + return defer.DeferredList(deferreds) + + def print_status(self): + self.console.set_batch_write(True) + if self.raw: + self.console.write("{!info!}Total upload: %f"%self.status["payload_upload_rate"]) + self.console.write("{!info!}Total download: %f"%self.status["payload_download_rate"]) + else: + self.console.write("{!info!}Total upload: %s"%deluge.common.fspeed(self.status["payload_upload_rate"])) + self.console.write("{!info!}Total download: %s"%deluge.common.fspeed(self.status["payload_download_rate"])) + self.console.write("{!info!}DHT Nodes: %i"%self.status["dht_nodes"]) + self.console.write("{!info!}Total connections: %i"%self.connections) + if self.torrents == -1: + self.console.write("{!error!}Error getting torrent info") + elif self.torrents != -2: + self.console.write("{!info!}Total torrents: %i"%len(self.torrents)) + states = ["Downloading","Seeding","Paused","Checking","Error","Queued"] + state_counts = {} + for state in states: + state_counts[state] = 0 + for t in self.torrents: + s = self.torrents[t] + state_counts[s["state"]] += 1 + for state in states: + self.console.write("{!info!} %s: %i"%(state,state_counts[state])) + + self.console.set_batch_write(False) diff --git a/deluge/ui/console/commands/update-tracker.py b/deluge/ui/console/commands/update-tracker.py new file mode 100644 index 000000000..a4aa880e1 --- /dev/null +++ b/deluge/ui/console/commands/update-tracker.py @@ -0,0 +1,65 @@ +# +# rm.py +# +# Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# +from deluge.ui.console.main import BaseCommand +import deluge.ui.console.colors as colors +from deluge.ui.client import client +import deluge.component as component + +from optparse import make_option + + +class Command(BaseCommand): + """Update tracker for torrent(s)""" + usage = "Usage: update-tracker [ * | [ ...] ]" + aliases = ['reannounce'] + + def handle(self, *args, **options): + self.console = component.get("ConsoleUI") + if len(args) == 0: + self.console.write(self.usage) + return + if len(args) > 0 and args[0].lower() == '*': + args = [""] + + torrent_ids = [] + for arg in args: + torrent_ids.extend(self.console.match_torrent(arg)) + + client.core.force_reannounce(torrent_ids) + + def complete(self, line): + # We use the ConsoleUI torrent tab complete method + return component.get("ConsoleUI").tab_complete_torrent(line) diff --git a/deluge/ui/console/eventlog.py b/deluge/ui/console/eventlog.py index 351186dd8..162bb3ff2 100644 --- a/deluge/ui/console/eventlog.py +++ b/deluge/ui/console/eventlog.py @@ -62,53 +62,53 @@ class EventLog(component.Component): client.register_event_handler("PluginEnabledEvent", self.on_plugin_enabled_event) client.register_event_handler("PluginDisabledEvent", self.on_plugin_disabled_event) - def on_torrent_added_event(self, event): + def on_torrent_added_event(self, torrent_id, from_state): def on_torrent_status(status): self.console.write(self.prefix + "TorrentAdded(from_state=%s): {!info!}%s (%s)" % ( - event.from_state, status["name"], event.torrent_id) + from_state, status["name"], torrent_id) ) - client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status) + client.core.get_torrent_status(torrent_id, ["name"]).addCallback(on_torrent_status) - def on_torrent_removed_event(self, event): + def on_torrent_removed_event(self, torrent_id): self.console.write(self.prefix + "TorrentRemoved: {!info!}%s (%s)" % - (self.console.get_torrent_name(event.torrent_id), event.torrent_id)) + (self.console.get_torrent_name(torrent_id), torrent_id)) - def on_torrent_state_changed_event(self, event): + def on_torrent_state_changed_event(self, torrent_id, state): # Modify the state string color - if event.state in colors.state_color: - state = colors.state_color[event.state] + event.state + if state in colors.state_color: + state = colors.state_color[state] + state self.console.write(self.prefix + "TorrentStateChanged: %s {!info!}%s (%s)" % - (state, self.console.get_torrent_name(event.torrent_id), event.torrent_id)) + (state, self.console.get_torrent_name(torrent_id), torrent_id)) - def on_torrent_paused_event(self, event): + def on_torrent_paused_event(self, torrent_id): self.console.write(self.prefix + "TorrentPaused: {!info!}%s (%s)" % - (self.console.get_torrent_name(event.torrent_id), event.torrent_id)) + (self.console.get_torrent_name(torrent_id), torrent_id)) - def on_torrent_finished_event(self, event): + def on_torrent_finished_event(self, torrent_id): self.console.write(self.prefix + "TorrentFinished: {!info!}%s (%s)" % - (self.console.get_torrent_name(event.torrent_id), event.torrent_id)) + (self.console.get_torrent_name(torrent_id), torrent_id)) - def on_new_version_available_event(self, event): + def on_new_version_available_event(self, version): self.console.write(self.prefix + "NewVersionAvailable: {!info!}%s" % - (event.new_release)) + (version)) - def on_session_paused_event(self, event): + def on_session_paused_event(self): self.console.write(self.prefix + "SessionPaused") - def on_session_resumed_event(self, event): + def on_session_resumed_event(self): self.console.write(self.prefix + "SessionResumed") - def on_config_value_changed_event(self, event): + def on_config_value_changed_event(self, key, value): color = "{!white,black,bold!}" - if type(event.value) in colors.type_color: - color = colors.type_color[type(event.value)] + if type(value) in colors.type_color: + color = colors.type_color[type(value)] self.console.write(self.prefix + "ConfigValueChanged: {!input!}%s: %s%s" % - (event.key, color, event.value)) + (key, color, value)) - def on_plugin_enabled_event(self, event): - self.console.write(self.prefix + "PluginEnabled: {!info!}%s" % event.plugin_name) + def on_plugin_enabled_event(self, name): + self.console.write(self.prefix + "PluginEnabled: {!info!}%s" % name) - def on_plugin_disabled_event(self, event): - self.console.write(self.prefix + "PluginDisabled: {!info!}%s" % event.plugin_name) + def on_plugin_disabled_event(self, name): + self.console.write(self.prefix + "PluginDisabled: {!info!}%s" % name) diff --git a/deluge/ui/console/main.py b/deluge/ui/console/main.py index 6ea3adb79..ddba4b59f 100644 --- a/deluge/ui/console/main.py +++ b/deluge/ui/console/main.py @@ -43,16 +43,17 @@ import locale from twisted.internet import defer, reactor -from deluge.ui.console import UI_PATH import deluge.component as component from deluge.ui.client import client import deluge.common from deluge.ui.coreconfig import CoreConfig +from deluge.ui.sessionproxy import SessionProxy from deluge.ui.console.statusbars import StatusBars from deluge.ui.console.eventlog import EventLog -import screen import colors from deluge.ui.ui import _UI +from deluge.ui.console import UI_PATH + log = logging.getLogger(__name__) @@ -62,16 +63,62 @@ class Console(_UI): def __init__(self): super(Console, self).__init__("console") - cmds = load_commands(os.path.join(UI_PATH, 'commands')) + group = optparse.OptionGroup(self.parser, "Console Options","These options control how " + "the console connects to the daemon. These options will be " + "used if you pass a command, or if you have autoconnect " + "enabled for the console ui.") - group = optparse.OptionGroup(self.parser, "Console Commands", - "\n".join(cmds.keys())) + group.add_option("-d","--daemon",dest="daemon_addr", + action="store",type="str",default="127.0.0.1", + help="Set the address of the daemon to connect to." + " [default: %default]") + group.add_option("-p","--port",dest="daemon_port", + help="Set the port to connect to the daemon on. [default: %default]", + action="store",type="int",default=58846) + group.add_option("-u","--username",dest="daemon_user", + help="Set the username to connect to the daemon with. [default: %default]", + action="store",type="string") + group.add_option("-P","--password",dest="daemon_pass", + help="Set the password to connect to the daemon with. [default: %default]", + action="store",type="string") self.parser.add_option_group(group) + self.cmds = load_commands(os.path.join(UI_PATH, 'commands')) + class CommandOptionGroup(optparse.OptionGroup): + def __init__(self, parser, title, description=None, cmds = None): + optparse.OptionGroup.__init__(self,parser,title,description) + self.cmds = cmds + + def format_help(self, formatter): + result = formatter.format_heading(self.title) + formatter.indent() + if self.description: + result += "%s\n"%formatter.format_description(self.description) + for cname in self.cmds: + cmd = self.cmds[cname] + if cmd.interactive_only or cname in cmd.aliases: continue + allnames = [cname] + allnames.extend(cmd.aliases) + cname = "/".join(allnames) + result += formatter.format_heading(" - ".join([cname,cmd.__doc__])) + formatter.indent() + result += "%*s%s\n" % (formatter.current_indent, "", cmd.usage) + formatter.dedent() + formatter.dedent() + return result + cmd_group = CommandOptionGroup(self.parser, "Console Commands", + description="The following commands can be issued at the " + "command line. Commands should be quoted, so, for example, " + "to pause torrent with id 'abc' you would run: '%s " + "\"pause abc\"'"%os.path.basename(sys.argv[0]), + cmds=self.cmds) + self.parser.add_option_group(cmd_group) + def start(self): super(Console, self).start() - - ConsoleUI(self.args) + ConsoleUI(self.args,self.cmds,(self.options.daemon_addr, + self.options.daemon_port,self.options.daemon_user, + self.options.daemon_pass)) def start(): Console().start() @@ -92,9 +139,11 @@ class OptionParser(optparse.OptionParser): """ raise Exception(msg) + class BaseCommand(object): usage = 'usage' + interactive_only = False option_list = tuple() aliases = [] @@ -122,6 +171,7 @@ class BaseCommand(object): epilog = self.epilog, option_list = self.option_list) + def load_commands(command_dir, exclude=[]): def get_command(name): return getattr(__import__('deluge.ui.console.commands.%s' % name, {}, {}, ['Command']), 'Command')() @@ -142,11 +192,13 @@ def load_commands(command_dir, exclude=[]): except OSError, e: return {} + class ConsoleUI(component.Component): - def __init__(self, args=None): + def __init__(self, args=None, cmds = None, daemon = None): component.Component.__init__(self, "ConsoleUI", 2) - self.batch_write = False + # keep track of events for the log view + self.events = [] try: locale.setlocale(locale.LC_ALL, '') @@ -155,40 +207,30 @@ class ConsoleUI(component.Component): self.encoding = sys.getdefaultencoding() log.debug("Using encoding: %s", self.encoding) - # Load all the commands - self._commands = load_commands(os.path.join(UI_PATH, 'commands')) + + + # start up the session proxy + self.sessionproxy = SessionProxy() client.set_disconnect_callback(self.on_client_disconnect) # Set the interactive flag to indicate where we should print the output self.interactive = True + self._commands = cmds if args: args = args[0] self.interactive = False - - # Try to connect to the localhost daemon - def on_connect(result): - def on_started(result): - if not self.interactive: - def on_started(result): - deferreds = [] - # If we have args, lets process them and quit - # allow multiple commands split by ";" - for arg in args.split(";"): - deferreds.append(defer.maybeDeferred(self.do_command, arg.strip())) - - def on_complete(result): - self.do_command("quit") - - dl = defer.DeferredList(deferreds).addCallback(on_complete) - - # We need to wait for the rpcs in start() to finish before processing - # any of the commands. - self.started_deferred.addCallback(on_started) - component.start().addCallback(on_started) - - d = client.connect() - d.addCallback(on_connect) + if not cmds: + print "Sorry, couldn't find any commands" + return + else: + from commander import Commander + cmdr = Commander(cmds) + if daemon: + cmdr.exec_args(args,*daemon) + else: + cmdr.exec_args(args,None,None,None,None) + self.coreconfig = CoreConfig() if self.interactive and not deluge.common.windows_check(): @@ -197,8 +239,13 @@ class ConsoleUI(component.Component): import curses.wrapper curses.wrapper(self.run) elif self.interactive and deluge.common.windows_check(): - print "You cannot run the deluge-console in interactive mode in Windows.\ - Please use commands from the command line, eg: deluge-console config;help;exit" + print """\nDeluge-console does not run in interactive mode on Windows. \n +Please use commands from the command line, eg:\n + deluge-console.exe help + deluge-console.exe info + deluge-console.exe "add --help" + deluge-console.exe "add -p c:\\mytorrents c:\\new.torrent" + """ else: reactor.run() @@ -213,8 +260,10 @@ class ConsoleUI(component.Component): # We want to do an interactive session, so start up the curses screen and # pass it the function that handles commands colors.init_colors() - self.screen = screen.Screen(stdscr, self.do_command, self.tab_completer, self.encoding) self.statusbars = StatusBars() + from modes.connectionmanager import ConnectionManager + self.stdscr = stdscr + self.screen = ConnectionManager(stdscr, self.encoding) self.eventlog = EventLog() self.screen.topbar = "{!status!}Deluge " + deluge.common.get_version() + " Console" @@ -228,202 +277,22 @@ class ConsoleUI(component.Component): # Start the twisted mainloop reactor.run() - def start(self): - # This gets fired once we have received the torrents list from the core - self.started_deferred = defer.Deferred() + def start(self): # Maintain a list of (torrent_id, name) for use in tab completion self.torrents = [] - def on_session_state(result): - def on_torrents_status(torrents): - for torrent_id, status in torrents.items(): - self.torrents.append((torrent_id, status["name"])) - self.started_deferred.callback(True) + if not self.interactive: + self.started_deferred = defer.Deferred() + def on_session_state(result): + def on_torrents_status(torrents): + for torrent_id, status in torrents.items(): + self.torrents.append((torrent_id, status["name"])) + self.started_deferred.callback(True) - client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status) - client.core.get_session_state().addCallback(on_session_state) - - # Register some event handlers to keep the torrent list up-to-date - client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event) - client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event) - - def update(self): - pass - - def set_batch_write(self, batch): - """ - When this is set the screen is not refreshed after a `:meth:write` until - this is set to False. - - :param batch: set True to prevent screen refreshes after a `:meth:write` - :type batch: bool - - """ - self.batch_write = batch - if not batch and self.interactive: - self.screen.refresh() - - def write(self, line): - """ - Writes a line out depending on if we're in interactive mode or not. - - :param line: str, the line to print - - """ - if self.interactive: - self.screen.add_line(line, not self.batch_write) - else: - print(colors.strip_colors(line)) - - def do_command(self, cmd): - """ - Processes a command. - - :param cmd: str, the command string - - """ - if not cmd: - return - cmd, _, line = cmd.partition(' ') - try: - parser = self._commands[cmd].create_parser() - except KeyError: - self.write("{!error!}Unknown command: %s" % cmd) - return - args = self._commands[cmd].split(line) - - # Do a little hack here to print 'command --help' properly - parser._print_help = parser.print_help - def print_help(f=None): - if self.interactive: - self.write(parser.format_help()) - else: - parser._print_help(f) - parser.print_help = print_help - - # Only these commands can be run when not connected to a daemon - not_connected_cmds = ["help", "connect", "quit"] - aliases = [] - for c in not_connected_cmds: - aliases.extend(self._commands[c].aliases) - not_connected_cmds.extend(aliases) - - if not client.connected() and cmd not in not_connected_cmds: - self.write("{!error!}Not connected to a daemon, please use the connect command first.") - return - - try: - options, args = parser.parse_args(args) - except Exception, e: - self.write("{!error!}Error parsing options: %s" % e) - return - - if not getattr(options, '_exit', False): - try: - ret = self._commands[cmd].handle(*args, **options.__dict__) - except Exception, e: - self.write("{!error!}" + str(e)) - log.exception(e) - import traceback - self.write("%s" % traceback.format_exc()) - return defer.succeed(True) - else: - return ret - - def tab_completer(self, line, cursor, second_hit): - """ - Called when the user hits 'tab' and will autocomplete or show options. - If a command is already supplied in the line, this function will call the - complete method of the command. - - :param line: str, the current input string - :param cursor: int, the cursor position in the line - :param second_hit: bool, if this is the second time in a row the tab key - has been pressed - - :returns: 2-tuple (string, cursor position) - - """ - # First check to see if there is no space, this will mean that it's a - # command that needs to be completed. - if " " not in line: - possible_matches = [] - # Iterate through the commands looking for ones that startwith the - # line. - for cmd in self._commands: - if cmd.startswith(line): - possible_matches.append(cmd + " ") - - line_prefix = "" - else: - cmd = line.split(" ")[0] - if cmd in self._commands: - # Call the command's complete method to get 'er done - possible_matches = self._commands[cmd].complete(line.split(" ")[-1]) - line_prefix = " ".join(line.split(" ")[:-1]) + " " - else: - # This is a bogus command - return (line, cursor) - - # No matches, so just return what we got passed - if len(possible_matches) == 0: - return (line, cursor) - # If we only have 1 possible match, then just modify the line and - # return it, else we need to print out the matches without modifying - # the line. - elif len(possible_matches) == 1: - new_line = line_prefix + possible_matches[0] - return (new_line, len(new_line)) - else: - if second_hit: - # Only print these out if it's a second_hit - self.write(" ") - for match in possible_matches: - self.write(match) - else: - p = " ".join(line.split(" ")[:-1]) - new_line = " ".join([p, os.path.commonprefix(possible_matches)]) - if len(new_line) > len(line): - line = new_line - cursor = len(line) - return (line, cursor) - - def tab_complete_torrent(self, line): - """ - Completes torrent_ids or names. - - :param line: str, the string to complete - - :returns: list of matches - - """ - - possible_matches = [] - - # Find all possible matches - for torrent_id, torrent_name in self.torrents: - if torrent_id.startswith(line): - possible_matches.append(torrent_id + " ") - if torrent_name.startswith(line): - possible_matches.append(torrent_name + " ") - - return possible_matches - - def get_torrent_name(self, torrent_id): - """ - Gets a torrent name from the torrents list. - - :param torrent_id: str, the torrent_id - - :returns: the name of the torrent or None - """ - - for tid, name in self.torrents: - if torrent_id == tid: - return name - - return None + client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status) + client.core.get_session_state().addCallback(on_session_state) + def match_torrent(self, string): """ Returns a list of torrent_id matches for the string. It will search both @@ -435,6 +304,8 @@ class ConsoleUI(component.Component): no matches are found. """ + if self.interactive and isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy): + return self.screen.match_torrent(string) ret = [] for tid, name in self.torrents: if tid.startswith(string) or name.startswith(string): @@ -442,15 +313,40 @@ class ConsoleUI(component.Component): return ret - def on_torrent_added_event(self, event): - def on_torrent_status(status): - self.torrents.append((event.torrent_id, status["name"])) - client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status) - def on_torrent_removed_event(self, event): - for index, (tid, name) in enumerate(self.torrents): - if event.torrent_id == tid: - del self.torrents[index] + def get_torrent_name(self, torrent_id): + if self.interactive and hasattr(self.screen,"get_torrent_name"): + return self.screen.get_torrent_name(torrent_id) + + for tid, name in self.torrents: + if torrent_id == tid: + return name + + return None + + + def set_batch_write(self, batch): + if self.interactive and isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy): + return self.screen.set_batch_write(batch) + + def tab_complete_torrent(self, line): + if self.interactive and isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy): + return self.screen.tab_complete_torrent(line) + + def set_mode(self, mode): + reactor.removeReader(self.screen) + self.screen = mode + self.statusbars.screen = self.screen + reactor.addReader(self.screen) def on_client_disconnect(self): component.stop() + + def write(self, s): + if self.interactive: + if isinstance(self.screen,deluge.ui.console.modes.legacy.Legacy): + self.screen.write(s) + else: + self.events.append(s) + else: + print colors.strip_colors(s.encode(self.encoding)) diff --git a/deluge/ui/console/modes/__init__.py b/deluge/ui/console/modes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/deluge/ui/console/modes/add_util.py b/deluge/ui/console/modes/add_util.py new file mode 100644 index 000000000..b741a6a11 --- /dev/null +++ b/deluge/ui/console/modes/add_util.py @@ -0,0 +1,106 @@ +# +# add_util.py +# +# Copyright (C) 2011 Nick Lanham +# +# Modified function from commands/add.py: +# Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# +from deluge.ui.client import client +import deluge.component as component +from deluge.ui.common import TorrentInfo +import deluge.common + +import os,base64,glob + +import logging +log = logging.getLogger(__name__) + +def __bracket_fixup(path): + if (path.find("[") == -1 and + path.find("]") == -1): + return path + sentinal = 256 + while (path.find(unichr(sentinal)) != -1): + sentinal+=1 + if sentinal > 65535: + log.error("Can't fix brackets in path, path contains all possible sentinal characters") + return path + newpath = path.replace("]",unichr(sentinal)) + newpath = newpath.replace("[","[[]") + newpath = newpath.replace(unichr(sentinal),"[]]") + return newpath + +def add_torrent(t_file, options, success_cb, fail_cb, ress): + t_options = {} + if options["path"]: + t_options["download_location"] = os.path.expanduser(options["path"]) + t_options["add_paused"] = options["add_paused"] + + is_url = (not (options["path_type"]==1)) and (deluge.common.is_url(t_file) or options["path_type"]==2) + is_mag = not(is_url) and (not (options["path_type"]==1)) and deluge.common.is_magnet(t_file) + + if is_url or is_mag: + files = [t_file] + else: + files = glob.glob(__bracket_fixup(t_file)) + num_files = len(files) + ress["total"] = num_files + + if num_files <= 0: + fail_cb("Doesn't exist",t_file,ress) + + for f in files: + if is_url: + client.core.add_torrent_url(f, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress) + elif is_mag: + client.core.add_torrent_magnet(f, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress) + else: + if not os.path.exists(f): + fail_cb("Doesn't exist",f,ress) + continue + if not os.path.isfile(f): + fail_cb("Is a directory",f,ress) + continue + + try: + TorrentInfo(f) + except Exception as e: + fail_cb(e.message,f,ress) + continue + + filename = os.path.split(f)[-1] + filedump = base64.encodestring(open(f).read()) + + client.core.add_torrent_file(filename, filedump, t_options).addCallback(success_cb,f,ress).addErrback(fail_cb,f,ress) + diff --git a/deluge/ui/console/modes/alltorrents.py b/deluge/ui/console/modes/alltorrents.py new file mode 100644 index 000000000..4193abc68 --- /dev/null +++ b/deluge/ui/console/modes/alltorrents.py @@ -0,0 +1,861 @@ +# -*- coding: utf-8 -*- +# +# alltorrents.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.component as component +from basemode import BaseMode +import deluge.common +from deluge.ui.client import client +from deluge.configmanager import ConfigManager + +from collections import deque + +from deluge.ui.sessionproxy import SessionProxy + +from popup import Popup,SelectablePopup,MessagePopup +from add_util import add_torrent +from input_popup import InputPopup +from torrentdetail import TorrentDetail +from preferences import Preferences +from torrent_actions import torrent_actions_popup +from eventview import EventView +from legacy import Legacy + +import format_utils,column + +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + + +# Big help string that gets displayed when the user hits 'h' +HELP_STR = """\ +This screen shows an overview of the current torrents Deluge is managing. \ +The currently selected torrent is indicated by having a white background. \ +You can change the selected torrent using the up/down arrows or the \ +PgUp/Pg keys. Home and End keys go to the first and last torrent \ +respectively. + +Operations can be performed on multiple torrents by marking them and \ +then hitting Enter. See below for the keys used to mark torrents. + +You can scroll a popup window that doesn't fit its content (like \ +this one) using the up/down arrows. + +All popup windows can be closed/canceled by hitting the Esc key \ +(you might need to wait a second for an Esc to register) + +The actions you can perform and the keys to perform them are as follows: + +{!info!}'h'{!normal!} - Show this help + +{!info!}'a'{!normal!} - Add a torrent + +{!info!}'p'{!normal!} - View/Set preferences + +{!info!}'/'{!normal!} - Search torrent names. Enter to exectue search, ESC to cancel + +{!info!}'n'{!normal!} - Next matching torrent for last search + +{!info!}'f'{!normal!} - Show only torrents in a certain state + (Will open a popup where you can select the state you want to see) + +{!info!}'i'{!normal!} - Show more detailed information about the current selected torrent + +{!info!}'e'{!normal!} - Show the event log view ({!info!}'q'{!normal!} to get out of event log) + +{!info!}'l'{!normal!} - Go into 'legacy' mode (the way deluge-console used to work) + +{!info!}'Q'{!normal!} - quit + +{!info!}'m'{!normal!} - Mark a torrent +{!info!}'M'{!normal!} - Mark all torrents between currently selected torrent and last marked torrent +{!info!}'c'{!normal!} - Un-mark all torrents + +{!info!}Right Arrow{!normal!} - Torrent Detail Mode. This includes more detailed information \ +about the currently selected torrent, as well as a view of the \ +files in the torrent and the ability to set file priorities. + +{!info!}Enter{!normal!} - Show torrent actions popup. Here you can do things like \ +pause/resume, remove, recheck and so on. These actions \ +apply to all currently marked torrents. The currently \ +selected torrent is automatically marked when you press enter. + +{!info!}'q'/Esc{!normal!} - Close a popup (Note that Esc can take a moment to register \ +as having been pressed. +""" + +class FILTER: + ALL=0 + ACTIVE=1 + DOWNLOADING=2 + SEEDING=3 + PAUSED=4 + CHECKING=5 + ERROR=6 + QUEUED=7 + +DEFAULT_PREFS = { + "show_queue":True, + "show_name":True, + "show_size":True, + "show_state":True, + "show_progress":True, + "show_seeders":True, + "show_peers":True, + "show_downspeed":True, + "show_upspeed":True, + "show_eta":False, + "show_ratio":False, + "show_avail":False, + "show_added":False, + "show_tracker":False, + "show_savepath":False, + "show_downloaded":False, + "show_uploaded":False, + "show_owner":False, + "queue_width":5, + "name_width":-1, + "size_width":15, + "state_width":13, + "progress_width":10, + "seeders_width":10, + "peers_width":10, + "downspeed_width":15, + "upspeed_width":15, + "eta_width":10, + "ratio_width":10, + "avail_width":10, + "added_width":25, + "tracker_width":15, + "savepath_width":15, + "downloaded_width":13, + "uploaded_width":13, + "owner_width":10, +} + +column_pref_names = ["queue","name","size","state", + "progress","seeders","peers", + "downspeed","upspeed","eta", + "ratio","avail","added","tracker", + "savepath","downloaded","uploaded", + "owner"] + +prefs_to_names = { + "queue":"#", + "name":"Name", + "size":"Size", + "state":"State", + "progress":"Progress", + "seeders":"Seeders", + "peers":"Peers", + "downspeed":"Down Speed", + "upspeed":"Up Speed", + "eta":"ETA", + "ratio":"Ratio", + "avail":"Avail", + "added":"Added", + "tracker":"Tracker", + "savepath":"Save Path", + "downloaded":"Downloaded", + "uploaded":"Uploaded", + "owner":"Owner", +} + +class AllTorrents(BaseMode, component.Component): + def __init__(self, stdscr, encoding=None): + self.formatted_rows = None + self.torrent_names = None + self.cursel = 1 + self.curoff = 1 # TODO: this should really be 0 indexed + self.column_string = "" + self.popup = None + self.messages = deque() + self.marked = [] + self.last_mark = -1 + self._sorted_ids = None + self._go_top = False + + self._curr_filter = None + self.entering_search = False + self.search_string = None + self.cursor = 0 + + self.coreconfig = component.get("ConsoleUI").coreconfig + + self.legacy_mode = None + + self.__status_dict = {} + self.__torrent_info_id = None + + BaseMode.__init__(self, stdscr, encoding) + component.Component.__init__(self, "AllTorrents", 1, depend=["SessionProxy"]) + curses.curs_set(0) + self.stdscr.notimeout(0) + + self.__split_help() + self.update_config() + + component.start(["AllTorrents"]) + + self._info_fields = [ + ("Name",None,("name",)), + ("State", None, ("state",)), + ("Down Speed", format_utils.format_speed, ("download_payload_rate",)), + ("Up Speed", format_utils.format_speed, ("upload_payload_rate",)), + ("Progress", format_utils.format_progress, ("progress",)), + ("ETA", deluge.common.ftime, ("eta",)), + ("Path", None, ("save_path",)), + ("Downloaded",deluge.common.fsize,("all_time_download",)), + ("Uploaded", deluge.common.fsize,("total_uploaded",)), + ("Share Ratio", format_utils.format_float, ("ratio",)), + ("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")), + ("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")), + ("Active Time",deluge.common.ftime,("active_time",)), + ("Seeding Time",deluge.common.ftime,("seeding_time",)), + ("Date Added",deluge.common.fdate,("time_added",)), + ("Availability", format_utils.format_float, ("distributed_copies",)), + ("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")), + ] + + self.__status_keys = ["name","state","download_payload_rate","upload_payload_rate", + "progress","eta","all_time_download","total_uploaded", "ratio", + "num_seeds","total_seeds","num_peers","total_peers", "active_time", + "seeding_time","time_added","distributed_copies", "num_pieces", + "piece_length","save_path"] + + # component start/update + def start(self): + component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,False) + + def update(self): + component.get("SessionProxy").get_torrents_status(self.__status_dict, self.__status_fields).addCallback(self.set_state,True) + if self.__torrent_info_id: + component.get("SessionProxy").get_torrent_status(self.__torrent_info_id, self.__status_keys).addCallback(self._on_torrent_status) + + def update_config(self): + self.config = ConfigManager("console.conf",DEFAULT_PREFS) + self.__cols_to_show = [pref for pref in column_pref_names if self.config["show_%s"%pref]] + self.__columns = [prefs_to_names[col] for col in self.__cols_to_show] + self.__status_fields = column.get_required_fields(self.__columns) + for rf in ["state","name","queue"]: # we always need these, even if we're not displaying them + if not rf in self.__status_fields: self.__status_fields.append(rf) + self.__update_columns() + + def __split_help(self): + self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2) + + def resume(self): + self._go_top = True + component.start(["AllTorrents"]) + self.refresh() + + def __update_columns(self): + self.column_widths = [self.config["%s_width"%c] for c in self.__cols_to_show] + req = sum(filter(lambda x:x >= 0,self.column_widths)) + if (req > self.cols): # can't satisfy requests, just spread out evenly + cw = int(self.cols/len(self.__columns)) + for i in range(0,len(self.column_widths)): + self.column_widths[i] = cw + else: + rem = self.cols - req + var_cols = len(filter(lambda x: x < 0,self.column_widths)) + if (var_cols > 0): + vw = int(rem/var_cols) + for i in range(0, len(self.column_widths)): + if (self.column_widths[i] < 0): + self.column_widths[i] = vw + + self.column_string = "{!header!}%s"%("".join(["%s%s"%(self.__columns[i]," "*(self.column_widths[i]-len(self.__columns[i]))) for i in range(0,len(self.__columns))])) + + + def set_state(self, state, refresh): + self.curstate = state # cache in case we change sort order + newnames = [] + newrows = [] + self._sorted_ids = self._sort_torrents(self.curstate) + for torrent_id in self._sorted_ids: + ts = self.curstate[torrent_id] + newnames.append(ts["name"]) + newrows.append((format_utils.format_row([column.get_column_value(name,ts) for name in self.__columns],self.column_widths),ts["state"])) + + self.numtorrents = len(state) + self.formatted_rows = newrows + self.torrent_names = newnames + if refresh: + self.refresh() + + def get_torrent_name(self, torrent_id): + for p,i in enumerate(self._sorted_ids): + if torrent_id == i: + return self.torrent_names[p] + return None + + def _scroll_up(self, by): + prevoff = self.curoff + self.cursel = max(self.cursel - by,1) + if ((self.cursel - 1) < self.curoff): + self.curoff = max(self.cursel - 1,1) + return prevoff != self.curoff + + def _scroll_down(self, by): + prevoff = self.curoff + self.cursel = min(self.cursel + by,self.numtorrents) + if ((self.curoff + self.rows - 5) < self.cursel): + self.curoff = self.cursel - self.rows + 5 + return prevoff != self.curoff + + def current_torrent_id(self): + if self._sorted_ids: + return self._sorted_ids[self.cursel-1] + else: + return None + + def _selected_torrent_ids(self): + ret = [] + for i in self.marked: + ret.append(self._sorted_ids[i-1]) + return ret + + def _on_torrent_status(self, state): + if (self.popup): + self.popup.clear() + name = state["name"] + off = int((self.cols/4)-(len(name)/2)) + self.popup.set_title(name) + for i,f in enumerate(self._info_fields): + if f[1] != None: + args = [] + try: + for key in f[2]: + args.append(state[key]) + except: + log.debug("Could not get info field: %s",e) + continue + info = f[1](*args) + else: + info = state[f[2][0]] + + nl = len(f[0])+4 + if (nl+len(info))>self.popup.width: + self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info[:(self.popup.width - nl)])) + info = info[(self.popup.width - nl):] + n = self.popup.width-3 + chunks = [info[i:i+n] for i in xrange(0, len(info), n)] + for c in chunks: + self.popup.add_line(" %s"%c) + else: + self.popup.add_line("{!info!}%s: {!input!}%s"%(f[0],info)) + self.refresh() + else: + self.__torrent_info_id = None + + + def on_resize(self, *args): + BaseMode.on_resize_norefresh(self, *args) + self.__update_columns() + self.__split_help() + if self.popup: + self.popup.handle_resize() + self.refresh() + + def _queue_sort(self, v1, v2): + if v1 == v2: + return 0 + if v2 < 0: + return -1 + if v1 < 0: + return 1 + if v1 > v2: + return 1 + if v2 > v1: + return -1 + + def _sort_torrents(self, state): + "sorts by queue #" + return sorted(state,cmp=self._queue_sort,key=lambda s:state.get(s)["queue"]) + + def _format_queue(self, qnum): + if (qnum >= 0): + return "%d"%(qnum+1) + else: + return "" + + + def show_torrent_details(self,tid): + def dodeets(arg): + if arg and True in arg[0]: + self.stdscr.clear() + component.get("ConsoleUI").set_mode(TorrentDetail(self,tid,self.stdscr,self.encoding)) + else: + self.messages.append(("Error","An error occured trying to display torrent details")) + component.stop(["AllTorrents"]).addCallback(dodeets) + + def show_preferences(self): + def _on_get_config(config): + client.core.get_listen_port().addCallback(_on_get_listen_port,config) + + def _on_get_listen_port(port,config): + client.core.get_cache_status().addCallback(_on_get_cache_status,port,config) + + def _on_get_cache_status(status,port,config): + def doprefs(arg): + if arg and True in arg[0]: + self.stdscr.clear() + component.get("ConsoleUI").set_mode(Preferences(self,config,self.config,port,status,self.stdscr,self.encoding)) + else: + self.messages.append(("Error","An error occured trying to display preferences")) + component.stop(["AllTorrents"]).addCallback(doprefs) + + client.core.get_config().addCallback(_on_get_config) + + + def __show_events(self): + def doevents(arg): + if arg and True in arg[0]: + self.stdscr.clear() + component.get("ConsoleUI").set_mode(EventView(self,self.stdscr,self.encoding)) + else: + self.messages.append(("Error","An error occured trying to display events")) + component.stop(["AllTorrents"]).addCallback(doevents) + + def __legacy_mode(self): + def dolegacy(arg): + if arg and True in arg[0]: + self.stdscr.clear() + if not self.legacy_mode: + self.legacy_mode = Legacy(self.stdscr,self.encoding) + component.get("ConsoleUI").set_mode(self.legacy_mode) + self.legacy_mode.refresh() + curses.curs_set(2) + else: + self.messages.append(("Error","An error occured trying to switch to legacy mode")) + component.stop(["AllTorrents"]).addCallback(dolegacy) + + def _torrent_filter(self, idx, data): + if data==FILTER.ALL: + self.__status_dict = {} + self._curr_filter = None + elif data==FILTER.ACTIVE: + self.__status_dict = {"state":"Active"} + self._curr_filter = "Active" + elif data==FILTER.DOWNLOADING: + self.__status_dict = {"state":"Downloading"} + self._curr_filter = "Downloading" + elif data==FILTER.SEEDING: + self.__status_dict = {"state":"Seeding"} + self._curr_filter = "Seeding" + elif data==FILTER.PAUSED: + self.__status_dict = {"state":"Paused"} + self._curr_filter = "Paused" + elif data==FILTER.CHECKING: + self.__status_dict = {"state":"Checking"} + self._curr_filter = "Checking" + elif data==FILTER.ERROR: + self.__status_dict = {"state":"Error"} + self._curr_filter = "Error" + elif data==FILTER.QUEUED: + self.__status_dict = {"state":"Queued"} + self._curr_filter = "Queued" + self._go_top = True + return True + + def _show_torrent_filter_popup(self): + self.popup = SelectablePopup(self,"Filter Torrents",self._torrent_filter) + self.popup.add_line("_All",data=FILTER.ALL) + self.popup.add_line("Ac_tive",data=FILTER.ACTIVE) + self.popup.add_line("_Downloading",data=FILTER.DOWNLOADING,foreground="green") + self.popup.add_line("_Seeding",data=FILTER.SEEDING,foreground="cyan") + self.popup.add_line("_Paused",data=FILTER.PAUSED) + self.popup.add_line("_Error",data=FILTER.ERROR,foreground="red") + self.popup.add_line("_Checking",data=FILTER.CHECKING,foreground="blue") + self.popup.add_line("Q_ueued",data=FILTER.QUEUED,foreground="yellow") + + def __report_add_status(self, succ_cnt, fail_cnt, fail_msgs): + if fail_cnt == 0: + self.report_message("Torrents Added","{!success!}Sucessfully added %d torrent(s)"%succ_cnt) + else: + msg = ("{!error!}Failed to add the following %d torrent(s):\n {!error!}"%fail_cnt)+"\n {!error!}".join(fail_msgs) + if succ_cnt != 0: + msg += "\n \n{!success!}Sucessfully added %d torrent(s)"%succ_cnt + self.report_message("Torrent Add Report",msg) + + def _do_add(self, result): + log.debug("Adding Torrent(s): %s (dl path: %s) (paused: %d)",result["file"],result["path"],result["add_paused"]) + ress = {"succ":0, + "fail":0, + "fmsg":[]} + + def fail_cb(msg,t_file,ress): + log.debug("failed to add torrent: %s: %s"%(t_file,msg)) + ress["fail"]+=1 + ress["fmsg"].append("%s: %s"%(t_file,msg)) + if (ress["succ"]+ress["fail"]) >= ress["total"]: + self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) + def suc_cb(tid,t_file,ress): + if tid: + log.debug("added torrent: %s (%s)"%(t_file,tid)) + ress["succ"]+=1 + if (ress["succ"]+ress["fail"]) >= ress["total"]: + self.__report_add_status(ress["succ"],ress["fail"],ress["fmsg"]) + else: + fail_cb("Already in session (probably)",t_file,ress) + + add_torrent(result["file"],result,suc_cb,fail_cb,ress) + + def _show_torrent_add_popup(self): + dl = "" + ap = 1 + try: + dl = self.coreconfig["download_location"] + except KeyError: + pass + try: + if self.coreconfig["add_paused"]: + ap = 0 + except KeyError: + pass + + self.popup = InputPopup(self,"Add Torrent (Esc to cancel)",close_cb=self._do_add) + self.popup.add_text_input("Enter path to torrent file:","file") + self.popup.add_text_input("Enter save path:","path",dl) + self.popup.add_select_input("Add Paused:","add_paused",["Yes","No"],[True,False],ap) + self.popup.add_spaces(1) + self.popup.add_select_input("Path is:","path_type",["Auto","File","URL"],[0,1,2],0) + + def report_message(self,title,message): + self.messages.append((title,message)) + + def clear_marks(self): + self.marked = [] + self.last_mark = -1 + + def set_popup(self,pu): + self.popup = pu + self.refresh() + + def refresh(self,lines=None): + #log.error("ref") + #import traceback + #traceback.print_stack() + # Something has requested we scroll to the top of the list + if self._go_top: + self.cursel = 1 + self.curoff = 1 + self._go_top = False + + # show a message popup if there's anything queued + if self.popup == None and self.messages: + title,msg = self.messages.popleft() + self.popup = MessagePopup(self,title,msg) + + if not lines: + self.stdscr.clear() + + # Update the status bars + if self._curr_filter == None: + self.add_string(0,self.statusbars.topbar) + else: + self.add_string(0,"%s {!filterstatus!}Current filter: %s"%(self.statusbars.topbar,self._curr_filter)) + self.add_string(1,self.column_string) + + if self.entering_search: + self.add_string(self.rows - 1,"{!black,white!}Search torrents: %s"%self.search_string) + else: + hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) + self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) + + # add all the torrents + if self.formatted_rows == []: + msg = "No torrents match filter".center(self.cols) + self.add_string(3, "{!info!}%s"%msg) + elif self.formatted_rows: + tidx = self.curoff + currow = 2 + + if lines: + todraw = [] + for l in lines: + todraw.append(self.formatted_rows[l]) + lines.reverse() + else: + todraw = self.formatted_rows[tidx-1:] + + for row in todraw: + # default style + fg = "white" + bg = "black" + attr = None + if lines: + tidx = lines.pop()+1 + currow = tidx-self.curoff+2 + + if tidx in self.marked: + bg = "blue" + attr = "bold" + + if tidx == self.cursel: + bg = "white" + attr = "bold" + if tidx in self.marked: + fg = "blue" + else: + fg = "black" + + if row[1] == "Downloading": + fg = "green" + elif row[1] == "Seeding": + fg = "cyan" + elif row[1] == "Error": + fg = "red" + elif row[1] == "Queued": + fg = "yellow" + elif row[1] == "Checking": + fg = "blue" + + if attr: + colorstr = "{!%s,%s,%s!}"%(fg,bg,attr) + else: + colorstr = "{!%s,%s!}"%(fg,bg) + + self.add_string(currow,"%s%s"%(colorstr,row[0]),trim=False) + tidx += 1 + currow += 1 + if (currow > (self.rows - 2)): + break + else: + self.add_string(1, "Waiting for torrents from core...") + + #self.stdscr.redrawwin() + if self.entering_search: + curses.curs_set(2) + self.stdscr.move(self.rows-1,self.cursor+17) + else: + curses.curs_set(0) + + self.stdscr.noutrefresh() + + if self.popup: + self.popup.refresh() + + curses.doupdate() + + + def _mark_unmark(self,idx): + if idx in self.marked: + self.marked.remove(idx) + self.last_mark = -1 + else: + self.marked.append(idx) + self.last_mark = idx + + def __do_search(self): + # search forward for the next torrent matching self.search_string + for i,n in enumerate(self.torrent_names[self.cursel:]): + if n.find(self.search_string) >= 0: + self.cursel += (i+1) + if ((self.curoff + self.rows - 5) < self.cursel): + self.curoff = self.cursel - self.rows + 5 + return + + def __update_search(self, c): + if c == curses.KEY_BACKSPACE or c == 127: + if self.search_string and self.cursor > 0: + self.search_string = self.search_string[:self.cursor - 1] + self.search_string[self.cursor:] + self.cursor-=1 + elif c == curses.KEY_DC: + if self.search_string and self.cursor < len(self.search_string): + self.search_string = self.search_string[:self.cursor] + self.search_string[self.cursor+1:] + elif c == curses.KEY_LEFT: + self.cursor = max(0,self.cursor-1) + elif c == curses.KEY_RIGHT: + self.cursor = min(len(self.search_string),self.cursor+1) + elif c == curses.KEY_HOME: + self.cursor = 0 + elif c == curses.KEY_END: + self.cursor = len(self.search_string) + elif c == 27: + self.search_string = None + self.entering_search = False + elif c == 10 or c == curses.KEY_ENTER: + self.entering_search = False + if self.search_string: + self.__do_search() + else: + self.search_string = None + elif c > 31 and c < 256: + stroke = chr(c) + uchar = "" + while not uchar: + try: + uchar = stroke.decode(self.encoding) + except UnicodeDecodeError: + c = self.stdscr.getch() + stroke += chr(c) + if uchar: + if self.cursor == len(self.search_string): + self.search_string += uchar + else: + # Insert into string + self.search_string = self.search_string[:self.cursor] + uchar + self.search_string[self.cursor:] + # Move the cursor forward + self.cursor+=1 + + def _doRead(self): + # Read the character + effected_lines = None + + c = self.stdscr.getch() + + if self.popup: + if self.popup.handle_read(c): + self.popup = None + self.refresh() + return + + if c > 31 and c < 256: + if chr(c) == 'Q': + from twisted.internet import reactor + if client.connected(): + def on_disconnect(result): + reactor.stop() + client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() + return + + if self.formatted_rows==None or self.popup: + return + + elif self.entering_search: + self.__update_search(c) + self.refresh([]) + return + + #log.error("pressed key: %d\n",c) + #if c == 27: # handle escape + # log.error("CANCEL") + + # Navigate the torrent list + if c == curses.KEY_UP: + if self.cursel == 1: return + if not self._scroll_up(1): + effected_lines = [self.cursel-1,self.cursel] + elif c == curses.KEY_PPAGE: + self._scroll_up(int(self.rows/2)) + elif c == curses.KEY_DOWN: + if self.cursel >= self.numtorrents: return + if not self._scroll_down(1): + effected_lines = [self.cursel-2,self.cursel-1] + elif c == curses.KEY_NPAGE: + self._scroll_down(int(self.rows/2)) + elif c == curses.KEY_HOME: + self._scroll_up(self.cursel) + elif c == curses.KEY_END: + self._scroll_down(self.numtorrents-self.cursel) + + elif c == curses.KEY_RIGHT: + # We enter a new mode for the selected torrent here + tid = self.current_torrent_id() + if tid: + self.show_torrent_details(tid) + return + + # Enter Key + elif (c == curses.KEY_ENTER or c == 10) and self.numtorrents: + if self.cursel not in self.marked: + self.marked.append(self.cursel) + self.last_mark = self.cursel + torrent_actions_popup(self,self._selected_torrent_ids(),details=True) + return + else: + if c > 31 and c < 256: + if chr(c) == '/': + self.search_string = "" + self.cursor = 0 + self.entering_search = True + elif chr(c) == 'n' and self.search_string: + self.__do_search() + elif chr(c) == 'j': + if not self._scroll_up(1): + effected_lines = [self.cursel-1,self.cursel] + elif chr(c) == 'k': + if not self._scroll_down(1): + effected_lines = [self.cursel-2,self.cursel-1] + elif chr(c) == 'i': + cid = self.current_torrent_id() + if cid: + def cb(): self.__torrent_info_id = None + self.popup = Popup(self,"Info",close_cb=cb) + self.popup.add_line("Getting torrent info...") + self.__torrent_info_id = cid + elif chr(c) == 'm': + self._mark_unmark(self.cursel) + effected_lines = [self.cursel-1] + elif chr(c) == 'M': + if self.last_mark >= 0: + if (self.cursel+1) > self.last_mark: + mrange = range(self.last_mark,self.cursel+1) + else: + mrange = range(self.cursel-1,self.last_mark) + self.marked.extend(mrange[1:]) + effected_lines = mrange + else: + self._mark_unmark(self.cursel) + effected_lines = [self.cursel-1] + elif chr(c) == 'c': + self.marked = [] + self.last_mark = -1 + elif chr(c) == 'a': + self._show_torrent_add_popup() + elif chr(c) == 'f': + self._show_torrent_filter_popup() + elif chr(c) == 'h': + self.popup = Popup(self,"Help",init_lines=self.__help_lines) + elif chr(c) == 'p': + self.show_preferences() + return + elif chr(c) == 'e': + self.__show_events() + return + elif chr(c) == 'l': + self.__legacy_mode() + return + + self.refresh(effected_lines) diff --git a/deluge/ui/console/modes/basemode.py b/deluge/ui/console/modes/basemode.py new file mode 100644 index 000000000..2dd9b87d7 --- /dev/null +++ b/deluge/ui/console/modes/basemode.py @@ -0,0 +1,240 @@ +# +# basemode.py +# +# Copyright (C) 2011 Nick Lanham +# +# Most code in this file taken from screen.py: +# Copyright (C) 2009 Andrew Resch +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import sys +import logging +try: + import curses +except ImportError: + pass + +import deluge.component as component +import deluge.ui.console.colors as colors +try: + import signal + from fcntl import ioctl + import termios + import struct +except: + pass + +from twisted.internet import reactor + +log = logging.getLogger(__name__) + +class CursesStdIO(object): + """fake fd to be registered as a reader with the twisted reactor. + Curses classes needing input should extend this""" + + def fileno(self): + """ We want to select on FD 0 """ + return 0 + + def doRead(self): + """called when input is ready""" + pass + def logPrefix(self): return 'CursesClient' + + +class BaseMode(CursesStdIO): + def __init__(self, stdscr, encoding=None, do_refresh=True): + """ + A mode that provides a curses screen designed to run as a reader in a twisted reactor. + This mode doesn't do much, just shows status bars and "Base Mode" on the screen + + Modes should subclass this and provide overrides for: + + _doRead(self) - Handle user input + refresh(self) - draw the mode to the screen + add_string(self, row, string) - add a string of text to be displayed. + see method for detailed info + + The init method of a subclass *must* call BaseMode.__init__ + + Useful fields after calling BaseMode.__init__: + self.stdscr - the curses screen + self.rows - # of rows on the curses screen + self.cols - # of cols on the curses screen + self.topbar - top statusbar + self.bottombar - bottom statusbar + """ + log.debug("BaseMode init!") + self.stdscr = stdscr + # Make the input calls non-blocking + self.stdscr.nodelay(1) + + # Strings for the 2 status bars + self.statusbars = component.get("StatusBars") + + # Keep track of the screen size + self.rows, self.cols = self.stdscr.getmaxyx() + try: + signal.signal(signal.SIGWINCH, self.on_resize) + except Exception, e: + log.debug("Unable to catch SIGWINCH signal!") + + if not encoding: + self.encoding = sys.getdefaultencoding() + else: + self.encoding = encoding + + colors.init_colors() + + # Do a refresh right away to draw the screen + if do_refresh: + self.refresh() + + def on_resize_norefresh(self, *args): + log.debug("on_resize_from_signal") + # Get the new rows and cols value + self.rows, self.cols = struct.unpack("hhhh", ioctl(0, termios.TIOCGWINSZ ,"\000"*8))[0:2] + curses.resizeterm(self.rows, self.cols) + + def on_resize(self, *args): + self.on_resize_norefresh(args) + self.refresh() + + def connectionLost(self, reason): + self.close() + + def add_string(self, row, string, scr=None, col = 0, pad=True, trim=True): + """ + Adds a string to the desired `:param:row`. + + :param row: int, the row number to write the string + :param string: string, the string of text to add + :param scr: curses.window, optional window to add string to instead of self.stdscr + :param col: int, optional starting column offset + :param pad: bool, optional bool if the string should be padded out to the width of the screen + :param trim: bool, optional bool if the string should be trimmed if it is too wide for the screen + + The text can be formatted with color using the following format: + + "{!fg, bg, attributes, ...!}" + + See: http://docs.python.org/library/curses.html#constants for attributes. + + Alternatively, it can use some built-in scheme for coloring. + See colors.py for built-in schemes. + + "{!scheme!}" + + Examples: + + "{!blue, black, bold!}My Text is {!white, black!}cool" + "{!info!}I am some info text!" + "{!error!}Uh oh!" + + + """ + if scr: + screen = scr + else: + screen = self.stdscr + try: + parsed = colors.parse_color_string(string, self.encoding) + except colors.BadColorString, e: + log.error("Cannot add bad color string %s: %s", string, e) + return + + for index, (color, s) in enumerate(parsed): + if index + 1 == len(parsed) and pad: + # This is the last string so lets append some " " to it + s += " " * (self.cols - (col + len(s)) - 1) + if trim: + y,x = screen.getmaxyx() + if (col+len(s)) > x: + s = "%s..."%s[0:x-4-col] + screen.addstr(row, col, s, color) + col += len(s) + + def draw_statusbars(self): + self.add_string(0, self.statusbars.topbar) + self.add_string(self.rows - 1, self.statusbars.bottombar) + + # This mode doesn't report errors + def report_message(self): + pass + + # This mode doesn't do anything with popups + def set_popup(self,popup): + pass + + # This mode doesn't support marking + def clear_marks(self): + pass + + def refresh(self): + """ + Refreshes the screen. + Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset` + attribute and the status bars. + """ + self.stdscr.clear() + self.draw_statusbars() + # Update the status bars + + self.add_string(1,"{!info!}Base Mode (or subclass hasn't overridden refresh)") + + self.stdscr.redrawwin() + self.stdscr.refresh() + + def doRead(self): + """ + Called when there is data to be read, ie, input from the keyboard. + """ + # We wrap this function to catch exceptions and shutdown the mainloop + try: + self._doRead() + except Exception, e: + log.exception(e) + reactor.stop() + + def _doRead(self): + # Read the character + c = self.stdscr.getch() + self.stdscr.refresh() + + def close(self): + """ + Clean up the curses stuff on exit. + """ + curses.nocbreak() + self.stdscr.keypad(0) + curses.echo() + curses.endwin() diff --git a/deluge/ui/console/modes/column.py b/deluge/ui/console/modes/column.py new file mode 100644 index 000000000..fcc7878a0 --- /dev/null +++ b/deluge/ui/console/modes/column.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# column.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.common +import format_utils + +import logging +log = logging.getLogger(__name__) + + +def format_queue(qnum): + if (qnum >= 0): + return "%d"%(qnum+1) + else: + return "" + +columns = { + "#":(("queue",),format_queue), + "Name":(("name",),None), + "Size":(("total_wanted",),deluge.common.fsize), + "State":(("state",),None), + "Progress":(("progress",),format_utils.format_progress), + "Seeders":(("num_seeds","total_seeds"),format_utils.format_seeds_peers), + "Peers":(("num_peers","total_peers"),format_utils.format_seeds_peers), + "Down Speed":(("download_payload_rate",),format_utils.format_speed), + "Up Speed":(("upload_payload_rate",),format_utils.format_speed), + "ETA":(("eta",), format_utils.format_time), + "Ratio":(("ratio",), format_utils.format_float), + "Avail":(("distributed_copies",), format_utils.format_float), + "Added":(("time_added",), deluge.common.fdate), + "Tracker":(("tracker_host",), None), + "Save Path":(("save_path",), None), + "Downloaded":(("all_time_download",), deluge.common.fsize), + "Uploaded":(("total_uploaded",), deluge.common.fsize), + "Owner":(("owner",),None) + } + +def get_column_value(name,state): + try: + col = columns[name] + except KeyError: + log.error("No such column: %s",name) + return None + + if col[1] != None: + args = [] + try: + for key in col[0]: + args.append(state[key]) + except: + log.error("Could not get column field: %s",col[0]) + return None + colval = col[1](*args) + else: + colval = state[col[0][0]] + return colval + + +def get_required_fields(cols): + fields = [] + for col in cols: + fields.extend(columns.get(col)[0]) + return fields + + + diff --git a/deluge/ui/console/modes/connectionmanager.py b/deluge/ui/console/modes/connectionmanager.py new file mode 100644 index 000000000..4f9b4686a --- /dev/null +++ b/deluge/ui/console/modes/connectionmanager.py @@ -0,0 +1,219 @@ +# +# connectionmanager.py +# +# Copyright (C) 2007-2009 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +# a mode that show's a popup to select which host to connect to + +import hashlib,time + +from collections import deque + +import deluge.ui.client +from deluge.ui.client import client +from deluge.configmanager import ConfigManager +from deluge.ui.coreconfig import CoreConfig +import deluge.component as component + +from alltorrents import AllTorrents +from basemode import BaseMode +from popup import SelectablePopup,MessagePopup +from input_popup import InputPopup + + +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 58846 + +DEFAULT_CONFIG = { + "hosts": [(hashlib.sha1(str(time.time())).hexdigest(), DEFAULT_HOST, DEFAULT_PORT, "", "")] +} + + +class ConnectionManager(BaseMode): + def __init__(self, stdscr, encoding=None): + self.popup = None + self.statuses = {} + self.messages = deque() + self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) + BaseMode.__init__(self, stdscr, encoding) + self.__update_statuses() + self.__update_popup() + + def __update_popup(self): + self.popup = SelectablePopup(self,"Select Host",self.__host_selected) + self.popup.add_line("{!white,black,bold!}'Q'=quit, 'r'=refresh, 'a'=add new host, 'D'=delete host",selectable=False) + for host in self.config["hosts"]: + if host[0] in self.statuses: + self.popup.add_line("%s:%d [Online] (%s)"%(host[1],host[2],self.statuses[host[0]]),data=host[0],foreground="green") + else: + self.popup.add_line("%s:%d [Offline]"%(host[1],host[2]),data=host[0],foreground="red") + self.inlist = True + self.refresh() + + def __update_statuses(self): + """Updates the host status""" + def on_connect(result, c, host_id): + def on_info(info, c): + self.statuses[host_id] = info + self.__update_popup() + c.disconnect() + + def on_info_fail(reason, c): + if host_id in self.statuses: + del self.statuses[host_id] + c.disconnect() + + d = c.daemon.info() + d.addCallback(on_info, c) + d.addErrback(on_info_fail, c) + + def on_connect_failed(reason, host_id): + if host_id in self.statuses: + del self.statuses[host_id] + + for host in self.config["hosts"]: + c = deluge.ui.client.Client() + hadr = host[1] + port = host[2] + user = host[3] + password = host[4] + d = c.connect(hadr, port, user, password) + d.addCallback(on_connect, c, host[0]) + d.addErrback(on_connect_failed, host[0]) + + def __on_connected(self,result): + component.start() + self.stdscr.clear() + at = AllTorrents(self.stdscr, self.encoding) + component.get("ConsoleUI").set_mode(at) + at.resume() + + def __host_selected(self, idx, data): + for host in self.config["hosts"]: + if host[0] == data and host[0] in self.statuses: + client.connect(host[1], host[2], host[3], host[4]).addCallback(self.__on_connected) + return False + + def __do_add(self,result): + hostname = result["hostname"] + try: + port = int(result["port"]) + except ValueError: + self.report_message("Can't add host","Invalid port. Must be an integer") + return + username = result["username"] + password = result["password"] + for host in self.config["hosts"]: + if (host[1],host[2],host[3]) == (hostname, port, username): + self.report_message("Can't add host","Host already in list") + return + newid = hashlib.sha1(str(time.time())).hexdigest() + self.config["hosts"].append((newid, hostname, port, username, password)) + self.config.save() + self.__update_popup() + + def __add_popup(self): + self.inlist = False + self.popup = InputPopup(self,"Add Host (esc to cancel)",close_cb=self.__do_add) + self.popup.add_text_input("Hostname:","hostname") + self.popup.add_text_input("Port:","port") + self.popup.add_text_input("Username:","username") + self.popup.add_text_input("Password:","password") + self.refresh() + + def __delete_current_host(self): + idx,data = self.popup.current_selection() + log.debug("deleting host: %s",data) + for host in self.config["hosts"]: + if host[0] == data: + self.config["hosts"].remove(host) + break + self.config.save() + + def report_message(self,title,message): + self.messages.append((title,message)) + + def refresh(self): + self.stdscr.clear() + self.draw_statusbars() + self.stdscr.noutrefresh() + + if self.popup == None and self.messages: + title,msg = self.messages.popleft() + self.popup = MessagePopup(self,title,msg) + + if not self.popup: + self.__update_popup() + + self.popup.refresh() + curses.doupdate() + + + def _doRead(self): + # Read the character + c = self.stdscr.getch() + + if c > 31 and c < 256: + if chr(c) == 'q' and self.inlist: return + if chr(c) == 'Q': + from twisted.internet import reactor + if client.connected(): + def on_disconnect(result): + reactor.stop() + client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() + return + if chr(c) == 'D' and self.inlist: + self.__delete_current_host() + self.__update_popup() + return + if chr(c) == 'r' and self.inlist: + self.__update_statuses() + if chr(c) == 'a' and self.inlist: + self.__add_popup() + return + + if self.popup: + if self.popup.handle_read(c): + self.popup = None + self.refresh() + return diff --git a/deluge/ui/console/modes/eventview.py b/deluge/ui/console/modes/eventview.py new file mode 100644 index 000000000..31fb606a0 --- /dev/null +++ b/deluge/ui/console/modes/eventview.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# eventview.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.component as component +from basemode import BaseMode +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + +class EventView(BaseMode): + def __init__(self, parent_mode, stdscr, encoding=None): + self.parent_mode = parent_mode + BaseMode.__init__(self, stdscr, encoding) + + def refresh(self): + "This method just shows each line of the event log" + events = component.get("ConsoleUI").events + + self.add_string(0,self.statusbars.topbar) + hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) + self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) + + if events: + for i,event in enumerate(events): + self.add_string(i+1,event) + else: + self.add_string(1,"{!white,black,bold!}No events to show yet") + + self.stdscr.noutrefresh() + curses.doupdate() + + def back_to_overview(self): + self.stdscr.clear() + component.get("ConsoleUI").set_mode(self.parent_mode) + self.parent_mode.resume() + + def _doRead(self): + c = self.stdscr.getch() + + if c > 31 and c < 256: + if chr(c) == 'Q': + from twisted.internet import reactor + if client.connected(): + def on_disconnect(result): + reactor.stop() + client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() + return + elif chr(c) == 'q': + self.back_to_overview() + return + + if c == 27: + self.back_to_overview() + return + + # TODO: Scroll event list + if c == curses.KEY_UP: + pass + elif c == curses.KEY_PPAGE: + pass + elif c == curses.KEY_DOWN: + pass + elif c == curses.KEY_NPAGE: + pass + + #self.refresh() diff --git a/deluge/ui/console/modes/format_utils.py b/deluge/ui/console/modes/format_utils.py new file mode 100644 index 000000000..fa064f812 --- /dev/null +++ b/deluge/ui/console/modes/format_utils.py @@ -0,0 +1,199 @@ +# -*- coding: utf-8 -*- +# +# format_utils.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.common +try: + import unicodedata + haveud = True +except: + haveud = False + +def format_speed(speed): + if (speed > 0): + return deluge.common.fspeed(speed) + else: + return "-" + +def format_time(time): + if (time > 0): + return deluge.common.ftime(time) + else: + return "-" + +def format_float(x): + if x < 0: + return "∞" + else: + return "%.3f"%x + +def format_seeds_peers(num, total): + return "%d (%d)"%(num,total) + +def format_progress(perc): + if perc < 100: + return "%.2f%%"%perc + else: + return "100%" + +def format_pieces(num, size): + return "%d (%s)"%(num,deluge.common.fsize(size)) + +def format_priority(prio): + if prio == -2: return "[Mixed]" + if prio < 0: return "-" + pstring = deluge.common.FILE_PRIORITY[prio] + if prio > 0: + return pstring[:pstring.index("Priority")-1] + else: + return pstring + +def trim_string(string, w, have_dbls): + if w <= 0: + return "" + elif w == 1: + return "…" + elif have_dbls: + # have to do this the slow way + chrs = [] + width = 4 + idx = 0 + while width < w: + chrs.append(string[idx]) + if unicodedata.east_asian_width(string[idx]) in ['W','F']: + width += 2 + else: + width += 1 + idx += 1 + if width != w: + chrs.pop() + chrs.append('.') + return "%s… "%("".join(chrs)) + else: + return "%s… "%(string[0:w-2]) + +def format_column(col, lim): + dbls = 0 + if haveud and isinstance(col,unicode): + # might have some double width chars + col = unicodedata.normalize("NFC",col) + for c in col: + if unicodedata.east_asian_width(c) in ['W','F']: + # found a wide/full char + dbls += 1 + size = len(col)+dbls + if (size >= lim - 1): + return trim_string(col,lim,dbls>0) + else: + return "%s%s"%(col," "*(lim-size)) + +def format_row(row,column_widths): + return "".join([format_column(row[i],column_widths[i]) for i in range(0,len(row))]) + +import re +from collections import deque +_strip_re = re.compile("\{!.*?!\}") +def wrap_string(string,width,min_lines=0,strip_colors=True): + """ + Wrap a string to fit in a particular width. Returns a list of output lines. + + :param string: str, the string to wrap + :param width: int, the maximum width of a line of text + :param min_lines: int, extra lines will be added so the output tuple contains at least min_lines lines + :param strip_colors: boolean, if True, text in {!!} blocks will not be considered as adding to the + width of the line. They will still be present in the output. + """ + ret = [] + s1 = string.split("\n") + + def insert_clr(s,offset,mtchs,clrs): + end_pos = offset+len(s) + while mtchs and (mtchs[0] <= end_pos) and (mtchs[0] >= offset): + mtc = mtchs.popleft()-offset + clr = clrs.popleft() + end_pos += len(clr) + s = "%s%s%s"%(s[:mtc],clr,s[mtc:]) + return s + + for s in s1: + cur_pos = offset = 0 + if strip_colors: + mtchs = deque() + clrs = deque() + for m in _strip_re.finditer(s): + mtchs.append(m.start()) + clrs.append(m.group()) + cstr = _strip_re.sub('',s) + else: + cstr = s + while len(cstr) > width: + sidx = cstr.rfind(" ",0,width-1) + sidx += 1 + if sidx > 0: + if strip_colors: + to_app = cstr[0:sidx] + to_app = insert_clr(to_app,offset,mtchs,clrs) + ret.append(to_app) + offset += len(to_app) + else: + ret.append(cstr[0:sidx]) + cstr = cstr[sidx:] + if not cstr: + cstr = None + break + else: + # can't find a reasonable split, just split at width + if strip_colors: + to_app = cstr[0:width] + to_app = insert_clr(to_app,offset,mtchs,clrs) + ret.append(to_app) + offset += len(to_app) + else: + ret.append(cstr[0:width]) + cstr = cstr[width:] + if not cstr: + cstr = None + break + if cstr != None: + if strip_colors: + ret.append(insert_clr(cstr,offset,mtchs,clrs)) + else: + ret.append(cstr) + + if min_lines>0: + for i in range(len(ret),min_lines): + ret.append(" ") + + return ret diff --git a/deluge/ui/console/modes/input_popup.py b/deluge/ui/console/modes/input_popup.py new file mode 100644 index 000000000..abe804285 --- /dev/null +++ b/deluge/ui/console/modes/input_popup.py @@ -0,0 +1,660 @@ +# +# input_popup.py +# +# Copyright (C) 2011 Nick Lanham +# +# Complete function from commands/add.py: +# Copyright (C) 2008-2009 Ido Abramovich +# Copyright (C) 2009 Andrew Resch +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +try: + import curses +except ImportError: + pass + +import logging,os,os.path + +from popup import Popup + +log = logging.getLogger(__name__) + +class InputField: + depend = None + # render the input. return number of rows taken up + def render(self,screen,row,width,selected,col=1): + return 0 + def handle_read(self, c): + if c in [curses.KEY_ENTER, 10, 127, 113]: + return True + return False + def get_value(self): + return None + def set_value(self, value): + pass + + def set_depend(self,i,inverse=False): + if not isinstance(i,CheckedInput): + raise Exception("Can only depend on CheckedInputs") + self.depend = i + self.inverse = inverse + + def depend_skip(self): + if not self.depend: + return False + if self.inverse: + return self.depend.checked + else: + return not self.depend.checked + +class CheckedInput(InputField): + def __init__(self, parent, message, name, checked=False): + self.parent = parent + self.chkd_inact = "[X] %s"%message + self.unchkd_inact = "[ ] %s"%message + self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message + self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message + self.name = name + self.checked = checked + + def render(self, screen, row, width, active, col=1): + if self.checked and active: + self.parent.add_string(row,self.chkd_act,screen,col,False,True) + elif self.checked: + self.parent.add_string(row,self.chkd_inact,screen,col,False,True) + elif active: + self.parent.add_string(row,self.unchkd_act,screen,col,False,True) + else: + self.parent.add_string(row,self.unchkd_inact,screen,col,False,True) + return 1 + + def handle_read(self, c): + if c == 32: + self.checked = not self.checked + + def get_value(self): + return self.checked + + def set_value(self, c): + self.checked = c + + +class CheckedPlusInput(InputField): + def __init__(self, parent, message, name, child,checked=False): + self.parent = parent + self.chkd_inact = "[X] %s"%message + self.unchkd_inact = "[ ] %s"%message + self.chkd_act = "[{!black,white,bold!}X{!white,black!}] %s"%message + self.unchkd_act = "[{!black,white,bold!} {!white,black!}] %s"%message + self.name = name + self.checked = checked + self.msglen = len(self.chkd_inact)+1 + self.child = child + self.child_active = False + + def render(self, screen, row, width, active, col=1): + isact = active and not self.child_active + if self.checked and isact: + self.parent.add_string(row,self.chkd_act,screen,col,False,True) + elif self.checked: + self.parent.add_string(row,self.chkd_inact,screen,col,False,True) + elif isact: + self.parent.add_string(row,self.unchkd_act,screen,col,False,True) + else: + self.parent.add_string(row,self.unchkd_inact,screen,col,False,True) + + if active and self.checked and self.child_active: + self.parent.add_string(row+1,"(esc to leave)",screen,col,False,True) + elif active and self.checked: + self.parent.add_string(row+1,"(right arrow to edit)",screen,col,False,True) + rows = 2 + # show child + if self.checked: + if isinstance(self.child,(TextInput,IntSpinInput,FloatSpinInput)): + crows = self.child.render(screen,row,width-self.msglen,self.child_active and active,col+self.msglen,self.msglen) + else: + crows = self.child.render(screen,row,width-self.msglen,self.child_active and active,col+self.msglen) + rows = max(rows,crows) + else: + self.parent.add_string(row,"(enable to view/edit value)",screen,col+self.msglen,False,True) + return rows + + def handle_read(self, c): + if self.child_active: + if c == 27: # leave child on esc + self.child_active = False + return + # pass keys through to child + self.child.handle_read(c) + else: + if c == 32: + self.checked = not self.checked + if c == curses.KEY_RIGHT: + self.child_active = True + + def get_value(self): + return self.checked + + def set_value(self, c): + self.checked = c + + def get_child(self): + return self.child + + + +class IntSpinInput(InputField): + def __init__(self, parent, message, name, move_func, value, min_val, max_val): + self.parent = parent + self.message = message + self.name = name + self.value = int(value) + self.initvalue = self.value + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string + self.move_func = move_func + self.min_val = min_val + self.max_val = max_val + self.need_update = False + + def render(self, screen, row, width, active, col=1, cursor_offset=0): + if not active and self.need_update: + if not self.valstr or self.valstr == '-': + self.value = self.initvalue + else: + self.value = int(self.valstr) + if self.value < self.min_val: + self.value = self.min_val + if self.value > self.max_val: + self.value = self.max_val + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + self.need_update = False + if not self.valstr: + self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True) + elif active: + self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True) + else: + self.parent.add_string(row,"%s [ %s ]"%(self.message,self.valstr),screen,col,False,True) + + if active: + self.move_func(row,self.cursor+self.cursoff+cursor_offset) + + return 1 + + def handle_read(self, c): + if c == curses.KEY_PPAGE and self.value < self.max_val: + self.value+=1 + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + elif c == curses.KEY_NPAGE and self.value > self.min_val: + self.value-=1 + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + elif c == curses.KEY_LEFT: + self.cursor = max(0,self.cursor-1) + elif c == curses.KEY_RIGHT: + self.cursor = min(len(self.valstr),self.cursor+1) + elif c == curses.KEY_HOME: + self.cursor = 0 + elif c == curses.KEY_END: + self.cursor = len(self.valstr) + elif c == curses.KEY_BACKSPACE or c == 127: + if self.valstr and self.cursor > 0: + self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:] + self.cursor-=1 + self.need_update = True + elif c == curses.KEY_DC: + if self.valstr and self.cursor < len(self.valstr): + self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:] + self.need_update = True + elif c == 45 and self.cursor == 0 and self.min_val < 0: + minus_place = self.valstr.find('-') + if minus_place >= 0: return + self.valstr = chr(c)+self.valstr + self.cursor += 1 + self.need_update = True + elif c > 47 and c < 58: + if c == 48 and self.cursor == 0: return + minus_place = self.valstr.find('-') + if self.cursor <= minus_place: return + if self.cursor == len(self.valstr): + self.valstr += chr(c) + else: + # Insert into string + self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:] + self.need_update = True + # Move the cursor forward + self.cursor+=1 + + + def get_value(self): + return self.value + + def set_value(self, val): + self.value = int(val) + self.valstr = "%d"%self.value + self.cursor = len(self.valstr) + + +class FloatSpinInput(InputField): + def __init__(self, parent, message, name, move_func, value, inc_amt, precision, min_val, max_val): + self.parent = parent + self.message = message + self.name = name + self.precision = precision + self.inc_amt = inc_amt + self.value = round(float(value),self.precision) + self.initvalue = self.value + self.fmt = "%%.%df"%precision + self.valstr = self.fmt%self.value + self.cursor = len(self.valstr) + self.cursoff = len(self.message)+4 # + 4 for the " [ " in the rendered string + self.move_func = move_func + self.min_val = min_val + self.max_val = max_val + self.need_update = False + + def __limit_value(self): + if self.value < self.min_val: + self.value = self.min_val + if self.value > self.max_val: + self.value = self.max_val + + def render(self, screen, row, width, active, col=1, cursor_offset=0): + if not active and self.need_update: + try: + self.value = round(float(self.valstr),self.precision) + self.__limit_value() + except ValueError: + self.value = self.initvalue + self.valstr = self.fmt%self.value + self.cursor = len(self.valstr) + self.need_update = False + if not self.valstr: + self.parent.add_string(row,"%s [ ]"%self.message,screen,col,False,True) + elif active: + self.parent.add_string(row,"%s [ {!black,white,bold!}%s{!white,black!} ]"%(self.message,self.valstr),screen,col,False,True) + else: + self.parent.add_string(row,"%s [ %s ]"%(self.message,self.valstr),screen,col,False,True) + if active: + self.move_func(row,self.cursor+self.cursoff+cursor_offset) + + return 1 + + def handle_read(self, c): + if c == curses.KEY_PPAGE: + self.value+=self.inc_amt + self.__limit_value() + self.valstr = self.fmt%self.value + self.cursor = len(self.valstr) + elif c == curses.KEY_NPAGE: + self.value-=self.inc_amt + self.__limit_value() + self.valstr = self.fmt%self.value + self.cursor = len(self.valstr) + elif c == curses.KEY_LEFT: + self.cursor = max(0,self.cursor-1) + elif c == curses.KEY_RIGHT: + self.cursor = min(len(self.valstr),self.cursor+1) + elif c == curses.KEY_HOME: + self.cursor = 0 + elif c == curses.KEY_END: + self.cursor = len(self.valstr) + elif c == curses.KEY_BACKSPACE or c == 127: + if self.valstr and self.cursor > 0: + self.valstr = self.valstr[:self.cursor - 1] + self.valstr[self.cursor:] + self.cursor-=1 + self.need_update = True + elif c == curses.KEY_DC: + if self.valstr and self.cursor < len(self.valstr): + self.valstr = self.valstr[:self.cursor] + self.valstr[self.cursor+1:] + self.need_update = True + elif c == 45 and self.cursor == 0 and self.min_val < 0: + minus_place = self.valstr.find('-') + if minus_place >= 0: return + self.valstr = chr(c)+self.valstr + self.cursor += 1 + self.need_update = True + elif c == 46: + minus_place = self.valstr.find('-') + if self.cursor <= minus_place: return + point_place = self.valstr.find('.') + if point_place >= 0: return + if self.cursor == len(self.valstr): + self.valstr += chr(c) + else: + # Insert into string + self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:] + self.need_update = True + # Move the cursor forward + self.cursor+=1 + elif (c > 47 and c < 58): + minus_place = self.valstr.find('-') + if self.cursor <= minus_place: return + if self.cursor == len(self.valstr): + self.valstr += chr(c) + else: + # Insert into string + self.valstr = self.valstr[:self.cursor] + chr(c) + self.valstr[self.cursor:] + self.need_update = True + # Move the cursor forward + self.cursor+=1 + + + def get_value(self): + return self.value + + def set_value(self, val): + self.value = round(float(val),self.precision) + self.valstr = self.fmt%self.value + self.cursor = len(self.valstr) + + +class SelectInput(InputField): + def __init__(self, parent, message, name, opts, vals, selidx): + self.parent = parent + self.message = message + self.name = name + self.opts = opts + self.vals = vals + self.selidx = selidx + + def render(self, screen, row, width, selected, col=1): + if self.message: + self.parent.add_string(row,self.message,screen,col,False,True) + row += 1 + off = col+1 + for i,opt in enumerate(self.opts): + if selected and i == self.selidx: + self.parent.add_string(row,"{!black,white,bold!}[%s]"%opt,screen,off,False,True) + elif i == self.selidx: + self.parent.add_string(row,"[{!white,black,underline!}%s{!white,black!}]"%opt,screen,off,False,True) + else: + self.parent.add_string(row,"[%s]"%opt,screen,off,False,True) + off += len(opt)+3 + if self.message: + return 2 + else: + return 1 + + def handle_read(self, c): + if c == curses.KEY_LEFT: + self.selidx = max(0,self.selidx-1) + if c == curses.KEY_RIGHT: + self.selidx = min(len(self.opts)-1,self.selidx+1) + + def get_value(self): + return self.vals[self.selidx] + + def set_value(self, nv): + for i,val in enumerate(self.vals): + if nv == val: + self.selidx = i + return + raise Exception("Invalid value for SelectInput") + +class TextInput(InputField): + def __init__(self, parent, move_func, width, message, name, value, docmp): + self.parent = parent + self.move_func = move_func + self.width = width + + self.message = message + self.name = name + self.value = value + self.docmp = docmp + + self.tab_count = 0 + self.cursor = len(self.value) + self.opts = None + self.opt_off = 0 + + def render(self,screen,row,width,selected,col=1,cursor_offset=0): + if self.message: + self.parent.add_string(row,self.message,screen,col,False,True) + row += 1 + if selected: + if self.opts: + self.parent.add_string(row+1,self.opts[self.opt_off:],screen,col,False,True) + if self.cursor > (width-3): + self.move_func(row,width-2) + else: + self.move_func(row,self.cursor+1+cursor_offset) + slen = len(self.value)+3 + if slen > width: + vstr = self.value[(slen-width):] + else: + vstr = self.value.ljust(width-2) + self.parent.add_string(row,"{!black,white,bold!}%s"%vstr,screen,col,False,False) + + if self.message: + return 3 + else: + return 2 + + def get_value(self): + return self.value + + def set_value(self,val): + self.value = val + self.cursor = len(self.value) + + # most of the cursor,input stuff here taken from ui/console/screen.py + def handle_read(self,c): + if c == 9 and self.docmp: + # Keep track of tab hit count to know when it's double-hit + self.tab_count += 1 + if self.tab_count > 1: + second_hit = True + self.tab_count = 0 + else: + second_hit = False + + # We only call the tab completer function if we're at the end of + # the input string on the cursor is on a space + if self.cursor == len(self.value) or self.value[self.cursor] == " ": + if self.opts: + prev = self.opt_off + self.opt_off += self.width-3 + # now find previous double space, best guess at a split point + # in future could keep opts unjoined to get this really right + self.opt_off = self.opts.rfind(" ",0,self.opt_off)+2 + if second_hit and self.opt_off == prev: # double tap and we're at the end + self.opt_off = 0 + else: + opts = self.complete(self.value) + if len(opts) == 1: # only one option, just complete it + self.value = opts[0] + self.cursor = len(opts[0]) + self.tab_count = 0 + elif len(opts) > 1: + prefix = os.path.commonprefix(opts) + if prefix: + self.value = prefix + self.cursor = len(prefix) + + if len(opts) > 1 and second_hit: # display multiple options on second tab hit + sp = self.value.rfind(os.sep)+1 + self.opts = " ".join([o[sp:] for o in opts]) + + # Cursor movement + elif c == curses.KEY_LEFT: + self.cursor = max(0,self.cursor-1) + elif c == curses.KEY_RIGHT: + self.cursor = min(len(self.value),self.cursor+1) + elif c == curses.KEY_HOME: + self.cursor = 0 + elif c == curses.KEY_END: + self.cursor = len(self.value) + + if c != 9: + self.opts = None + self.opt_off = 0 + self.tab_count = 0 + + # Delete a character in the input string based on cursor position + if c == curses.KEY_BACKSPACE or c == 127: + if self.value and self.cursor > 0: + self.value = self.value[:self.cursor - 1] + self.value[self.cursor:] + self.cursor-=1 + elif c == curses.KEY_DC: + if self.value and self.cursor < len(self.value): + self.value = self.value[:self.cursor] + self.value[self.cursor+1:] + elif c > 31 and c < 256: + # Emulate getwch + stroke = chr(c) + uchar = "" + while not uchar: + try: + uchar = stroke.decode(self.parent.encoding) + except UnicodeDecodeError: + c = self.parent.stdscr.getch() + stroke += chr(c) + if uchar: + if self.cursor == len(self.value): + self.value += uchar + else: + # Insert into string + self.value = self.value[:self.cursor] + uchar + self.value[self.cursor:] + # Move the cursor forward + self.cursor+=1 + + + def complete(self,line): + line = os.path.abspath(os.path.expanduser(line)) + ret = [] + if os.path.exists(line): + # This is a correct path, check to see if it's a directory + if os.path.isdir(line): + # Directory, so we need to show contents of directory + #ret.extend(os.listdir(line)) + for f in os.listdir(line): + # Skip hidden + if f.startswith("."): + continue + f = os.path.join(line, f) + if os.path.isdir(f): + f += os.sep + ret.append(f) + else: + # This is a file, but we could be looking for another file that + # shares a common prefix. + for f in os.listdir(os.path.dirname(line)): + if f.startswith(os.path.split(line)[1]): + ret.append(os.path.join( os.path.dirname(line), f)) + else: + # This path does not exist, so lets do a listdir on it's parent + # and find any matches. + ret = [] + if os.path.isdir(os.path.dirname(line)): + for f in os.listdir(os.path.dirname(line)): + if f.startswith(os.path.split(line)[1]): + p = os.path.join(os.path.dirname(line), f) + + if os.path.isdir(p): + p += os.sep + ret.append(p) + return ret + + +class InputPopup(Popup): + def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=None): + Popup.__init__(self,parent_mode,title,width_req,height_req,close_cb) + self.inputs = [] + self.spaces = [] + self.current_input = 0 + + def move(self,r,c): + self._cursor_row = r + self._cursor_col = c + + def add_text_input(self, message, name, value="", complete=True): + """ + Add a text input field to the popup. + + :param message: string to display above the input field + :param name: name of the field, for the return callback + :param value: initial value of the field + :param complete: should completion be run when tab is hit and this field is active + """ + self.inputs.append(TextInput(self.parent, self.move, self.width, message, + name, value, complete)) + + def add_spaces(self, num): + self.spaces.append((len(self.inputs)-1,num)) + + def add_select_input(self, message, name, opts, vals, default_index=0): + self.inputs.append(SelectInput(self.parent, message, name, opts, vals, default_index)) + + def add_checked_input(self, message, name, checked=False): + self.inputs.append(CheckedInput(self.parent,message,name,checked)) + + def _refresh_lines(self): + self._cursor_row = -1 + self._cursor_col = -1 + curses.curs_set(0) + crow = 1 + spos = 0 + for i,ipt in enumerate(self.inputs): + crow += ipt.render(self.screen,crow,self.width,i==self.current_input) + if self.spaces and (spos < len(self.spaces)) and (i == self.spaces[spos][0]): + crow += self.spaces[spos][1] + spos += 1 + + # need to do this last as adding things moves the cursor + if self._cursor_row >= 0: + curses.curs_set(2) + self.screen.move(self._cursor_row,self._cursor_col) + + def handle_read(self, c): + if c == curses.KEY_UP: + self.current_input = max(0,self.current_input-1) + elif c == curses.KEY_DOWN: + self.current_input = min(len(self.inputs)-1,self.current_input+1) + elif c == curses.KEY_ENTER or c == 10: + if self.close_cb: + vals = {} + for ipt in self.inputs: + vals[ipt.name] = ipt.get_value() + curses.curs_set(0) + self.close_cb(vals) + return True # close the popup + elif c == 27: # close on esc, no action + return True + elif self.inputs: + self.inputs[self.current_input].handle_read(c) + + self.refresh() + return False + diff --git a/deluge/ui/console/screen.py b/deluge/ui/console/modes/legacy.py similarity index 60% rename from deluge/ui/console/screen.py rename to deluge/ui/console/modes/legacy.py index bf6b53190..0434bac07 100644 --- a/deluge/ui/console/screen.py +++ b/deluge/ui/console/modes/legacy.py @@ -1,6 +1,7 @@ # -# screen.py +# legacy.py # +# Copyright (C) 2008-2009 Ido Abramovich # Copyright (C) 2009 Andrew Resch # # Deluge is free software. @@ -33,61 +34,35 @@ # # -import sys -import logging try: import curses except ImportError: pass -import colors -try: - import signal - from fcntl import ioctl - import termios - import struct -except: - pass -from twisted.internet import reactor +from basemode import BaseMode +import deluge.ui.console.colors as colors +from twisted.internet import defer, reactor +from deluge.ui.client import client +import deluge.component as component +import logging,os log = logging.getLogger(__name__) -class CursesStdIO(object): - """fake fd to be registered as a reader with the twisted reactor. - Curses classes needing input should extend this""" - - def fileno(self): - """ We want to select on FD 0 """ - return 0 - - def doRead(self): - """called when input is ready""" - pass - def logPrefix(self): return 'CursesClient' LINES_BUFFER_SIZE = 5000 INPUT_HISTORY_SIZE = 500 -class Screen(CursesStdIO): - def __init__(self, stdscr, command_parser, tab_completer=None, encoding=None): - """ - A curses screen designed to run as a reader in a twisted reactor. +class Legacy(BaseMode): + def __init__(self, stdscr, encoding=None): - :param command_parser: a function that will be passed a string when the - user hits enter - :param tab_completer: a function that is sent the `:prop:input` string when - the user hits tab. It's intended purpose is to modify the input string. - It should return a 2-tuple (input string, input cursor). + self.batch_write = False + self.lines = [] - """ - log.debug("Screen init!") - # Function to be called with commands - self.command_parser = command_parser - self.tab_completer = tab_completer - self.stdscr = stdscr - # Make the input calls non-blocking - self.stdscr.nodelay(1) + # A list of strings to be displayed based on the offset (scroll) + self.lines = [] + # The offset to display lines + self.display_lines_offset = 0 # Holds the user input and is cleared on 'enter' self.input = "" @@ -101,206 +76,34 @@ class Screen(CursesStdIO): # Keep track of double-tabs self.tab_count = 0 - # Strings for the 2 status bars - self.topbar = "" - self.bottombar = "" + # Get a handle to the main console + self.console = component.get("ConsoleUI") - # A list of strings to be displayed based on the offset (scroll) - self.lines = [] - # The offset to display lines - self.display_lines_offset = 0 + # show the cursor + curses.curs_set(2) - # Keep track of the screen size - self.rows, self.cols = self.stdscr.getmaxyx() - try: - signal.signal(signal.SIGWINCH, self.on_resize) - except Exception, e: - log.debug("Unable to catch SIGWINCH signal!") + BaseMode.__init__(self, stdscr, encoding) - if not encoding: - self.encoding = sys.getdefaultencoding() - else: - self.encoding = encoding + # This gets fired once we have received the torrents list from the core + self.started_deferred = defer.Deferred() - # Do a refresh right away to draw the screen - self.refresh() + # Maintain a list of (torrent_id, name) for use in tab completion + self.torrents = [] + def on_session_state(result): + def on_torrents_status(torrents): + for torrent_id, status in torrents.items(): + self.torrents.append((torrent_id, status["name"])) + self.started_deferred.callback(True) - def on_resize(self, *args): - log.debug("on_resize_from_signal") - # Get the new rows and cols value - self.rows, self.cols = struct.unpack("hhhh", ioctl(0, termios.TIOCGWINSZ ,"\000"*8))[0:2] - curses.resizeterm(self.rows, self.cols) - self.refresh() + client.core.get_torrents_status({"id": result}, ["name"]).addCallback(on_torrents_status) + client.core.get_session_state().addCallback(on_session_state) - def connectionLost(self, reason): - self.close() + # Register some event handlers to keep the torrent list up-to-date + client.register_event_handler("TorrentAddedEvent", self.on_torrent_added_event) + client.register_event_handler("TorrentRemovedEvent", self.on_torrent_removed_event) - def add_line(self, text, refresh=True): - """ - Add a line to the screen. This will be showed between the two bars. - The text can be formatted with color using the following format: - - "{!fg, bg, attributes, ...!}" - - See: http://docs.python.org/library/curses.html#constants for attributes. - - Alternatively, it can use some built-in scheme for coloring. - See colors.py for built-in schemes. - - "{!scheme!}" - - Examples: - - "{!blue, black, bold!}My Text is {!white, black!}cool" - "{!info!}I am some info text!" - "{!error!}Uh oh!" - - :param text: the text to show - :type text: string - :param refresh: if True, the screen will refresh after the line is added - :type refresh: bool - - """ - - def get_line_chunks(line): - """ - Returns a list of 2-tuples (color string, text) - - """ - chunks = [] - num_chunks = line.count("{!") - for i in range(num_chunks): - # Find the beginning and end of the color tag - beg = line.find("{!") - end = line.find("!}") + 2 - color = line[beg:end] - line = line[end:] - - # Check to see if this is the last chunk - if i + 1 == num_chunks: - text = line - else: - # Not the last chunk so get the text up to the next tag - # and remove the text from line - text = line[:line.find("{!")] - line = line[line.find("{!"):] - - chunks.append((color, text)) - - return chunks - - for line in text.splitlines(): - # We need to check for line lengths here and split as necessary - try: - line_length = colors.get_line_length(line) - except colors.BadColorString: - log.error("Passed a bad colored string..") - line_length = len(line) - - if line_length >= (self.cols - 1): - s = "" - # The length of the text without the color tags - s_len = 0 - # We need to split this over multiple lines - for chunk in get_line_chunks(line): - if (len(chunk[1]) + s_len) < (self.cols - 1): - # This chunk plus the current string in 's' isn't over - # the maximum width, so just append the color tag and text - s += chunk[0] + chunk[1] - s_len += len(chunk[1]) - else: - # The chunk plus the current string in 's' is too long. - # We need to take as much of the chunk and put it into 's' - # with the color tag. - remain = (self.cols - 1) - s_len - s += chunk[0] + chunk[1][:remain] - # We append the line since it's full - self.lines.append(s) - # Start a new 's' with the remainder chunk - s = chunk[0] + chunk[1][remain:] - s_len = len(chunk[1][remain:]) - # Append the final string which may or may not be the full width - if s: - self.lines.append(s) - else: - self.lines.append(line) - - while len(self.lines) > LINES_BUFFER_SIZE: - # Remove the oldest line if the max buffer size has been reached - del self.lines[0] - - if refresh: - self.refresh() - - def add_string(self, row, string): - """ - Adds a string to the desired `:param:row`. - - :param row: int, the row number to write the string - - """ - col = 0 - try: - parsed = colors.parse_color_string(string, self.encoding) - except colors.BadColorString, e: - log.error("Cannot add bad color string %s: %s", string, e) - return - - for index, (color, s) in enumerate(parsed): - if index + 1 == len(parsed): - # This is the last string so lets append some " " to it - s += " " * (self.cols - (col + len(s)) - 1) - self.stdscr.addstr(row, col, s, color) - col += len(s) - - def refresh(self): - """ - Refreshes the screen. - Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset` - attribute and the status bars. - """ - self.stdscr.clear() - - # Update the status bars - self.add_string(0, self.topbar) - self.add_string(self.rows - 2, self.bottombar) - - # The number of rows minus the status bars and the input line - available_lines = self.rows - 3 - # If the amount of lines exceeds the number of rows, we need to figure out - # which ones to display based on the offset - if len(self.lines) > available_lines: - # Get the lines to display based on the offset - offset = len(self.lines) - self.display_lines_offset - lines = self.lines[-(available_lines - offset):offset] - elif len(self.lines) == available_lines: - lines = self.lines - else: - lines = [""] * (available_lines - len(self.lines)) - lines.extend(self.lines) - - # Add the lines to the screen - for index, line in enumerate(lines): - self.add_string(index + 1, line) - - # Add the input string - self.add_string(self.rows - 1, self.input) - - # Move the cursor - self.stdscr.move(self.rows - 1, self.input_cursor) - self.stdscr.redrawwin() - self.stdscr.refresh() - - def doRead(self): - """ - Called when there is data to be read, ie, input from the keyboard. - """ - # We wrap this function to catch exceptions and shutdown the mainloop - try: - self._doRead() - except Exception, e: - log.exception(e) - reactor.stop() + def update(self): + pass def _doRead(self): # Read the character @@ -310,7 +113,7 @@ class Screen(CursesStdIO): if c == curses.KEY_ENTER or c == 10: if self.input: self.add_line(">>> " + self.input) - self.command_parser(self.input.encode(self.encoding)) + self.do_command(self.input.encode(self.encoding)) if len(self.input_history) == INPUT_HISTORY_SIZE: # Remove the oldest input history if the max history size # is reached. @@ -426,14 +229,375 @@ class Screen(CursesStdIO): # Update the input string on the screen self.add_string(self.rows - 1, self.input) - self.stdscr.move(self.rows - 1, self.input_cursor) + try: + self.stdscr.move(self.rows - 1, self.input_cursor) + except curses.error: + pass self.stdscr.refresh() - def close(self): + + + def refresh(self): """ - Clean up the curses stuff on exit. + Refreshes the screen. + Updates the lines based on the`:attr:lines` based on the `:attr:display_lines_offset` + attribute and the status bars. """ - curses.nocbreak() - self.stdscr.keypad(0) - curses.echo() - curses.endwin() + self.stdscr.clear() + + # Update the status bars + self.add_string(0, self.statusbars.topbar) + self.add_string(self.rows - 2, self.statusbars.bottombar) + + # The number of rows minus the status bars and the input line + available_lines = self.rows - 3 + # If the amount of lines exceeds the number of rows, we need to figure out + # which ones to display based on the offset + if len(self.lines) > available_lines: + # Get the lines to display based on the offset + offset = len(self.lines) - self.display_lines_offset + lines = self.lines[-(available_lines - offset):offset] + elif len(self.lines) == available_lines: + lines = self.lines + else: + lines = [""] * (available_lines - len(self.lines)) + lines.extend(self.lines) + + # Add the lines to the screen + for index, line in enumerate(lines): + self.add_string(index + 1, line) + + # Add the input string + self.add_string(self.rows - 1, self.input) + + # Move the cursor + try: + self.stdscr.move(self.rows - 1, self.input_cursor) + except curses.error: + pass + self.stdscr.redrawwin() + self.stdscr.refresh() + + + def add_line(self, text, refresh=True): + """ + Add a line to the screen. This will be showed between the two bars. + The text can be formatted with color using the following format: + + "{!fg, bg, attributes, ...!}" + + See: http://docs.python.org/library/curses.html#constants for attributes. + + Alternatively, it can use some built-in scheme for coloring. + See colors.py for built-in schemes. + + "{!scheme!}" + + Examples: + + "{!blue, black, bold!}My Text is {!white, black!}cool" + "{!info!}I am some info text!" + "{!error!}Uh oh!" + + :param text: the text to show + :type text: string + :param refresh: if True, the screen will refresh after the line is added + :type refresh: bool + + """ + + def get_line_chunks(line): + """ + Returns a list of 2-tuples (color string, text) + + """ + chunks = [] + num_chunks = line.count("{!") + for i in range(num_chunks): + # Find the beginning and end of the color tag + beg = line.find("{!") + end = line.find("!}") + 2 + color = line[beg:end] + line = line[end:] + + # Check to see if this is the last chunk + if i + 1 == num_chunks: + text = line + else: + # Not the last chunk so get the text up to the next tag + # and remove the text from line + text = line[:line.find("{!")] + line = line[line.find("{!"):] + + chunks.append((color, text)) + + return chunks + + for line in text.splitlines(): + # We need to check for line lengths here and split as necessary + try: + line_length = colors.get_line_length(line) + except colors.BadColorString: + log.error("Passed a bad colored string..") + line_length = len(line) + + if line_length >= (self.cols - 1): + s = "" + # The length of the text without the color tags + s_len = 0 + # We need to split this over multiple lines + for chunk in get_line_chunks(line): + if (len(chunk[1]) + s_len) < (self.cols - 1): + # This chunk plus the current string in 's' isn't over + # the maximum width, so just append the color tag and text + s += chunk[0] + chunk[1] + s_len += len(chunk[1]) + else: + # The chunk plus the current string in 's' is too long. + # We need to take as much of the chunk and put it into 's' + # with the color tag. + remain = (self.cols - 1) - s_len + s += chunk[0] + chunk[1][:remain] + # We append the line since it's full + self.lines.append(s) + # Start a new 's' with the remainder chunk + s = chunk[0] + chunk[1][remain:] + s_len = len(chunk[1][remain:]) + # Append the final string which may or may not be the full width + if s: + self.lines.append(s) + else: + self.lines.append(line) + + while len(self.lines) > LINES_BUFFER_SIZE: + # Remove the oldest line if the max buffer size has been reached + del self.lines[0] + + if refresh: + self.refresh() + + + def add_string(self, row, string): + """ + Adds a string to the desired `:param:row`. + + :param row: int, the row number to write the string + + """ + col = 0 + try: + parsed = colors.parse_color_string(string, self.encoding) + except colors.BadColorString, e: + log.error("Cannot add bad color string %s: %s", string, e) + return + + for index, (color, s) in enumerate(parsed): + if index + 1 == len(parsed): + # This is the last string so lets append some " " to it + s += " " * (self.cols - (col + len(s)) - 1) + try: + self.stdscr.addstr(row, col, s, color) + except curses.error: + pass + + col += len(s) + + + def do_command(self, cmd): + """ + Processes a command. + + :param cmd: str, the command string + + """ + if not cmd: + return + cmd, _, line = cmd.partition(' ') + try: + parser = self.console._commands[cmd].create_parser() + except KeyError: + self.write("{!error!}Unknown command: %s" % cmd) + return + args = self.console._commands[cmd].split(line) + + # Do a little hack here to print 'command --help' properly + parser._print_help = parser.print_help + def print_help(f=None): + parser._print_help(f) + parser.print_help = print_help + + # Only these commands can be run when not connected to a daemon + not_connected_cmds = ["help", "connect", "quit"] + aliases = [] + for c in not_connected_cmds: + aliases.extend(self.console._commands[c].aliases) + not_connected_cmds.extend(aliases) + + if not client.connected() and cmd not in not_connected_cmds: + self.write("{!error!}Not connected to a daemon, please use the connect command first.") + return + + try: + options, args = parser.parse_args(args) + except Exception, e: + self.write("{!error!}Error parsing options: %s" % e) + return + + if not getattr(options, '_exit', False): + try: + ret = self.console._commands[cmd].handle(*args, **options.__dict__) + except Exception, e: + self.write("{!error!}" + str(e)) + log.exception(e) + import traceback + self.write("%s" % traceback.format_exc()) + return defer.succeed(True) + else: + return ret + + + + def set_batch_write(self, batch): + """ + When this is set the screen is not refreshed after a `:meth:write` until + this is set to False. + + :param batch: set True to prevent screen refreshes after a `:meth:write` + :type batch: bool + + """ + self.batch_write = batch + if not batch: + self.refresh() + + def write(self, line): + """ + Writes a line out + + :param line: str, the line to print + + """ + self.add_line(line, not self.batch_write) + + + def tab_completer(self, line, cursor, second_hit): + """ + Called when the user hits 'tab' and will autocomplete or show options. + If a command is already supplied in the line, this function will call the + complete method of the command. + + :param line: str, the current input string + :param cursor: int, the cursor position in the line + :param second_hit: bool, if this is the second time in a row the tab key + has been pressed + + :returns: 2-tuple (string, cursor position) + + """ + # First check to see if there is no space, this will mean that it's a + # command that needs to be completed. + if " " not in line: + possible_matches = [] + # Iterate through the commands looking for ones that startwith the + # line. + for cmd in self.console._commands: + if cmd.startswith(line): + possible_matches.append(cmd + " ") + + line_prefix = "" + else: + cmd = line.split(" ")[0] + if cmd in self.console._commands: + # Call the command's complete method to get 'er done + possible_matches = self.console._commands[cmd].complete(line.split(" ")[-1]) + line_prefix = " ".join(line.split(" ")[:-1]) + " " + else: + # This is a bogus command + return (line, cursor) + + # No matches, so just return what we got passed + if len(possible_matches) == 0: + return (line, cursor) + # If we only have 1 possible match, then just modify the line and + # return it, else we need to print out the matches without modifying + # the line. + elif len(possible_matches) == 1: + new_line = line_prefix + possible_matches[0] + return (new_line, len(new_line)) + else: + if second_hit: + # Only print these out if it's a second_hit + self.write(" ") + for match in possible_matches: + self.write(match) + else: + p = " ".join(line.split(" ")[:-1]) + new_line = " ".join([p, os.path.commonprefix(possible_matches)]) + if len(new_line) > len(line): + line = new_line + cursor = len(line) + return (line, cursor) + + + def tab_complete_torrent(self, line): + """ + Completes torrent_ids or names. + + :param line: str, the string to complete + + :returns: list of matches + + """ + possible_matches = [] + + # Find all possible matches + for torrent_id, torrent_name in self.torrents: + if torrent_id.startswith(line): + possible_matches.append(torrent_id + " ") + if torrent_name.startswith(line): + possible_matches.append(torrent_name + " ") + + return possible_matches + + def get_torrent_name(self, torrent_id): + """ + Gets a torrent name from the torrents list. + + :param torrent_id: str, the torrent_id + + :returns: the name of the torrent or None + """ + + for tid, name in self.torrents: + if torrent_id == tid: + return name + + return None + + def match_torrent(self, string): + """ + Returns a list of torrent_id matches for the string. It will search both + torrent_ids and torrent names, but will only return torrent_ids. + + :param string: str, the string to match on + + :returns: list of matching torrent_ids. Will return an empty list if + no matches are found. + + """ + ret = [] + for tid, name in self.torrents: + if tid.startswith(string) or name.startswith(string): + ret.append(tid) + + return ret + + def on_torrent_added_event(self, event): + def on_torrent_status(status): + self.torrents.append((event.torrent_id, status["name"])) + client.core.get_torrent_status(event.torrent_id, ["name"]).addCallback(on_torrent_status) + + def on_torrent_removed_event(self, event): + for index, (tid, name) in enumerate(self.torrents): + if event.torrent_id == tid: + del self.torrents[index] diff --git a/deluge/ui/console/modes/popup.py b/deluge/ui/console/modes/popup.py new file mode 100644 index 000000000..f4d00d49e --- /dev/null +++ b/deluge/ui/console/modes/popup.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +# +# popup.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +try: + import curses + import signal +except ImportError: + pass + +import format_utils +import logging +log = logging.getLogger(__name__) + +class Popup: + def __init__(self,parent_mode,title,width_req=-1,height_req=-1,close_cb=None,init_lines=None): + """ + Init a new popup. The default constructor will handle sizing and borders and the like. + + NB: The parent mode is responsible for calling refresh on any popups it wants to show. + This should be called as the last thing in the parents refresh method. + + The parent *must* also call _doRead on the popup instead of/in addition to + running its own _doRead code if it wants to have the popup handle user input. + + :param parent_mode: must be a basemode (or subclass) which the popup will be drawn over + :parem title: string, the title of the popup window + + Popups have two methods that must be implemented: + + refresh(self) - draw the popup window to screen. this default mode simply draws a bordered window + with the supplied title to the screen + + add_string(self, row, string) - add string at row. handles triming/ignoring if the string won't fit in the popup + + _doRead(self) - handle user input to the popup. + """ + self.parent = parent_mode + + if (height_req <= 0): + height_req = int(self.parent.rows/2) + if (width_req <= 0): + width_req = int(self.parent.cols/2) + by = (self.parent.rows/2)-(height_req/2) + bx = (self.parent.cols/2)-(width_req/2) + self.screen = curses.newwin(height_req,width_req,by,bx) + + self.title = title + self.close_cb = close_cb + self.height,self.width = self.screen.getmaxyx() + self.divider = None + self.lineoff = 0 + if init_lines: + self._lines = init_lines + else: + self._lines = [] + + def _refresh_lines(self): + crow = 1 + for line in self._lines[self.lineoff:]: + if (crow >= self.height-1): + break + self.parent.add_string(crow,line,self.screen,1,False,True) + crow+=1 + + def handle_resize(self): + log.debug("Resizing popup window (actually, just creating a new one)") + self.screen = curses.newwin((self.parent.rows/2),(self.parent.cols/2),(self.parent.rows/4),(self.parent.cols/4)) + self.height,self.width = self.screen.getmaxyx() + + + def refresh(self): + self.screen.clear() + self.screen.border(0,0,0,0) + toff = max(1,int((self.parent.cols/4)-(len(self.title)/2))) + self.parent.add_string(0,"{!white,black,bold!}%s"%self.title,self.screen,toff,False,True) + + self._refresh_lines() + if (len(self._lines) > (self.height-2)): + lts = len(self._lines)-(self.height-3) + perc_sc = float(self.lineoff)/lts + sb_pos = int((self.height-2)*perc_sc)+1 + if (sb_pos == 1) and (self.lineoff != 0): + sb_pos += 1 + self.parent.add_string(sb_pos, "{!white,black,bold!}|",self.screen,col=(self.width-1),pad=False,trim=False) + + self.screen.redrawwin() + self.screen.noutrefresh() + + def clear(self): + self._lines = [] + + def handle_read(self, c): + if c == curses.KEY_UP: + self.lineoff = max(0,self.lineoff -1) + elif c == curses.KEY_DOWN: + if len(self._lines)-self.lineoff > (self.height-2): + self.lineoff += 1 + + elif c == curses.KEY_ENTER or c == 10 or c == 27: # close on enter/esc + if self.close_cb: + self.close_cb() + return True # close the popup + + if c > 31 and c < 256 and chr(c) == 'q': + if self.close_cb: + self.close_cb() + return True # close the popup + + self.refresh() + + return False + + def set_title(self, title): + self.title = title + + def add_line(self, string): + self._lines.append(string) + + def add_divider(self): + if not self.divider: + self.divider = "-"*(self.width-2) + self._lines.append(self.divider) + + +class SelectablePopup(Popup): + """ + A popup which will let the user select from some of the lines that + are added. + """ + def __init__(self,parent_mode,title,selection_callback,*args): + Popup.__init__(self,parent_mode,title) + self._selection_callback = selection_callback + self._selection_args = args + self._selectable_lines = [] + self._select_data = [] + self._line_foregrounds = [] + self._udxs = {} + self._hotkeys = {} + self._selected = -1 + + def add_line(self, string, selectable=True, use_underline=True, data=None, foreground=None): + if use_underline: + udx = string.find('_') + if udx >= 0: + string = string[:udx]+string[udx+1:] + self._udxs[len(self._lines)+1] = udx + c = string[udx].lower() + self._hotkeys[c] = len(self._lines) + Popup.add_line(self,string) + self._line_foregrounds.append(foreground) + if selectable: + self._selectable_lines.append(len(self._lines)-1) + self._select_data.append(data) + if self._selected < 0: + self._selected = (len(self._lines)-1) + + def _refresh_lines(self): + crow = 1 + for row,line in enumerate(self._lines): + if (crow >= self.height-1): + break + if (row < self.lineoff): + continue + fg = self._line_foregrounds[row] + udx = self._udxs.get(crow) + if row == self._selected: + if fg == None: fg = "black" + colorstr = "{!%s,white,bold!}"%fg + if udx >= 0: + ustr = "{!%s,white,bold,underline!}"%fg + else: + if fg == None: fg = "white" + colorstr = "{!%s,black!}"%fg + if udx >= 0: + ustr = "{!%s,black,underline!}"%fg + if udx == 0: + self.parent.add_string(crow,"- %s%c%s%s"%(ustr,line[0],colorstr,line[1:]),self.screen,1,False,True) + elif udx > 0: + # well, this is a litte gross + self.parent.add_string(crow,"- %s%s%s%c%s%s"%(colorstr,line[:udx],ustr,line[udx],colorstr,line[udx+1:]),self.screen,1,False,True) + else: + self.parent.add_string(crow,"- %s%s"%(colorstr,line),self.screen,1,False,True) + crow+=1 + + def current_selection(self): + "Returns a tuple of (selected index, selected data)" + idx = self._selectable_lines.index(self._selected) + return (idx,self._select_data[idx]) + + def add_divider(self,color="white"): + if not self.divider: + self.divider = "-"*(self.width-6)+" -" + self._lines.append(self.divider) + self._line_foregrounds.append(color) + + def handle_read(self, c): + if c == curses.KEY_UP: + #self.lineoff = max(0,self.lineoff -1) + if (self._selected != self._selectable_lines[0] and + len(self._selectable_lines) > 1): + idx = self._selectable_lines.index(self._selected) + self._selected = self._selectable_lines[idx-1] + elif c == curses.KEY_DOWN: + #if len(self._lines)-self.lineoff > (self.height-2): + # self.lineoff += 1 + idx = self._selectable_lines.index(self._selected) + if (idx < len(self._selectable_lines)-1): + self._selected = self._selectable_lines[idx+1] + elif c == 27: # close on esc, no action + return True + elif c == curses.KEY_ENTER or c == 10: + idx = self._selectable_lines.index(self._selected) + return self._selection_callback(idx,self._select_data[idx],*self._selection_args) + if c > 31 and c < 256: + if chr(c) == 'q': + return True # close the popup + uc = chr(c).lower() + if uc in self._hotkeys: + # exec hotkey action + idx = self._selectable_lines.index(self._hotkeys[uc]) + return self._selection_callback(idx,self._select_data[idx],*self._selection_args) + self.refresh() + + return False + + +class MessagePopup(Popup): + """ + Popup that just displays a message + """ + def __init__(self, parent_mode, title, message): + self.message = message + self.width= int(parent_mode.cols/2) + lns = format_utils.wrap_string(self.message,self.width-2,3,True) + hr = min(len(lns)+2,int(parent_mode.rows/2)) + Popup.__init__(self,parent_mode,title,height_req=hr) + self._lines = lns + + def handle_resize(self): + Popup.handle_resize(self) + self.clear() + self._lines = self._split_message() diff --git a/deluge/ui/console/modes/preference_panes.py b/deluge/ui/console/modes/preference_panes.py new file mode 100644 index 000000000..e24921629 --- /dev/null +++ b/deluge/ui/console/modes/preference_panes.py @@ -0,0 +1,396 @@ +# +# preference_panes.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from deluge.ui.console.modes.input_popup import TextInput,SelectInput,CheckedInput,IntSpinInput,FloatSpinInput,CheckedPlusInput +import deluge.ui.console.modes.alltorrents + +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + + +class NoInput: + def depend_skip(self): + return False + +class Header(NoInput): + def __init__(self, parent, header, space_above, space_below): + self.parent = parent + self.header = "{!white,black,bold!}%s"%header + self.space_above = space_above + self.space_below = space_below + self.name = header + + def render(self, screen, row, width, active, offset): + rows = 1 + if self.space_above: + row += 1 + rows += 1 + self.parent.add_string(row,self.header,screen,offset-1,False,True) + if self.space_below: rows += 1 + return rows + +class InfoField(NoInput): + def __init__(self,parent,label,value,name): + self.parent = parent + self.label = label + self.value = value + self.txt = "%s %s"%(label,value) + self.name = name + + def render(self, screen, row, width, active, offset): + self.parent.add_string(row,self.txt,screen,offset-1,False,True) + return 1 + + def set_value(self, v): + self.value = v + if type(v) == float: + self.txt = "%s %.2f"%(self.label,self.value) + else: + self.txt = "%s %s"%(self.label,self.value) + +class BasePane: + def __init__(self, offset, parent, width): + self.offset = offset+1 + self.parent = parent + self.width = width + self.inputs = [] + self.active_input = -1 + + # have we scrolled down in the list + self.input_offset = 0 + + def move(self,r,c): + self._cursor_row = r + self._cursor_col = c + + def add_config_values(self,conf_dict): + for ipt in self.inputs: + if not isinstance(ipt,NoInput): + # gross, have to special case in/out ports since they are tuples + if ipt.name in ("listen_ports_to","listen_ports_from", + "out_ports_from","out_ports_to"): + if ipt.name == "listen_ports_to": + conf_dict["listen_ports"] = (self.infrom.get_value(),self.into.get_value()) + if ipt.name == "out_ports_to": + conf_dict["outgoing_ports"] = (self.outfrom.get_value(),self.outto.get_value()) + else: + conf_dict[ipt.name] = ipt.get_value() + if hasattr(ipt,"get_child"): + c = ipt.get_child() + conf_dict[c.name] = c.get_value() + + def update_values(self, conf_dict): + for ipt in self.inputs: + if not isinstance(ipt,NoInput): + try: + ipt.set_value(conf_dict[ipt.name]) + except KeyError: # just ignore if it's not in dict + pass + if hasattr(ipt,"get_child"): + try: + c = ipt.get_child() + c.set_value(conf_dict[c.name]) + except KeyError: # just ignore if it's not in dict + pass + + def render(self, mode, screen, width, active): + self._cursor_row = -1 + if self.active_input < 0: + for i,ipt in enumerate(self.inputs): + if not isinstance(ipt,NoInput): + self.active_input = i + break + drew_act = not active + crow = 1 + for i,ipt in enumerate(self.inputs): + if ipt.depend_skip() or i= (mode.prefs_height): + break + + if not drew_act: + self.input_offset+=1 + mode.refresh() + return 0 + + if active and self._cursor_row >= 0: + curses.curs_set(2) + screen.move(self._cursor_row,self._cursor_col+self.offset-1) + else: + curses.curs_set(0) + + return crow + + # just handles setting the active input + def handle_read(self,c): + if not self.inputs: # no inputs added yet + return + + if c == curses.KEY_UP: + nc = max(0,self.active_input-1) + while isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip(): + nc-=1 + if nc <= 0: break + if not isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip(): + self.active_input = nc + elif c == curses.KEY_DOWN: + ilen = len(self.inputs) + nc = min(self.active_input+1,ilen-1) + while isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip(): + nc+=1 + if nc >= ilen: + nc-=1 + break + if not isinstance(self.inputs[nc], NoInput) or self.inputs[nc].depend_skip(): + self.active_input = nc + else: + self.inputs[self.active_input].handle_read(c) + + + def add_header(self, header, space_above=False, space_below=False): + self.inputs.append(Header(self.parent, header, space_above, space_below)) + + def add_info_field(self, label, value, name): + self.inputs.append(InfoField(self.parent, label, value, name)) + + def add_text_input(self, name, msg, dflt_val): + self.inputs.append(TextInput(self.parent,self.move,self.width,msg,name,dflt_val,False)) + + def add_select_input(self, name, msg, opts, vals, selidx): + self.inputs.append(SelectInput(self.parent,msg,name,opts,vals,selidx)) + + def add_checked_input(self, name, message, checked): + self.inputs.append(CheckedInput(self.parent,message,name,checked)) + + def add_checkedplus_input(self, name, message, child, checked): + self.inputs.append(CheckedPlusInput(self.parent,message,name,child,checked)) + + def add_int_spin_input(self, name, message, value, min_val, max_val): + self.inputs.append(IntSpinInput(self.parent,message,name,self.move,value,min_val,max_val)) + + def add_float_spin_input(self, name, message, value, inc_amt, precision, min_val, max_val): + self.inputs.append(FloatSpinInput(self.parent,message,name,self.move,value,inc_amt,precision,min_val,max_val)) + + +class DownloadsPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + + self.add_header("Folders") + self.add_text_input("download_location","Download To:",parent.core_config["download_location"]) + cmptxt = TextInput(self.parent,self.move,self.width,None,"move_completed_path",parent.core_config["move_completed_path"],False) + self.add_checkedplus_input("move_completed","Move completed to:",cmptxt,parent.core_config["move_completed"]) + autotxt = TextInput(self.parent,self.move,self.width,None,"autoadd_location",parent.core_config["autoadd_location"],False) + self.add_checkedplus_input("autoadd_enable","Auto add .torrents from:",autotxt,parent.core_config["autoadd_enable"]) + copytxt = TextInput(self.parent,self.move,self.width,None,"torrentfiles_location",parent.core_config["torrentfiles_location"],False) + self.add_checkedplus_input("copy_torrent_file","Copy of .torrent files to:",copytxt,parent.core_config["copy_torrent_file"]) + self.add_checked_input("del_copy_torrent_file","Delete copy of torrent file on remove",parent.core_config["del_copy_torrent_file"]) + + self.add_header("Allocation",True) + + if parent.core_config["compact_allocation"]: + alloc_idx = 1 + else: + alloc_idx = 0 + self.add_select_input("compact_allocation",None,["Use Full Allocation","Use Compact Allocation"],[False,True],alloc_idx) + self.add_header("Options",True) + self.add_checked_input("prioritize_first_last_pieces","Prioritize first and last pieces of torrent",parent.core_config["prioritize_first_last_pieces"]) + self.add_checked_input("add_paused","Add torrents in paused state",parent.core_config["add_paused"]) + + +class NetworkPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Incomming Ports") + inrand = CheckedInput(parent,"Use Random Ports Active Port: %d"%parent.active_port,"random_port",parent.core_config["random_port"]) + self.inputs.append(inrand) + listen_ports = parent.core_config["listen_ports"] + self.infrom = IntSpinInput(self.parent," From:","listen_ports_from",self.move,listen_ports[0],0,65535) + self.infrom.set_depend(inrand,True) + self.into = IntSpinInput(self.parent," To: ","listen_ports_to",self.move,listen_ports[1],0,65535) + self.into.set_depend(inrand,True) + self.inputs.append(self.infrom) + self.inputs.append(self.into) + + + self.add_header("Outgoing Ports",True) + outrand = CheckedInput(parent,"Use Random Ports","random_outgoing_ports",parent.core_config["random_outgoing_ports"]) + self.inputs.append(outrand) + out_ports = parent.core_config["outgoing_ports"] + self.outfrom = IntSpinInput(self.parent," From:","out_ports_from",self.move,out_ports[0],0,65535) + self.outfrom.set_depend(outrand,True) + self.outto = IntSpinInput(self.parent," To: ","out_ports_to",self.move,out_ports[1],0,65535) + self.outto.set_depend(outrand,True) + self.inputs.append(self.outfrom) + self.inputs.append(self.outto) + + + self.add_header("Interface",True) + self.add_text_input("listen_interface","IP address of the interface to listen on (leave empty for default):",parent.core_config["listen_interface"]) + + self.add_header("TOS",True) + self.add_text_input("peer_tos","Peer TOS Byte:",parent.core_config["peer_tos"]) + + self.add_header("Network Extras") + self.add_checked_input("upnp","UPnP",parent.core_config["upnp"]) + self.add_checked_input("natpmp","NAT-PMP",parent.core_config["natpmp"]) + self.add_checked_input("utpex","Peer Exchange",parent.core_config["utpex"]) + self.add_checked_input("lsd","LSD",parent.core_config["lsd"]) + self.add_checked_input("dht","DHT",parent.core_config["dht"]) + + self.add_header("Encryption",True) + self.add_select_input("enc_in_policy","Inbound:",["Forced","Enabled","Disabled"],[0,1,2],parent.core_config["enc_in_policy"]) + self.add_select_input("enc_out_policy","Outbound:",["Forced","Enabled","Disabled"],[0,1,2],parent.core_config["enc_out_policy"]) + self.add_select_input("enc_level","Level:",["Handshake","Full Stream","Either"],[0,1,2],parent.core_config["enc_level"]) + self.add_checked_input("enc_prefer_rc4","Encrypt Entire Stream",parent.core_config["enc_prefer_rc4"]) + + +class BandwidthPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Global Bandwidth Usage") + self.add_int_spin_input("max_connections_global","Maximum Connections:",parent.core_config["max_connections_global"],-1,9000) + self.add_int_spin_input("max_upload_slots_global","Maximum Upload Slots:",parent.core_config["max_upload_slots_global"],-1,9000) + self.add_float_spin_input("max_download_speed","Maximum Download Speed (KiB/s):",parent.core_config["max_download_speed"],1.0,1,-1.0,60000.0) + self.add_float_spin_input("max_upload_speed","Maximum Upload Speed (KiB/s):",parent.core_config["max_upload_speed"],1.0,1,-1.0,60000.0) + self.add_int_spin_input("max_half_open_connections","Maximum Half-Open Connections:",parent.core_config["max_half_open_connections"],-1,9999) + self.add_int_spin_input("max_connections_per_second","Maximum Connection Attempts per Second:",parent.core_config["max_connections_per_second"],-1,9999) + self.add_checked_input("ignore_limits_on_local_network","Ignore limits on local network",parent.core_config["ignore_limits_on_local_network"]) + self.add_checked_input("rate_limit_ip_overhead","Rate Limit IP Overhead",parent.core_config["rate_limit_ip_overhead"]) + self.add_header("Per Torrent Bandwidth Usage",True) + self.add_int_spin_input("max_connections_per_torrent","Maximum Connections:",parent.core_config["max_connections_per_torrent"],-1,9000) + self.add_int_spin_input("max_upload_slots_per_torrent","Maximum Upload Slots:",parent.core_config["max_upload_slots_per_torrent"],-1,9000) + self.add_float_spin_input("max_download_speed_per_torrent","Maximum Download Speed (KiB/s):",parent.core_config["max_download_speed_per_torrent"],1.0,1,-1.0,60000.0) + self.add_float_spin_input("max_upload_speed_per_torrent","Maximum Upload Speed (KiB/s):",parent.core_config["max_upload_speed_per_torrent"],1.0,1,-1.0,60000.0) + +class InterfacePane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Columns To Display") + for cpn in deluge.ui.console.modes.alltorrents.column_pref_names: + pn = "show_%s"%cpn + self.add_checked_input(pn, + deluge.ui.console.modes.alltorrents.prefs_to_names[cpn], + parent.console_config[pn]) + self.add_header("Column Widths (-1 = expand)",True) + for cpn in deluge.ui.console.modes.alltorrents.column_pref_names: + pn = "%s_width"%cpn + self.add_int_spin_input(pn, + deluge.ui.console.modes.alltorrents.prefs_to_names[cpn], + parent.console_config[pn],-1,100) + +class OtherPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("System Information") + self.add_info_field(" Help us improve Deluge by sending us your","","") + self.add_info_field(" Python version, PyGTK version, OS and processor","","") + self.add_info_field(" types. Absolutely no other information is sent.","","") + self.add_checked_input("send_info","Yes, please send anonymous statistics.",parent.core_config["send_info"]) + self.add_header("GeoIP Database",True) + self.add_text_input("geoip_db_location","Location:",parent.core_config["geoip_db_location"]) + +class DaemonPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Port") + self.add_int_spin_input("daemon_port","Daemon Port:",parent.core_config["daemon_port"],0,65535) + self.add_header("Connections",True) + self.add_checked_input("allow_remote","Allow remote connections",parent.core_config["allow_remote"]) + self.add_header("Other",True) + self.add_checked_input("new_release_check","Periodically check the website for new releases",parent.core_config["new_release_check"]) + +class QueuePane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("General") + self.add_checked_input("queue_new_to_top","Queue new torrents to top",parent.core_config["queue_new_to_top"]) + self.add_header("Active Torrents",True) + self.add_int_spin_input("max_active_limit","Total active:",parent.core_config["max_active_limit"],-1,9999) + self.add_int_spin_input("max_active_downloading","Total active downloading:",parent.core_config["max_active_downloading"],-1,9999) + self.add_int_spin_input("max_active_seeding","Total active seeding:",parent.core_config["max_active_seeding"],-1,9999) + self.add_checked_input("dont_count_slow_torrents","Do not count slow torrents",parent.core_config["dont_count_slow_torrents"]) + self.add_header("Seeding",True) + self.add_float_spin_input("share_ratio_limit","Share Ratio Limit:",parent.core_config["share_ratio_limit"],1.0,2,-1.0,100.0) + self.add_float_spin_input("seed_time_ratio_limit","Share Time Ratio:",parent.core_config["seed_time_ratio_limit"],1.0,2,-1.0,100.0) + self.add_int_spin_input("seed_time_limit","Seed time (m):",parent.core_config["seed_time_limit"],-1,10000) + seedratio = FloatSpinInput(self.parent,"","stop_seed_ratio",self.move,parent.core_config["stop_seed_ratio"],0.1,2,0.5,100.0) + self.add_checkedplus_input("stop_seed_at_ratio","Stop seeding when share ratio reaches:",seedratio,parent.core_config["stop_seed_at_ratio"]) + self.add_checked_input("remove_seed_at_ratio","Remove torrent when share ratio reached",parent.core_config["remove_seed_at_ratio"]) + +class ProxyPane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Proxy Settings Comming Soon") + +class CachePane(BasePane): + def __init__(self, offset, parent, width): + BasePane.__init__(self,offset,parent,width) + self.add_header("Settings") + self.add_int_spin_input("cache_size","Cache Size (16 KiB blocks):",parent.core_config["cache_size"],0,99999) + self.add_int_spin_input("cache_expiry","Cache Expiry (seconds):",parent.core_config["cache_expiry"],1,32000) + self.add_header("Status (press 'r' to refresh status)",True) + self.add_header(" Write") + self.add_info_field(" Blocks Written:",self.parent.status["blocks_written"],"blocks_written") + self.add_info_field(" Writes:",self.parent.status["writes"],"writes") + self.add_info_field(" Write Cache Hit Ratio:","%.2f"%self.parent.status["write_hit_ratio"],"write_hit_ratio") + self.add_header(" Read") + self.add_info_field(" Blocks Read:",self.parent.status["blocks_read"],"blocks_read") + self.add_info_field(" Blocks Read hit:",self.parent.status["blocks_read_hit"],"blocks_read_hit") + self.add_info_field(" Reads:",self.parent.status["reads"],"reads") + self.add_info_field(" Read Cache Hit Ratio:","%.2f"%self.parent.status["read_hit_ratio"],"read_hit_ratio") + self.add_header(" Size") + self.add_info_field(" Cache Size:",self.parent.status["cache_size"],"cache_size") + self.add_info_field(" Read Cache Size:",self.parent.status["read_cache_size"],"read_cache_size") + + def update_cache_status(self, status): + for ipt in self.inputs: + if isinstance(ipt,InfoField): + try: + ipt.set_value(status[ipt.name]) + except KeyError: + pass diff --git a/deluge/ui/console/modes/preferences.py b/deluge/ui/console/modes/preferences.py new file mode 100644 index 000000000..0c0cd5a52 --- /dev/null +++ b/deluge/ui/console/modes/preferences.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +# +# preferences.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.component as component +from deluge.ui.client import client +from basemode import BaseMode +from input_popup import Popup,SelectInput + +from preference_panes import DownloadsPane,NetworkPane,BandwidthPane,InterfacePane +from preference_panes import OtherPane,DaemonPane,QueuePane,ProxyPane,CachePane + +from collections import deque + +try: + import curses +except ImportError: + pass + + +import logging +log = logging.getLogger(__name__) + + +# Big help string that gets displayed when the user hits 'h' +HELP_STR = \ +"""This screen lets you view and configure various options +in deluge. + +There are three main sections to this screen. Only one +section is active at a time. You can switch the active +section by hitting TAB (or Shift-TAB to go back one) + +The section on the left displays the various categories +that the settings fall in. You can navigate the list +using the up/down arrows + +The section on the right shows the settings for the +selected category. When this section is active +you can navigate the various settings with the up/down +arrows. Special keys for each input type are described +below. + +The final section is at the bottom right, the: +[Cancel] [Apply] [OK] buttons. When this section +is active, simply select the option you want using +the arrow keys and press Enter to confim. + + +Special keys for various input types are as follows: +- For text inputs you can simply type in the value. + +- For numeric inputs (indicated by the value being + in []s), you can type a value, or use PageUp and + PageDown to increment/decrement the value. + +- For checkbox inputs use the spacebar to toggle + +- For checkbox plus something else inputs (the + something else being only visible when you + check the box) you can toggle the check with + space, use the right arrow to edit the other + value, and escape to get back to the check box. + + +""" +HELP_LINES = HELP_STR.split('\n') + + +class ZONE: + CATEGORIES = 0 + PREFRENCES = 1 + ACTIONS = 2 + +class Preferences(BaseMode): + def __init__(self, parent_mode, core_config, console_config, active_port, status, stdscr, encoding=None): + self.parent_mode = parent_mode + self.categories = [_("Downloads"), _("Network"), _("Bandwidth"), + _("Interface"), _("Other"), _("Daemon"), _("Queue"), _("Proxy"), + _("Cache")] # , _("Plugins")] + self.cur_cat = 0 + self.popup = None + self.messages = deque() + self.action_input = None + + self.core_config = core_config + self.console_config = console_config + self.active_port = active_port + self.status = status + + self.active_zone = ZONE.CATEGORIES + + # how wide is the left 'pane' with categories + self.div_off = 15 + + BaseMode.__init__(self, stdscr, encoding, False) + + # create the panes + self.prefs_width = self.cols-self.div_off-1 + self.prefs_height = self.rows-4 + self.panes = [ + DownloadsPane(self.div_off+2, self, self.prefs_width), + NetworkPane(self.div_off+2, self, self.prefs_width), + BandwidthPane(self.div_off+2, self, self.prefs_width), + InterfacePane(self.div_off+2, self, self.prefs_width), + OtherPane(self.div_off+2, self, self.prefs_width), + DaemonPane(self.div_off+2, self, self.prefs_width), + QueuePane(self.div_off+2, self, self.prefs_width), + ProxyPane(self.div_off+2, self, self.prefs_width), + CachePane(self.div_off+2, self, self.prefs_width) + ] + + self.action_input = SelectInput(self,None,None,["Cancel","Apply","OK"],[0,1,2],0) + self.refresh() + + def __draw_catetories(self): + for i,category in enumerate(self.categories): + if i == self.cur_cat and self.active_zone == ZONE.CATEGORIES: + self.add_string(i+1,"- {!black,white,bold!}%s"%category,pad=False) + elif i == self.cur_cat: + self.add_string(i+1,"- {!black,white!}%s"%category,pad=False) + else: + self.add_string(i+1,"- %s"%category) + self.stdscr.vline(1,self.div_off,'|',self.rows-2) + + def __draw_preferences(self): + self.panes[self.cur_cat].render(self,self.stdscr, self.prefs_width, self.active_zone == ZONE.PREFRENCES) + + def __draw_actions(self): + selected = self.active_zone == ZONE.ACTIONS + self.stdscr.hline(self.rows-3,self.div_off+1,"_",self.cols) + self.action_input.render(self.stdscr,self.rows-2,self.cols,selected,self.cols-22) + + def refresh(self): + if self.popup == None and self.messages: + title,msg = self.messages.popleft() + self.popup = MessagePopup(self,title,msg) + + self.stdscr.clear() + self.add_string(0,self.statusbars.topbar) + hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) + self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) + + self.__draw_catetories() + self.__draw_actions() + + # do this last since it moves the cursor + self.__draw_preferences() + + self.stdscr.noutrefresh() + + if self.popup: + self.popup.refresh() + + curses.doupdate() + + def __category_read(self, c): + # Navigate prefs + if c == curses.KEY_UP: + self.cur_cat = max(0,self.cur_cat-1) + elif c == curses.KEY_DOWN: + self.cur_cat = min(len(self.categories)-1,self.cur_cat+1) + + def __prefs_read(self, c): + self.panes[self.cur_cat].handle_read(c) + + def __apply_prefs(self): + new_core_config = {} + for pane in self.panes: + if not isinstance(pane,InterfacePane): + pane.add_config_values(new_core_config) + # Apply Core Prefs + if client.connected(): + # Only do this if we're connected to a daemon + config_to_set = {} + for key in new_core_config.keys(): + # The values do not match so this needs to be updated + if self.core_config[key] != new_core_config[key]: + config_to_set[key] = new_core_config[key] + + if config_to_set: + # Set each changed config value in the core + client.core.set_config(config_to_set) + client.force_call(True) + # Update the configuration + self.core_config.update(config_to_set) + + # Update Interface Prefs + new_console_config = {} + didupdate = False + for pane in self.panes: + # could just access panes by index, but that would break if panes + # are ever reordered, so do it the slightly slower but safer way + if isinstance(pane,InterfacePane): + pane.add_config_values(new_console_config) + for key in new_console_config.keys(): + # The values do not match so this needs to be updated + if self.console_config[key] != new_console_config[key]: + self.console_config[key] = new_console_config[key] + didupdate = True + if didupdate: + # changed something, save config and tell alltorrents + self.console_config.save() + self.parent_mode.update_config() + + + def __update_preferences(self,core_config): + self.core_config = core_config + for pane in self.panes: + pane.update_values(core_config) + + def __actions_read(self, c): + self.action_input.handle_read(c) + if c == curses.KEY_ENTER or c == 10: + # take action + if self.action_input.selidx == 0: # cancel + self.back_to_parent() + elif self.action_input.selidx == 1: # apply + self.__apply_prefs() + client.core.get_config().addCallback(self.__update_preferences) + elif self.action_input.selidx == 2: # OK + self.__apply_prefs() + self.back_to_parent() + + + def back_to_parent(self): + self.stdscr.clear() + component.get("ConsoleUI").set_mode(self.parent_mode) + self.parent_mode.resume() + + def _doRead(self): + c = self.stdscr.getch() + + if self.popup: + if self.popup.handle_read(c): + self.popup = None + self.refresh() + return + + if c > 31 and c < 256: + if chr(c) == 'Q': + from twisted.internet import reactor + if client.connected(): + def on_disconnect(result): + reactor.stop() + client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() + return + elif chr(c) == 'h': + self.popup = Popup(self,"Preferences Help") + for l in HELP_LINES: + self.popup.add_line(l) + + if c == 9: + self.active_zone += 1 + if self.active_zone > ZONE.ACTIONS: + self.active_zone = ZONE.CATEGORIES + + elif c == curses.KEY_BTAB: + self.active_zone -= 1 + if self.active_zone < ZONE.CATEGORIES: + self.active_zone = ZONE.ACTIONS + + elif c == 114 and isinstance(self.panes[self.cur_cat],CachePane): + client.core.get_cache_status().addCallback(self.panes[self.cur_cat].update_cache_status) + + else: + if self.active_zone == ZONE.CATEGORIES: + self.__category_read(c) + elif self.active_zone == ZONE.PREFRENCES: + self.__prefs_read(c) + elif self.active_zone == ZONE.ACTIONS: + self.__actions_read(c) + + self.refresh() + + diff --git a/deluge/ui/console/modes/torrent_actions.py b/deluge/ui/console/modes/torrent_actions.py new file mode 100644 index 000000000..a15de5246 --- /dev/null +++ b/deluge/ui/console/modes/torrent_actions.py @@ -0,0 +1,161 @@ +# torrent_actions.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +from deluge.ui.client import client +from popup import SelectablePopup +from input_popup import InputPopup + +import logging +log = logging.getLogger(__name__) + +class ACTION: + PAUSE=0 + RESUME=1 + REANNOUNCE=2 + EDIT_TRACKERS=3 + RECHECK=4 + REMOVE=5 + REMOVE_DATA=6 + REMOVE_NODATA=7 + DETAILS=8 + MOVE_STORAGE=9 + QUEUE=10 + QUEUE_TOP=11 + QUEUE_UP=12 + QUEUE_DOWN=13 + QUEUE_BOTTOM=14 + +def action_error(error,mode): + rerr = error.value + mode.report_message("An Error Occurred","%s got error %s: %s"%(rerr.method,rerr.exception_type,rerr.exception_msg)) + mode.refresh() + +def torrent_action(idx, data, mode, ids): + if ids: + if data==ACTION.PAUSE: + log.debug("Pausing torrents: %s",ids) + client.core.pause_torrent(ids).addErrback(action_error,mode) + elif data==ACTION.RESUME: + log.debug("Resuming torrents: %s", ids) + client.core.resume_torrent(ids).addErrback(action_error,mode) + elif data==ACTION.QUEUE: + def do_queue(idx,qact,mode,ids): + if qact == ACTION.QUEUE_TOP: + log.debug("Queuing torrents top") + client.core.queue_top(ids) + elif qact == ACTION.QUEUE_UP: + log.debug("Queuing torrents up") + client.core.queue_up(ids) + elif qact == ACTION.QUEUE_DOWN: + log.debug("Queuing torrents down") + client.core.queue_down(ids) + elif qact == ACTION.QUEUE_BOTTOM: + log.debug("Queuing torrents bottom") + client.core.queue_bottom(ids) + if len(ids) == 1: + mode.clear_marks() + return True + popup = SelectablePopup(mode,"Queue Action",do_queue,mode,ids) + popup.add_line("_Top",data=ACTION.QUEUE_TOP) + popup.add_line("_Up",data=ACTION.QUEUE_UP) + popup.add_line("_Down",data=ACTION.QUEUE_DOWN) + popup.add_line("_Bottom",data=ACTION.QUEUE_BOTTOM) + mode.set_popup(popup) + return False + elif data==ACTION.REMOVE: + def do_remove(idx,data,mode,ids): + if data: + wd = data==ACTION.REMOVE_DATA + for tid in ids: + log.debug("Removing torrent: %s,%d",tid,wd) + client.core.remove_torrent(tid,wd).addErrback(action_error,mode) + if len(ids) == 1: + mode.clear_marks() + return True + popup = SelectablePopup(mode,"Confirm Remove",do_remove,mode,ids) + popup.add_line("Are you sure you want to remove the marked torrents?",selectable=False) + popup.add_line("Remove with _data",data=ACTION.REMOVE_DATA) + popup.add_line("Remove _torrent",data=ACTION.REMOVE_NODATA) + popup.add_line("_Cancel",data=0) + mode.set_popup(popup) + return False + elif data==ACTION.MOVE_STORAGE: + def do_move(res): + import os.path + if os.path.exists(res["path"]) and not os.path.isdir(res["path"]): + mode.report_message("Cannot Move Storage","{!error!}%s exists and is not a directory"%res["path"]) + else: + log.debug("Moving %s to: %s",ids,res["path"]) + client.core.move_storage(ids,res["path"]).addErrback(action_error,mode) + if len(ids) == 1: + mode.clear_marks() + return True + popup = InputPopup(mode,"Move Storage (Esc to cancel)",close_cb=do_move) + popup.add_text_input("Enter path to move to:","path") + mode.set_popup(popup) + return False + elif data==ACTION.RECHECK: + log.debug("Rechecking torrents: %s", ids) + client.core.force_recheck(ids).addErrback(action_error,mode) + elif data==ACTION.REANNOUNCE: + log.debug("Reannouncing torrents: %s",ids) + client.core.force_reannounce(ids).addErrback(action_error,mode) + elif data==ACTION.DETAILS: + log.debug("Torrent details") + tid = mode.current_torrent_id() + if tid: + mode.show_torrent_details(tid) + else: + log.error("No current torrent in _torrent_action, this is a bug") + if len(ids) == 1: + mode.clear_marks() + return True + +# Creates the popup. mode is the calling mode, tids is a list of torrents to take action upon +def torrent_actions_popup(mode,tids,details=False): + popup = SelectablePopup(mode,"Torrent Actions",torrent_action,mode,tids) + popup.add_line("_Pause",data=ACTION.PAUSE) + popup.add_line("_Resume",data=ACTION.RESUME) + popup.add_divider() + popup.add_line("Queue",data=ACTION.QUEUE) + popup.add_divider() + popup.add_line("_Update Tracker",data=ACTION.REANNOUNCE) + popup.add_divider() + popup.add_line("Remo_ve Torrent",data=ACTION.REMOVE) + popup.add_line("_Force Recheck",data=ACTION.RECHECK) + popup.add_line("_Move Storage",data=ACTION.MOVE_STORAGE) + if details: + popup.add_divider() + popup.add_line("Torrent _Details",data=ACTION.DETAILS) + mode.set_popup(popup) diff --git a/deluge/ui/console/modes/torrentdetail.py b/deluge/ui/console/modes/torrentdetail.py new file mode 100644 index 000000000..ae9161e1d --- /dev/null +++ b/deluge/ui/console/modes/torrentdetail.py @@ -0,0 +1,536 @@ +# -*- coding: utf-8 -*- +# +# torrentdetail.py +# +# Copyright (C) 2011 Nick Lanham +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import deluge.component as component +from basemode import BaseMode +import deluge.common +from deluge.ui.client import client + +from sys import maxint +from collections import deque + +from deluge.ui.sessionproxy import SessionProxy + +from popup import Popup,SelectablePopup,MessagePopup +from add_util import add_torrent +from input_popup import InputPopup +import format_utils + +from torrent_actions import torrent_actions_popup + +try: + import curses +except ImportError: + pass + +import logging +log = logging.getLogger(__name__) + +# Big help string that gets displayed when the user hits 'h' +HELP_STR = """\ +This screen shows detailed information about a torrent, and also the \ +information about the individual files in the torrent. + +You can navigate the file list with the Up/Down arrows and use space to \ +collapse/expand the file tree. + +All popup windows can be closed/canceled by hitting the Esc key \ +(you might need to wait a second for an Esc to register) + +The actions you can perform and the keys to perform them are as follows: + +{!info!}'h'{!normal!} - Show this help + +{!info!}'a'{!normal!} - Show torrent actions popup. Here you can do things like \ +pause/resume, remove, recheck and so on. + +{!info!}'m'{!normal!} - Mark a file +{!info!}'c'{!normal!} - Un-mark all files + +{!info!}Space{!normal!} - Expand/Collapse currently selected folder + +{!info!}Enter{!normal!} - Show priority popup in which you can set the \ +download priority of selected files. + +{!info!}Left Arrow{!normal!} - Go back to torrent overview. +""" + +class TorrentDetail(BaseMode, component.Component): + def __init__(self, alltorrentmode, torrentid, stdscr, encoding=None): + self.alltorrentmode = alltorrentmode + self.torrentid = torrentid + self.torrent_state = None + self.popup = None + self.messages = deque() + self._status_keys = ["files", "name","state","download_payload_rate","upload_payload_rate", + "progress","eta","all_time_download","total_uploaded", "ratio", + "num_seeds","total_seeds","num_peers","total_peers", "active_time", + "seeding_time","time_added","distributed_copies", "num_pieces", + "piece_length","save_path","file_progress","file_priorities","message"] + self._info_fields = [ + ("Name",None,("name",)), + ("State", None, ("state",)), + ("Status",None,("message",)), + ("Down Speed", format_utils.format_speed, ("download_payload_rate",)), + ("Up Speed", format_utils.format_speed, ("upload_payload_rate",)), + ("Progress", format_utils.format_progress, ("progress",)), + ("ETA", deluge.common.ftime, ("eta",)), + ("Path", None, ("save_path",)), + ("Downloaded",deluge.common.fsize,("all_time_download",)), + ("Uploaded", deluge.common.fsize,("total_uploaded",)), + ("Share Ratio", lambda x:x < 0 and "∞" or "%.3f"%x, ("ratio",)), + ("Seeders",format_utils.format_seeds_peers,("num_seeds","total_seeds")), + ("Peers",format_utils.format_seeds_peers,("num_peers","total_peers")), + ("Active Time",deluge.common.ftime,("active_time",)), + ("Seeding Time",deluge.common.ftime,("seeding_time",)), + ("Date Added",deluge.common.fdate,("time_added",)), + ("Availability", lambda x:x < 0 and "∞" or "%.3f"%x, ("distributed_copies",)), + ("Pieces", format_utils.format_pieces, ("num_pieces","piece_length")), + ] + self.file_list = None + self.current_file = None + self.current_file_idx = 0 + self.file_limit = maxint + self.file_off = 0 + self.more_to_draw = False + + self.column_string = "" + self.files_sep = None + + self.marked = {} + + BaseMode.__init__(self, stdscr, encoding) + component.Component.__init__(self, "TorrentDetail", 1, depend=["SessionProxy"]) + + self.__split_help() + + self.column_names = ["Filename", "Size", "Progress", "Priority"] + self.__update_columns() + + component.start(["TorrentDetail"]) + curses.curs_set(0) + self.stdscr.notimeout(0) + + # component start/update + def start(self): + component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state) + def update(self): + component.get("SessionProxy").get_torrent_status(self.torrentid, self._status_keys).addCallback(self.set_state) + + def set_state(self, state): + log.debug("got state") + need_prio_update = False + if not self.file_list: + # don't keep getting the files once we've got them once + if state.get("files"): + self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (torrent has %d files)"%len(state["files"])).center(self.cols)) + self.file_list,self.file_dict = self.build_file_list(state["files"],state["file_progress"],state["file_priorities"]) + self._status_keys.remove("files") + else: + self.files_sep = "{!green,black,bold,underline!}%s"%(("Files (File list unknown)").center(self.cols)) + need_prio_update = True + self.__fill_progress(self.file_list,state["file_progress"]) + for i,prio in enumerate(state["file_priorities"]): + if self.file_dict[i][6] != prio: + need_prio_update = True + self.file_dict[i][6] = prio + if need_prio_update: + self.__fill_prio(self.file_list) + del state["file_progress"] + del state["file_priorities"] + self.torrent_state = state + self.refresh() + + def __split_help(self): + self.__help_lines = format_utils.wrap_string(HELP_STR,(self.cols/2)-2) + + # split file list into directory tree. this function assumes all files in a + # particular directory are returned together. it won't work otherwise. + # returned list is a list of lists of the form: + # [file/dir_name,index,size,children,expanded,progress,priority] + # for directories index values count down from maxint (for marking usage), + # for files the index is the value returned in the + # state object for use with other libtorrent calls (i.e. setting prio) + # + # Also returns a dictionary that maps index values to the file leaves + # for fast updating of progress and priorities + def build_file_list(self, file_tuples,prog,prio): + ret = [] + retdict = {} + diridx = maxint + for f in file_tuples: + cur = ret + ps = f["path"].split("/") + fin = ps[-1] + for p in ps: + if not cur or p != cur[-1][0]: + cl = [] + if p == fin: + ent = [p,f["index"],f["size"],cl,False, + format_utils.format_progress(prog[f["index"]]*100), + prio[f["index"]]] + retdict[f["index"]] = ent + else: + ent = [p,diridx,-1,cl,False,0,-1] + retdict[diridx] = ent + diridx-=1 + cur.append(ent) + cur = cl + else: + cur = cur[-1][3] + self.__build_sizes(ret) + self.__fill_progress(ret,prog) + return (ret,retdict) + + # fill in the sizes of the directory entries based on their children + def __build_sizes(self, fs): + ret = 0 + for f in fs: + if f[2] == -1: + val = self.__build_sizes(f[3]) + ret += val + f[2] = val + else: + ret += f[2] + return ret + + # fills in progress fields in all entries based on progs + # returns the # of bytes complete in all the children of fs + def __fill_progress(self,fs,progs): + if not progs: return 0 + tb = 0 + for f in fs: + if f[3]: # dir, has some children + bd = self.__fill_progress(f[3],progs) + f[5] = format_utils.format_progress((bd/f[2])*100) + else: # file, update own prog and add to total + bd = f[2]*progs[f[1]] + f[5] = format_utils.format_progress(progs[f[1]]*100) + tb += bd + return tb + + def __fill_prio(self,fs): + for f in fs: + if f[3]: # dir, so fill in children and compute our prio + self.__fill_prio(f[3]) + s = set([e[6] for e in f[3]]) # pull out all child prios and turn into a set + if len(s) > 1: + f[6] = -2 # mixed + else: + f[6] = s.pop() + + def __update_columns(self): + self.column_widths = [-1,15,15,20] + req = sum(filter(lambda x:x >= 0,self.column_widths)) + if (req > self.cols): # can't satisfy requests, just spread out evenly + cw = int(self.cols/len(self.column_names)) + for i in range(0,len(self.column_widths)): + self.column_widths[i] = cw + else: + rem = self.cols - req + var_cols = len(filter(lambda x: x < 0,self.column_widths)) + vw = int(rem/var_cols) + for i in range(0, len(self.column_widths)): + if (self.column_widths[i] < 0): + self.column_widths[i] = vw + + self.column_string = "{!green,black,bold!}%s"%("".join(["%s%s"%(self.column_names[i]," "*(self.column_widths[i]-len(self.column_names[i]))) for i in range(0,len(self.column_names))])) + + + def report_message(self,title,message): + self.messages.append((title,message)) + + def clear_marks(self): + self.marked = {} + + def set_popup(self,pu): + self.popup = pu + self.refresh() + + + def draw_files(self,files,depth,off,idx): + for fl in files: + # kick out if we're going to draw too low on the screen + if (off >= self.rows-1): + self.more_to_draw = True + return -1,-1 + + self.file_limit = idx + + if idx >= self.file_off: + # set fg/bg colors based on if we are selected/marked or not + + # default values + fg = "white" + bg = "black" + + if fl[1] in self.marked: + bg = "blue" + + if idx == self.current_file_idx: + self.current_file = fl + bg = "white" + if fl[1] in self.marked: + fg = "blue" + else: + fg = "black" + + color_string = "{!%s,%s!}"%(fg,bg) + + #actually draw the dir/file string + if fl[3] and fl[4]: # this is an expanded directory + xchar = 'v' + elif fl[3]: # collapsed directory + xchar = '>' + else: # file + xchar = '-' + + r = format_utils.format_row(["%s%s %s"%(" "*depth,xchar,fl[0]), + deluge.common.fsize(fl[2]),fl[5], + format_utils.format_priority(fl[6])], + self.column_widths) + + self.add_string(off,"%s%s"%(color_string,r),trim=False) + off += 1 + + if fl[3] and fl[4]: + # recurse if we have children and are expanded + off,idx = self.draw_files(fl[3],depth+1,off,idx+1) + if off < 0: return (off,idx) + else: + idx += 1 + + return (off,idx) + + def on_resize(self, *args): + BaseMode.on_resize_norefresh(self, *args) + self.__update_columns() + self.__split_help() + if self.popup: + self.popup.handle_resize() + self.refresh() + + def refresh(self,lines=None): + # show a message popup if there's anything queued + if self.popup == None and self.messages: + title,msg = self.messages.popleft() + self.popup = MessagePopup(self,title,msg) + + # Update the status bars + self.stdscr.clear() + self.add_string(0,self.statusbars.topbar) + hstr = "%sPress [h] for help"%(" "*(self.cols - len(self.statusbars.bottombar) - 10)) + self.add_string(self.rows - 1, "%s%s"%(self.statusbars.bottombar,hstr)) + + if self.files_sep: + self.add_string((self.rows/2)-1,self.files_sep) + + off = 1 + if self.torrent_state: + for f in self._info_fields: + if off >= (self.rows/2): break + if f[1] != None: + args = [] + try: + for key in f[2]: + args.append(self.torrent_state[key]) + except: + log.debug("Could not get info field: %s",e) + continue + info = f[1](*args) + else: + info = self.torrent_state[f[2][0]] + + self.add_string(off,"{!info!}%s: {!input!}%s"%(f[0],info)) + off += 1 + else: + self.add_string(1, "Waiting for torrent state") + + off = self.rows/2 + self.add_string(off,self.column_string) + if self.file_list: + off += 1 + self.more_to_draw = False + self.draw_files(self.file_list,0,off,0) + + #self.stdscr.redrawwin() + self.stdscr.noutrefresh() + + if self.popup: + self.popup.refresh() + + curses.doupdate() + + # expand or collapse the current file + def expcol_cur_file(self): + self.current_file[4] = not self.current_file[4] + self.refresh() + + def file_list_down(self): + if (self.current_file_idx + 1) > self.file_limit: + if self.more_to_draw: + self.current_file_idx += 1 + self.file_off += 1 + else: + return + else: + self.current_file_idx += 1 + + self.refresh() + + def file_list_up(self): + self.current_file_idx = max(0,self.current_file_idx-1) + self.file_off = min(self.file_off,self.current_file_idx) + self.refresh() + + def back_to_overview(self): + component.stop(["TorrentDetail"]) + component.deregister("TorrentDetail") + self.stdscr.clear() + component.get("ConsoleUI").set_mode(self.alltorrentmode) + self.alltorrentmode.resume() + + # build list of priorities for all files in the torrent + # based on what is currently selected and a selected priority. + def build_prio_list(self, files, ret_list, parent_prio, selected_prio): + # has a priority been set on my parent (if so, I inherit it) + for f in files: + if f[3]: # dir, check if i'm setting on whole dir, then recurse + if f[1] in self.marked: # marked, recurse and update all children with new prio + parent_prio = selected_prio + self.build_prio_list(f[3],ret_list,parent_prio,selected_prio) + parent_prio = -1 + else: # not marked, just recurse + self.build_prio_list(f[3],ret_list,parent_prio,selected_prio) + else: # file, need to add to list + if f[1] in self.marked or parent_prio >= 0: + # selected (or parent selected), use requested priority + ret_list.append((f[1],selected_prio)) + else: + # not selected, just keep old priority + ret_list.append((f[1],f[6])) + + def do_priority(self, idx, data): + plist = [] + self.build_prio_list(self.file_list,plist,-1,data) + plist.sort() + priorities = [p[1] for p in plist] + log.debug("priorities: %s", priorities) + + client.core.set_torrent_file_priorities(self.torrentid, priorities) + + if len(self.marked) == 1: + self.marked = {} + return True + + # show popup for priority selections + def show_priority_popup(self): + if self.marked: + self.popup = SelectablePopup(self,"Set File Priority",self.do_priority) + self.popup.add_line("_Do Not Download",data=deluge.common.FILE_PRIORITY["Do Not Download"]) + self.popup.add_line("_Normal Priority",data=deluge.common.FILE_PRIORITY["Normal Priority"]) + self.popup.add_line("_High Priority",data=deluge.common.FILE_PRIORITY["High Priority"]) + self.popup.add_line("H_ighest Priority",data=deluge.common.FILE_PRIORITY["Highest Priority"]) + + def __mark_unmark(self,idx): + if idx in self.marked: + del self.marked[idx] + else: + self.marked[idx] = True + + def _doRead(self): + c = self.stdscr.getch() + + if self.popup: + if self.popup.handle_read(c): + self.popup = None + self.refresh() + return + + if c > 31 and c < 256: + if chr(c) == 'Q': + from twisted.internet import reactor + if client.connected(): + def on_disconnect(result): + reactor.stop() + client.disconnect().addCallback(on_disconnect) + else: + reactor.stop() + return + elif chr(c) == 'q': + self.back_to_overview() + return + + if c == 27 or c == curses.KEY_LEFT: + self.back_to_overview() + return + + if not self.torrent_state: + # actions below only makes sense if there is a torrent state + return + + # Navigate the torrent list + if c == curses.KEY_UP: + self.file_list_up() + elif c == curses.KEY_PPAGE: + pass + elif c == curses.KEY_DOWN: + self.file_list_down() + elif c == curses.KEY_NPAGE: + pass + + # Enter Key + elif c == curses.KEY_ENTER or c == 10: + self.marked[self.current_file[1]] = True + self.show_priority_popup() + + # space + elif c == 32: + self.expcol_cur_file() + else: + if c > 31 and c < 256: + if chr(c) == 'm': + if self.current_file: + self.__mark_unmark(self.current_file[1]) + elif chr(c) == 'c': + self.marked = {} + elif chr(c) == 'a': + torrent_actions_popup(self,[self.torrentid],details=False) + return + elif chr(c) == 'h': + self.popup = Popup(self,"Help",init_lines=self.__help_lines) + + self.refresh() diff --git a/deluge/ui/console/statusbars.py b/deluge/ui/console/statusbars.py index 44be33cf0..9d7a09fec 100644 --- a/deluge/ui/console/statusbars.py +++ b/deluge/ui/console/statusbars.py @@ -41,7 +41,6 @@ class StatusBars(component.Component): def __init__(self): component.Component.__init__(self, "StatusBars", 2, depend=["CoreConfig"]) self.config = component.get("CoreConfig") - self.screen = component.get("ConsoleUI").screen # Hold some values we get from the core self.connections = 0 @@ -49,6 +48,10 @@ class StatusBars(component.Component): self.upload = "" self.dht = 0 + # Default values + self.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version() + self.bottombar = "{!status!}C: %s" % self.connections + def start(self): self.update() @@ -77,30 +80,28 @@ class StatusBars(component.Component): def update_statusbars(self): # Update the topbar string - self.screen.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version() + self.topbar = "{!status!}Deluge %s Console - " % deluge.common.get_version() if client.connected(): info = client.connection_info() - self.screen.topbar += "%s@%s:%s" % (info[2], info[0], info[1]) + self.topbar += "%s@%s:%s" % (info[2], info[0], info[1]) else: - self.screen.topbar += "Not Connected" + self.topbar += "Not Connected" # Update the bottombar string - self.screen.bottombar = "{!status!}C: %s" % self.connections + self.bottombar = "{!status!}C: %s" % self.connections if self.config["max_connections_global"] > -1: - self.screen.bottombar += " (%s)" % self.config["max_connections_global"] + self.bottombar += " (%s)" % self.config["max_connections_global"] - self.screen.bottombar += " D: %s/s" % self.download + self.bottombar += " D: %s/s" % self.download if self.config["max_download_speed"] > -1: - self.screen.bottombar += " (%s KiB/s)" % self.config["max_download_speed"] + self.bottombar += " (%s KiB/s)" % self.config["max_download_speed"] - self.screen.bottombar += " U: %s/s" % self.upload + self.bottombar += " U: %s/s" % self.upload if self.config["max_upload_speed"] > -1: - self.screen.bottombar += " (%s KiB/s)" % self.config["max_upload_speed"] + self.bottombar += " (%s KiB/s)" % self.config["max_upload_speed"] if self.config["dht"]: - self.screen.bottombar += " DHT: %s" % self.dht - - self.screen.refresh() + self.bottombar += " DHT: %s" % self.dht diff --git a/deluge/ui/coreconfig.py b/deluge/ui/coreconfig.py index fe002e64a..6120dcfa8 100644 --- a/deluge/ui/coreconfig.py +++ b/deluge/ui/coreconfig.py @@ -45,8 +45,8 @@ class CoreConfig(component.Component): log.debug("CoreConfig init..") component.Component.__init__(self, "CoreConfig") self.config = {} - def on_configvaluechanged_event(event): - self.config[event.key] = event.value + def on_configvaluechanged_event(key, value): + self.config[key] = value client.register_event_handler("ConfigValueChangedEvent", on_configvaluechanged_event) def start(self): diff --git a/deluge/data/icons/hicolor/128x128/apps/deluge.png b/deluge/ui/data/icons/hicolor/128x128/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/128x128/apps/deluge.png rename to deluge/ui/data/icons/hicolor/128x128/apps/deluge.png diff --git a/deluge/data/icons/hicolor/16x16/apps/deluge.png b/deluge/ui/data/icons/hicolor/16x16/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/16x16/apps/deluge.png rename to deluge/ui/data/icons/hicolor/16x16/apps/deluge.png diff --git a/deluge/data/icons/hicolor/192x192/apps/deluge.png b/deluge/ui/data/icons/hicolor/192x192/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/192x192/apps/deluge.png rename to deluge/ui/data/icons/hicolor/192x192/apps/deluge.png diff --git a/deluge/data/icons/hicolor/22x22/apps/deluge.png b/deluge/ui/data/icons/hicolor/22x22/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/22x22/apps/deluge.png rename to deluge/ui/data/icons/hicolor/22x22/apps/deluge.png diff --git a/deluge/data/icons/hicolor/24x24/apps/deluge.png b/deluge/ui/data/icons/hicolor/24x24/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/24x24/apps/deluge.png rename to deluge/ui/data/icons/hicolor/24x24/apps/deluge.png diff --git a/deluge/data/icons/hicolor/256x256/apps/deluge.png b/deluge/ui/data/icons/hicolor/256x256/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/256x256/apps/deluge.png rename to deluge/ui/data/icons/hicolor/256x256/apps/deluge.png diff --git a/deluge/data/icons/hicolor/32x32/apps/deluge.png b/deluge/ui/data/icons/hicolor/32x32/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/32x32/apps/deluge.png rename to deluge/ui/data/icons/hicolor/32x32/apps/deluge.png diff --git a/deluge/data/icons/hicolor/36x36/apps/deluge.png b/deluge/ui/data/icons/hicolor/36x36/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/36x36/apps/deluge.png rename to deluge/ui/data/icons/hicolor/36x36/apps/deluge.png diff --git a/deluge/data/icons/hicolor/48x48/apps/deluge.png b/deluge/ui/data/icons/hicolor/48x48/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/48x48/apps/deluge.png rename to deluge/ui/data/icons/hicolor/48x48/apps/deluge.png diff --git a/deluge/data/icons/hicolor/64x64/apps/deluge.png b/deluge/ui/data/icons/hicolor/64x64/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/64x64/apps/deluge.png rename to deluge/ui/data/icons/hicolor/64x64/apps/deluge.png diff --git a/deluge/data/icons/hicolor/72x72/apps/deluge.png b/deluge/ui/data/icons/hicolor/72x72/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/72x72/apps/deluge.png rename to deluge/ui/data/icons/hicolor/72x72/apps/deluge.png diff --git a/deluge/data/icons/hicolor/96x96/apps/deluge.png b/deluge/ui/data/icons/hicolor/96x96/apps/deluge.png similarity index 100% rename from deluge/data/icons/hicolor/96x96/apps/deluge.png rename to deluge/ui/data/icons/hicolor/96x96/apps/deluge.png diff --git a/deluge/data/icons/scalable/apps/deluge.svg b/deluge/ui/data/icons/scalable/apps/deluge.svg similarity index 100% rename from deluge/data/icons/scalable/apps/deluge.svg rename to deluge/ui/data/icons/scalable/apps/deluge.svg diff --git a/deluge/data/pixmaps/active.svg b/deluge/ui/data/pixmaps/active.svg similarity index 100% rename from deluge/data/pixmaps/active.svg rename to deluge/ui/data/pixmaps/active.svg diff --git a/deluge/data/pixmaps/active16.png b/deluge/ui/data/pixmaps/active16.png similarity index 100% rename from deluge/data/pixmaps/active16.png rename to deluge/ui/data/pixmaps/active16.png diff --git a/deluge/data/pixmaps/alert.svg b/deluge/ui/data/pixmaps/alert.svg similarity index 100% rename from deluge/data/pixmaps/alert.svg rename to deluge/ui/data/pixmaps/alert.svg diff --git a/deluge/data/pixmaps/alert16.png b/deluge/ui/data/pixmaps/alert16.png similarity index 100% rename from deluge/data/pixmaps/alert16.png rename to deluge/ui/data/pixmaps/alert16.png diff --git a/deluge/data/pixmaps/all.svg b/deluge/ui/data/pixmaps/all.svg similarity index 100% rename from deluge/data/pixmaps/all.svg rename to deluge/ui/data/pixmaps/all.svg diff --git a/deluge/data/pixmaps/all16.png b/deluge/ui/data/pixmaps/all16.png similarity index 100% rename from deluge/data/pixmaps/all16.png rename to deluge/ui/data/pixmaps/all16.png diff --git a/deluge/data/pixmaps/checking.svg b/deluge/ui/data/pixmaps/checking.svg similarity index 100% rename from deluge/data/pixmaps/checking.svg rename to deluge/ui/data/pixmaps/checking.svg diff --git a/deluge/data/pixmaps/checking16.png b/deluge/ui/data/pixmaps/checking16.png similarity index 100% rename from deluge/data/pixmaps/checking16.png rename to deluge/ui/data/pixmaps/checking16.png diff --git a/deluge/data/pixmaps/deluge-about.png b/deluge/ui/data/pixmaps/deluge-about.png similarity index 100% rename from deluge/data/pixmaps/deluge-about.png rename to deluge/ui/data/pixmaps/deluge-about.png diff --git a/deluge/data/pixmaps/deluge.ico b/deluge/ui/data/pixmaps/deluge.ico similarity index 100% rename from deluge/data/pixmaps/deluge.ico rename to deluge/ui/data/pixmaps/deluge.ico diff --git a/deluge/data/pixmaps/deluge.png b/deluge/ui/data/pixmaps/deluge.png similarity index 100% rename from deluge/data/pixmaps/deluge.png rename to deluge/ui/data/pixmaps/deluge.png diff --git a/deluge/data/pixmaps/deluge.svg b/deluge/ui/data/pixmaps/deluge.svg similarity index 100% rename from deluge/data/pixmaps/deluge.svg rename to deluge/ui/data/pixmaps/deluge.svg diff --git a/deluge/data/pixmaps/deluge.xpm b/deluge/ui/data/pixmaps/deluge.xpm similarity index 100% rename from deluge/data/pixmaps/deluge.xpm rename to deluge/ui/data/pixmaps/deluge.xpm diff --git a/deluge/data/pixmaps/deluge16.png b/deluge/ui/data/pixmaps/deluge16.png similarity index 100% rename from deluge/data/pixmaps/deluge16.png rename to deluge/ui/data/pixmaps/deluge16.png diff --git a/deluge/data/pixmaps/dht.svg b/deluge/ui/data/pixmaps/dht.svg similarity index 100% rename from deluge/data/pixmaps/dht.svg rename to deluge/ui/data/pixmaps/dht.svg diff --git a/deluge/data/pixmaps/dht16.png b/deluge/ui/data/pixmaps/dht16.png similarity index 100% rename from deluge/data/pixmaps/dht16.png rename to deluge/ui/data/pixmaps/dht16.png diff --git a/deluge/data/pixmaps/downloading.svg b/deluge/ui/data/pixmaps/downloading.svg similarity index 100% rename from deluge/data/pixmaps/downloading.svg rename to deluge/ui/data/pixmaps/downloading.svg diff --git a/deluge/data/pixmaps/downloading16.png b/deluge/ui/data/pixmaps/downloading16.png similarity index 100% rename from deluge/data/pixmaps/downloading16.png rename to deluge/ui/data/pixmaps/downloading16.png diff --git a/deluge/data/pixmaps/flags/FLAGS_LICENCE b/deluge/ui/data/pixmaps/flags/FLAGS_LICENCE similarity index 100% rename from deluge/data/pixmaps/flags/FLAGS_LICENCE rename to deluge/ui/data/pixmaps/flags/FLAGS_LICENCE diff --git a/deluge/data/pixmaps/flags/ad.png b/deluge/ui/data/pixmaps/flags/ad.png similarity index 100% rename from deluge/data/pixmaps/flags/ad.png rename to deluge/ui/data/pixmaps/flags/ad.png diff --git a/deluge/data/pixmaps/flags/ae.png b/deluge/ui/data/pixmaps/flags/ae.png similarity index 100% rename from deluge/data/pixmaps/flags/ae.png rename to deluge/ui/data/pixmaps/flags/ae.png diff --git a/deluge/data/pixmaps/flags/af.png b/deluge/ui/data/pixmaps/flags/af.png similarity index 100% rename from deluge/data/pixmaps/flags/af.png rename to deluge/ui/data/pixmaps/flags/af.png diff --git a/deluge/data/pixmaps/flags/ag.png b/deluge/ui/data/pixmaps/flags/ag.png similarity index 100% rename from deluge/data/pixmaps/flags/ag.png rename to deluge/ui/data/pixmaps/flags/ag.png diff --git a/deluge/data/pixmaps/flags/ai.png b/deluge/ui/data/pixmaps/flags/ai.png similarity index 100% rename from deluge/data/pixmaps/flags/ai.png rename to deluge/ui/data/pixmaps/flags/ai.png diff --git a/deluge/data/pixmaps/flags/al.png b/deluge/ui/data/pixmaps/flags/al.png similarity index 100% rename from deluge/data/pixmaps/flags/al.png rename to deluge/ui/data/pixmaps/flags/al.png diff --git a/deluge/data/pixmaps/flags/am.png b/deluge/ui/data/pixmaps/flags/am.png similarity index 100% rename from deluge/data/pixmaps/flags/am.png rename to deluge/ui/data/pixmaps/flags/am.png diff --git a/deluge/data/pixmaps/flags/an.png b/deluge/ui/data/pixmaps/flags/an.png similarity index 100% rename from deluge/data/pixmaps/flags/an.png rename to deluge/ui/data/pixmaps/flags/an.png diff --git a/deluge/data/pixmaps/flags/ao.png b/deluge/ui/data/pixmaps/flags/ao.png similarity index 100% rename from deluge/data/pixmaps/flags/ao.png rename to deluge/ui/data/pixmaps/flags/ao.png diff --git a/deluge/data/pixmaps/flags/aq.png b/deluge/ui/data/pixmaps/flags/aq.png similarity index 100% rename from deluge/data/pixmaps/flags/aq.png rename to deluge/ui/data/pixmaps/flags/aq.png diff --git a/deluge/data/pixmaps/flags/ar.png b/deluge/ui/data/pixmaps/flags/ar.png similarity index 100% rename from deluge/data/pixmaps/flags/ar.png rename to deluge/ui/data/pixmaps/flags/ar.png diff --git a/deluge/data/pixmaps/flags/as.png b/deluge/ui/data/pixmaps/flags/as.png similarity index 100% rename from deluge/data/pixmaps/flags/as.png rename to deluge/ui/data/pixmaps/flags/as.png diff --git a/deluge/data/pixmaps/flags/at.png b/deluge/ui/data/pixmaps/flags/at.png similarity index 100% rename from deluge/data/pixmaps/flags/at.png rename to deluge/ui/data/pixmaps/flags/at.png diff --git a/deluge/data/pixmaps/flags/au.png b/deluge/ui/data/pixmaps/flags/au.png similarity index 100% rename from deluge/data/pixmaps/flags/au.png rename to deluge/ui/data/pixmaps/flags/au.png diff --git a/deluge/data/pixmaps/flags/aw.png b/deluge/ui/data/pixmaps/flags/aw.png similarity index 100% rename from deluge/data/pixmaps/flags/aw.png rename to deluge/ui/data/pixmaps/flags/aw.png diff --git a/deluge/data/pixmaps/flags/ax.png b/deluge/ui/data/pixmaps/flags/ax.png similarity index 100% rename from deluge/data/pixmaps/flags/ax.png rename to deluge/ui/data/pixmaps/flags/ax.png diff --git a/deluge/data/pixmaps/flags/az.png b/deluge/ui/data/pixmaps/flags/az.png similarity index 100% rename from deluge/data/pixmaps/flags/az.png rename to deluge/ui/data/pixmaps/flags/az.png diff --git a/deluge/data/pixmaps/flags/ba.png b/deluge/ui/data/pixmaps/flags/ba.png similarity index 100% rename from deluge/data/pixmaps/flags/ba.png rename to deluge/ui/data/pixmaps/flags/ba.png diff --git a/deluge/data/pixmaps/flags/bb.png b/deluge/ui/data/pixmaps/flags/bb.png similarity index 100% rename from deluge/data/pixmaps/flags/bb.png rename to deluge/ui/data/pixmaps/flags/bb.png diff --git a/deluge/data/pixmaps/flags/bd.png b/deluge/ui/data/pixmaps/flags/bd.png similarity index 100% rename from deluge/data/pixmaps/flags/bd.png rename to deluge/ui/data/pixmaps/flags/bd.png diff --git a/deluge/data/pixmaps/flags/be.png b/deluge/ui/data/pixmaps/flags/be.png similarity index 100% rename from deluge/data/pixmaps/flags/be.png rename to deluge/ui/data/pixmaps/flags/be.png diff --git a/deluge/data/pixmaps/flags/bf.png b/deluge/ui/data/pixmaps/flags/bf.png similarity index 100% rename from deluge/data/pixmaps/flags/bf.png rename to deluge/ui/data/pixmaps/flags/bf.png diff --git a/deluge/data/pixmaps/flags/bg.png b/deluge/ui/data/pixmaps/flags/bg.png similarity index 100% rename from deluge/data/pixmaps/flags/bg.png rename to deluge/ui/data/pixmaps/flags/bg.png diff --git a/deluge/data/pixmaps/flags/bh.png b/deluge/ui/data/pixmaps/flags/bh.png similarity index 100% rename from deluge/data/pixmaps/flags/bh.png rename to deluge/ui/data/pixmaps/flags/bh.png diff --git a/deluge/data/pixmaps/flags/bi.png b/deluge/ui/data/pixmaps/flags/bi.png similarity index 100% rename from deluge/data/pixmaps/flags/bi.png rename to deluge/ui/data/pixmaps/flags/bi.png diff --git a/deluge/data/pixmaps/flags/bj.png b/deluge/ui/data/pixmaps/flags/bj.png similarity index 100% rename from deluge/data/pixmaps/flags/bj.png rename to deluge/ui/data/pixmaps/flags/bj.png diff --git a/deluge/data/pixmaps/flags/bm.png b/deluge/ui/data/pixmaps/flags/bm.png similarity index 100% rename from deluge/data/pixmaps/flags/bm.png rename to deluge/ui/data/pixmaps/flags/bm.png diff --git a/deluge/data/pixmaps/flags/bn.png b/deluge/ui/data/pixmaps/flags/bn.png similarity index 100% rename from deluge/data/pixmaps/flags/bn.png rename to deluge/ui/data/pixmaps/flags/bn.png diff --git a/deluge/data/pixmaps/flags/bo.png b/deluge/ui/data/pixmaps/flags/bo.png similarity index 100% rename from deluge/data/pixmaps/flags/bo.png rename to deluge/ui/data/pixmaps/flags/bo.png diff --git a/deluge/data/pixmaps/flags/br.png b/deluge/ui/data/pixmaps/flags/br.png similarity index 100% rename from deluge/data/pixmaps/flags/br.png rename to deluge/ui/data/pixmaps/flags/br.png diff --git a/deluge/data/pixmaps/flags/bs.png b/deluge/ui/data/pixmaps/flags/bs.png similarity index 100% rename from deluge/data/pixmaps/flags/bs.png rename to deluge/ui/data/pixmaps/flags/bs.png diff --git a/deluge/data/pixmaps/flags/bt.png b/deluge/ui/data/pixmaps/flags/bt.png similarity index 100% rename from deluge/data/pixmaps/flags/bt.png rename to deluge/ui/data/pixmaps/flags/bt.png diff --git a/deluge/data/pixmaps/flags/bv.png b/deluge/ui/data/pixmaps/flags/bv.png similarity index 100% rename from deluge/data/pixmaps/flags/bv.png rename to deluge/ui/data/pixmaps/flags/bv.png diff --git a/deluge/data/pixmaps/flags/bw.png b/deluge/ui/data/pixmaps/flags/bw.png similarity index 100% rename from deluge/data/pixmaps/flags/bw.png rename to deluge/ui/data/pixmaps/flags/bw.png diff --git a/deluge/data/pixmaps/flags/by.png b/deluge/ui/data/pixmaps/flags/by.png similarity index 100% rename from deluge/data/pixmaps/flags/by.png rename to deluge/ui/data/pixmaps/flags/by.png diff --git a/deluge/data/pixmaps/flags/bz.png b/deluge/ui/data/pixmaps/flags/bz.png similarity index 100% rename from deluge/data/pixmaps/flags/bz.png rename to deluge/ui/data/pixmaps/flags/bz.png diff --git a/deluge/data/pixmaps/flags/ca.png b/deluge/ui/data/pixmaps/flags/ca.png similarity index 100% rename from deluge/data/pixmaps/flags/ca.png rename to deluge/ui/data/pixmaps/flags/ca.png diff --git a/deluge/data/pixmaps/flags/cc.png b/deluge/ui/data/pixmaps/flags/cc.png similarity index 100% rename from deluge/data/pixmaps/flags/cc.png rename to deluge/ui/data/pixmaps/flags/cc.png diff --git a/deluge/data/pixmaps/flags/cd.png b/deluge/ui/data/pixmaps/flags/cd.png similarity index 100% rename from deluge/data/pixmaps/flags/cd.png rename to deluge/ui/data/pixmaps/flags/cd.png diff --git a/deluge/data/pixmaps/flags/cf.png b/deluge/ui/data/pixmaps/flags/cf.png similarity index 100% rename from deluge/data/pixmaps/flags/cf.png rename to deluge/ui/data/pixmaps/flags/cf.png diff --git a/deluge/data/pixmaps/flags/cg.png b/deluge/ui/data/pixmaps/flags/cg.png similarity index 100% rename from deluge/data/pixmaps/flags/cg.png rename to deluge/ui/data/pixmaps/flags/cg.png diff --git a/deluge/data/pixmaps/flags/ch.png b/deluge/ui/data/pixmaps/flags/ch.png similarity index 100% rename from deluge/data/pixmaps/flags/ch.png rename to deluge/ui/data/pixmaps/flags/ch.png diff --git a/deluge/data/pixmaps/flags/ci.png b/deluge/ui/data/pixmaps/flags/ci.png similarity index 100% rename from deluge/data/pixmaps/flags/ci.png rename to deluge/ui/data/pixmaps/flags/ci.png diff --git a/deluge/data/pixmaps/flags/ck.png b/deluge/ui/data/pixmaps/flags/ck.png similarity index 100% rename from deluge/data/pixmaps/flags/ck.png rename to deluge/ui/data/pixmaps/flags/ck.png diff --git a/deluge/data/pixmaps/flags/cl.png b/deluge/ui/data/pixmaps/flags/cl.png similarity index 100% rename from deluge/data/pixmaps/flags/cl.png rename to deluge/ui/data/pixmaps/flags/cl.png diff --git a/deluge/data/pixmaps/flags/cm.png b/deluge/ui/data/pixmaps/flags/cm.png similarity index 100% rename from deluge/data/pixmaps/flags/cm.png rename to deluge/ui/data/pixmaps/flags/cm.png diff --git a/deluge/data/pixmaps/flags/cn.png b/deluge/ui/data/pixmaps/flags/cn.png similarity index 100% rename from deluge/data/pixmaps/flags/cn.png rename to deluge/ui/data/pixmaps/flags/cn.png diff --git a/deluge/data/pixmaps/flags/co.png b/deluge/ui/data/pixmaps/flags/co.png similarity index 100% rename from deluge/data/pixmaps/flags/co.png rename to deluge/ui/data/pixmaps/flags/co.png diff --git a/deluge/data/pixmaps/flags/cr.png b/deluge/ui/data/pixmaps/flags/cr.png similarity index 100% rename from deluge/data/pixmaps/flags/cr.png rename to deluge/ui/data/pixmaps/flags/cr.png diff --git a/deluge/data/pixmaps/flags/cs.png b/deluge/ui/data/pixmaps/flags/cs.png similarity index 100% rename from deluge/data/pixmaps/flags/cs.png rename to deluge/ui/data/pixmaps/flags/cs.png diff --git a/deluge/data/pixmaps/flags/cu.png b/deluge/ui/data/pixmaps/flags/cu.png similarity index 100% rename from deluge/data/pixmaps/flags/cu.png rename to deluge/ui/data/pixmaps/flags/cu.png diff --git a/deluge/data/pixmaps/flags/cv.png b/deluge/ui/data/pixmaps/flags/cv.png similarity index 100% rename from deluge/data/pixmaps/flags/cv.png rename to deluge/ui/data/pixmaps/flags/cv.png diff --git a/deluge/data/pixmaps/flags/cx.png b/deluge/ui/data/pixmaps/flags/cx.png similarity index 100% rename from deluge/data/pixmaps/flags/cx.png rename to deluge/ui/data/pixmaps/flags/cx.png diff --git a/deluge/data/pixmaps/flags/cy.png b/deluge/ui/data/pixmaps/flags/cy.png similarity index 100% rename from deluge/data/pixmaps/flags/cy.png rename to deluge/ui/data/pixmaps/flags/cy.png diff --git a/deluge/data/pixmaps/flags/cz.png b/deluge/ui/data/pixmaps/flags/cz.png similarity index 100% rename from deluge/data/pixmaps/flags/cz.png rename to deluge/ui/data/pixmaps/flags/cz.png diff --git a/deluge/data/pixmaps/flags/de.png b/deluge/ui/data/pixmaps/flags/de.png similarity index 100% rename from deluge/data/pixmaps/flags/de.png rename to deluge/ui/data/pixmaps/flags/de.png diff --git a/deluge/data/pixmaps/flags/dj.png b/deluge/ui/data/pixmaps/flags/dj.png similarity index 100% rename from deluge/data/pixmaps/flags/dj.png rename to deluge/ui/data/pixmaps/flags/dj.png diff --git a/deluge/data/pixmaps/flags/dk.png b/deluge/ui/data/pixmaps/flags/dk.png similarity index 100% rename from deluge/data/pixmaps/flags/dk.png rename to deluge/ui/data/pixmaps/flags/dk.png diff --git a/deluge/data/pixmaps/flags/dm.png b/deluge/ui/data/pixmaps/flags/dm.png similarity index 100% rename from deluge/data/pixmaps/flags/dm.png rename to deluge/ui/data/pixmaps/flags/dm.png diff --git a/deluge/data/pixmaps/flags/do.png b/deluge/ui/data/pixmaps/flags/do.png similarity index 100% rename from deluge/data/pixmaps/flags/do.png rename to deluge/ui/data/pixmaps/flags/do.png diff --git a/deluge/data/pixmaps/flags/dz.png b/deluge/ui/data/pixmaps/flags/dz.png similarity index 100% rename from deluge/data/pixmaps/flags/dz.png rename to deluge/ui/data/pixmaps/flags/dz.png diff --git a/deluge/data/pixmaps/flags/ec.png b/deluge/ui/data/pixmaps/flags/ec.png similarity index 100% rename from deluge/data/pixmaps/flags/ec.png rename to deluge/ui/data/pixmaps/flags/ec.png diff --git a/deluge/data/pixmaps/flags/ee.png b/deluge/ui/data/pixmaps/flags/ee.png similarity index 100% rename from deluge/data/pixmaps/flags/ee.png rename to deluge/ui/data/pixmaps/flags/ee.png diff --git a/deluge/data/pixmaps/flags/eg.png b/deluge/ui/data/pixmaps/flags/eg.png similarity index 100% rename from deluge/data/pixmaps/flags/eg.png rename to deluge/ui/data/pixmaps/flags/eg.png diff --git a/deluge/data/pixmaps/flags/eh.png b/deluge/ui/data/pixmaps/flags/eh.png similarity index 100% rename from deluge/data/pixmaps/flags/eh.png rename to deluge/ui/data/pixmaps/flags/eh.png diff --git a/deluge/data/pixmaps/flags/er.png b/deluge/ui/data/pixmaps/flags/er.png similarity index 100% rename from deluge/data/pixmaps/flags/er.png rename to deluge/ui/data/pixmaps/flags/er.png diff --git a/deluge/data/pixmaps/flags/es.png b/deluge/ui/data/pixmaps/flags/es.png similarity index 100% rename from deluge/data/pixmaps/flags/es.png rename to deluge/ui/data/pixmaps/flags/es.png diff --git a/deluge/data/pixmaps/flags/et.png b/deluge/ui/data/pixmaps/flags/et.png similarity index 100% rename from deluge/data/pixmaps/flags/et.png rename to deluge/ui/data/pixmaps/flags/et.png diff --git a/deluge/data/pixmaps/flags/fi.png b/deluge/ui/data/pixmaps/flags/fi.png similarity index 100% rename from deluge/data/pixmaps/flags/fi.png rename to deluge/ui/data/pixmaps/flags/fi.png diff --git a/deluge/data/pixmaps/flags/fj.png b/deluge/ui/data/pixmaps/flags/fj.png similarity index 100% rename from deluge/data/pixmaps/flags/fj.png rename to deluge/ui/data/pixmaps/flags/fj.png diff --git a/deluge/data/pixmaps/flags/fk.png b/deluge/ui/data/pixmaps/flags/fk.png similarity index 100% rename from deluge/data/pixmaps/flags/fk.png rename to deluge/ui/data/pixmaps/flags/fk.png diff --git a/deluge/data/pixmaps/flags/fm.png b/deluge/ui/data/pixmaps/flags/fm.png similarity index 100% rename from deluge/data/pixmaps/flags/fm.png rename to deluge/ui/data/pixmaps/flags/fm.png diff --git a/deluge/data/pixmaps/flags/fo.png b/deluge/ui/data/pixmaps/flags/fo.png similarity index 100% rename from deluge/data/pixmaps/flags/fo.png rename to deluge/ui/data/pixmaps/flags/fo.png diff --git a/deluge/data/pixmaps/flags/fr.png b/deluge/ui/data/pixmaps/flags/fr.png similarity index 100% rename from deluge/data/pixmaps/flags/fr.png rename to deluge/ui/data/pixmaps/flags/fr.png diff --git a/deluge/data/pixmaps/flags/fx.png b/deluge/ui/data/pixmaps/flags/fx.png similarity index 100% rename from deluge/data/pixmaps/flags/fx.png rename to deluge/ui/data/pixmaps/flags/fx.png diff --git a/deluge/data/pixmaps/flags/ga.png b/deluge/ui/data/pixmaps/flags/ga.png similarity index 100% rename from deluge/data/pixmaps/flags/ga.png rename to deluge/ui/data/pixmaps/flags/ga.png diff --git a/deluge/data/pixmaps/flags/gb.png b/deluge/ui/data/pixmaps/flags/gb.png similarity index 100% rename from deluge/data/pixmaps/flags/gb.png rename to deluge/ui/data/pixmaps/flags/gb.png diff --git a/deluge/data/pixmaps/flags/gd.png b/deluge/ui/data/pixmaps/flags/gd.png similarity index 100% rename from deluge/data/pixmaps/flags/gd.png rename to deluge/ui/data/pixmaps/flags/gd.png diff --git a/deluge/data/pixmaps/flags/ge.png b/deluge/ui/data/pixmaps/flags/ge.png similarity index 100% rename from deluge/data/pixmaps/flags/ge.png rename to deluge/ui/data/pixmaps/flags/ge.png diff --git a/deluge/data/pixmaps/flags/gf.png b/deluge/ui/data/pixmaps/flags/gf.png similarity index 100% rename from deluge/data/pixmaps/flags/gf.png rename to deluge/ui/data/pixmaps/flags/gf.png diff --git a/deluge/data/pixmaps/flags/gg.png b/deluge/ui/data/pixmaps/flags/gg.png similarity index 100% rename from deluge/data/pixmaps/flags/gg.png rename to deluge/ui/data/pixmaps/flags/gg.png diff --git a/deluge/data/pixmaps/flags/gh.png b/deluge/ui/data/pixmaps/flags/gh.png similarity index 100% rename from deluge/data/pixmaps/flags/gh.png rename to deluge/ui/data/pixmaps/flags/gh.png diff --git a/deluge/data/pixmaps/flags/gi.png b/deluge/ui/data/pixmaps/flags/gi.png similarity index 100% rename from deluge/data/pixmaps/flags/gi.png rename to deluge/ui/data/pixmaps/flags/gi.png diff --git a/deluge/data/pixmaps/flags/gl.png b/deluge/ui/data/pixmaps/flags/gl.png similarity index 100% rename from deluge/data/pixmaps/flags/gl.png rename to deluge/ui/data/pixmaps/flags/gl.png diff --git a/deluge/data/pixmaps/flags/gm.png b/deluge/ui/data/pixmaps/flags/gm.png similarity index 100% rename from deluge/data/pixmaps/flags/gm.png rename to deluge/ui/data/pixmaps/flags/gm.png diff --git a/deluge/data/pixmaps/flags/gn.png b/deluge/ui/data/pixmaps/flags/gn.png similarity index 100% rename from deluge/data/pixmaps/flags/gn.png rename to deluge/ui/data/pixmaps/flags/gn.png diff --git a/deluge/data/pixmaps/flags/gp.png b/deluge/ui/data/pixmaps/flags/gp.png similarity index 100% rename from deluge/data/pixmaps/flags/gp.png rename to deluge/ui/data/pixmaps/flags/gp.png diff --git a/deluge/data/pixmaps/flags/gq.png b/deluge/ui/data/pixmaps/flags/gq.png similarity index 100% rename from deluge/data/pixmaps/flags/gq.png rename to deluge/ui/data/pixmaps/flags/gq.png diff --git a/deluge/data/pixmaps/flags/gr.png b/deluge/ui/data/pixmaps/flags/gr.png similarity index 100% rename from deluge/data/pixmaps/flags/gr.png rename to deluge/ui/data/pixmaps/flags/gr.png diff --git a/deluge/data/pixmaps/flags/gs.png b/deluge/ui/data/pixmaps/flags/gs.png similarity index 100% rename from deluge/data/pixmaps/flags/gs.png rename to deluge/ui/data/pixmaps/flags/gs.png diff --git a/deluge/data/pixmaps/flags/gt.png b/deluge/ui/data/pixmaps/flags/gt.png similarity index 100% rename from deluge/data/pixmaps/flags/gt.png rename to deluge/ui/data/pixmaps/flags/gt.png diff --git a/deluge/data/pixmaps/flags/gu.png b/deluge/ui/data/pixmaps/flags/gu.png similarity index 100% rename from deluge/data/pixmaps/flags/gu.png rename to deluge/ui/data/pixmaps/flags/gu.png diff --git a/deluge/data/pixmaps/flags/gw.png b/deluge/ui/data/pixmaps/flags/gw.png similarity index 100% rename from deluge/data/pixmaps/flags/gw.png rename to deluge/ui/data/pixmaps/flags/gw.png diff --git a/deluge/data/pixmaps/flags/gy.png b/deluge/ui/data/pixmaps/flags/gy.png similarity index 100% rename from deluge/data/pixmaps/flags/gy.png rename to deluge/ui/data/pixmaps/flags/gy.png diff --git a/deluge/data/pixmaps/flags/hk.png b/deluge/ui/data/pixmaps/flags/hk.png similarity index 100% rename from deluge/data/pixmaps/flags/hk.png rename to deluge/ui/data/pixmaps/flags/hk.png diff --git a/deluge/data/pixmaps/flags/hm.png b/deluge/ui/data/pixmaps/flags/hm.png similarity index 100% rename from deluge/data/pixmaps/flags/hm.png rename to deluge/ui/data/pixmaps/flags/hm.png diff --git a/deluge/data/pixmaps/flags/hn.png b/deluge/ui/data/pixmaps/flags/hn.png similarity index 100% rename from deluge/data/pixmaps/flags/hn.png rename to deluge/ui/data/pixmaps/flags/hn.png diff --git a/deluge/data/pixmaps/flags/hr.png b/deluge/ui/data/pixmaps/flags/hr.png similarity index 100% rename from deluge/data/pixmaps/flags/hr.png rename to deluge/ui/data/pixmaps/flags/hr.png diff --git a/deluge/data/pixmaps/flags/ht.png b/deluge/ui/data/pixmaps/flags/ht.png similarity index 100% rename from deluge/data/pixmaps/flags/ht.png rename to deluge/ui/data/pixmaps/flags/ht.png diff --git a/deluge/data/pixmaps/flags/hu.png b/deluge/ui/data/pixmaps/flags/hu.png similarity index 100% rename from deluge/data/pixmaps/flags/hu.png rename to deluge/ui/data/pixmaps/flags/hu.png diff --git a/deluge/data/pixmaps/flags/id.png b/deluge/ui/data/pixmaps/flags/id.png similarity index 100% rename from deluge/data/pixmaps/flags/id.png rename to deluge/ui/data/pixmaps/flags/id.png diff --git a/deluge/data/pixmaps/flags/ie.png b/deluge/ui/data/pixmaps/flags/ie.png similarity index 100% rename from deluge/data/pixmaps/flags/ie.png rename to deluge/ui/data/pixmaps/flags/ie.png diff --git a/deluge/data/pixmaps/flags/il.png b/deluge/ui/data/pixmaps/flags/il.png similarity index 100% rename from deluge/data/pixmaps/flags/il.png rename to deluge/ui/data/pixmaps/flags/il.png diff --git a/deluge/data/pixmaps/flags/in.png b/deluge/ui/data/pixmaps/flags/in.png similarity index 100% rename from deluge/data/pixmaps/flags/in.png rename to deluge/ui/data/pixmaps/flags/in.png diff --git a/deluge/data/pixmaps/flags/io.png b/deluge/ui/data/pixmaps/flags/io.png similarity index 100% rename from deluge/data/pixmaps/flags/io.png rename to deluge/ui/data/pixmaps/flags/io.png diff --git a/deluge/data/pixmaps/flags/iq.png b/deluge/ui/data/pixmaps/flags/iq.png similarity index 100% rename from deluge/data/pixmaps/flags/iq.png rename to deluge/ui/data/pixmaps/flags/iq.png diff --git a/deluge/data/pixmaps/flags/ir.png b/deluge/ui/data/pixmaps/flags/ir.png similarity index 100% rename from deluge/data/pixmaps/flags/ir.png rename to deluge/ui/data/pixmaps/flags/ir.png diff --git a/deluge/data/pixmaps/flags/is.png b/deluge/ui/data/pixmaps/flags/is.png similarity index 100% rename from deluge/data/pixmaps/flags/is.png rename to deluge/ui/data/pixmaps/flags/is.png diff --git a/deluge/data/pixmaps/flags/it.png b/deluge/ui/data/pixmaps/flags/it.png similarity index 100% rename from deluge/data/pixmaps/flags/it.png rename to deluge/ui/data/pixmaps/flags/it.png diff --git a/deluge/data/pixmaps/flags/je.png b/deluge/ui/data/pixmaps/flags/je.png similarity index 100% rename from deluge/data/pixmaps/flags/je.png rename to deluge/ui/data/pixmaps/flags/je.png diff --git a/deluge/data/pixmaps/flags/jm.png b/deluge/ui/data/pixmaps/flags/jm.png similarity index 100% rename from deluge/data/pixmaps/flags/jm.png rename to deluge/ui/data/pixmaps/flags/jm.png diff --git a/deluge/data/pixmaps/flags/jo.png b/deluge/ui/data/pixmaps/flags/jo.png similarity index 100% rename from deluge/data/pixmaps/flags/jo.png rename to deluge/ui/data/pixmaps/flags/jo.png diff --git a/deluge/data/pixmaps/flags/jp.png b/deluge/ui/data/pixmaps/flags/jp.png similarity index 100% rename from deluge/data/pixmaps/flags/jp.png rename to deluge/ui/data/pixmaps/flags/jp.png diff --git a/deluge/data/pixmaps/flags/ke.png b/deluge/ui/data/pixmaps/flags/ke.png similarity index 100% rename from deluge/data/pixmaps/flags/ke.png rename to deluge/ui/data/pixmaps/flags/ke.png diff --git a/deluge/data/pixmaps/flags/kg.png b/deluge/ui/data/pixmaps/flags/kg.png similarity index 100% rename from deluge/data/pixmaps/flags/kg.png rename to deluge/ui/data/pixmaps/flags/kg.png diff --git a/deluge/data/pixmaps/flags/kh.png b/deluge/ui/data/pixmaps/flags/kh.png similarity index 100% rename from deluge/data/pixmaps/flags/kh.png rename to deluge/ui/data/pixmaps/flags/kh.png diff --git a/deluge/data/pixmaps/flags/ki.png b/deluge/ui/data/pixmaps/flags/ki.png similarity index 100% rename from deluge/data/pixmaps/flags/ki.png rename to deluge/ui/data/pixmaps/flags/ki.png diff --git a/deluge/data/pixmaps/flags/km.png b/deluge/ui/data/pixmaps/flags/km.png similarity index 100% rename from deluge/data/pixmaps/flags/km.png rename to deluge/ui/data/pixmaps/flags/km.png diff --git a/deluge/data/pixmaps/flags/kn.png b/deluge/ui/data/pixmaps/flags/kn.png similarity index 100% rename from deluge/data/pixmaps/flags/kn.png rename to deluge/ui/data/pixmaps/flags/kn.png diff --git a/deluge/data/pixmaps/flags/kp.png b/deluge/ui/data/pixmaps/flags/kp.png similarity index 100% rename from deluge/data/pixmaps/flags/kp.png rename to deluge/ui/data/pixmaps/flags/kp.png diff --git a/deluge/data/pixmaps/flags/kr.png b/deluge/ui/data/pixmaps/flags/kr.png similarity index 100% rename from deluge/data/pixmaps/flags/kr.png rename to deluge/ui/data/pixmaps/flags/kr.png diff --git a/deluge/data/pixmaps/flags/kw.png b/deluge/ui/data/pixmaps/flags/kw.png similarity index 100% rename from deluge/data/pixmaps/flags/kw.png rename to deluge/ui/data/pixmaps/flags/kw.png diff --git a/deluge/data/pixmaps/flags/ky.png b/deluge/ui/data/pixmaps/flags/ky.png similarity index 100% rename from deluge/data/pixmaps/flags/ky.png rename to deluge/ui/data/pixmaps/flags/ky.png diff --git a/deluge/data/pixmaps/flags/kz.png b/deluge/ui/data/pixmaps/flags/kz.png similarity index 100% rename from deluge/data/pixmaps/flags/kz.png rename to deluge/ui/data/pixmaps/flags/kz.png diff --git a/deluge/data/pixmaps/flags/la.png b/deluge/ui/data/pixmaps/flags/la.png similarity index 100% rename from deluge/data/pixmaps/flags/la.png rename to deluge/ui/data/pixmaps/flags/la.png diff --git a/deluge/data/pixmaps/flags/lb.png b/deluge/ui/data/pixmaps/flags/lb.png similarity index 100% rename from deluge/data/pixmaps/flags/lb.png rename to deluge/ui/data/pixmaps/flags/lb.png diff --git a/deluge/data/pixmaps/flags/lc.png b/deluge/ui/data/pixmaps/flags/lc.png similarity index 100% rename from deluge/data/pixmaps/flags/lc.png rename to deluge/ui/data/pixmaps/flags/lc.png diff --git a/deluge/data/pixmaps/flags/li.png b/deluge/ui/data/pixmaps/flags/li.png similarity index 100% rename from deluge/data/pixmaps/flags/li.png rename to deluge/ui/data/pixmaps/flags/li.png diff --git a/deluge/data/pixmaps/flags/lk.png b/deluge/ui/data/pixmaps/flags/lk.png similarity index 100% rename from deluge/data/pixmaps/flags/lk.png rename to deluge/ui/data/pixmaps/flags/lk.png diff --git a/deluge/data/pixmaps/flags/lr.png b/deluge/ui/data/pixmaps/flags/lr.png similarity index 100% rename from deluge/data/pixmaps/flags/lr.png rename to deluge/ui/data/pixmaps/flags/lr.png diff --git a/deluge/data/pixmaps/flags/ls.png b/deluge/ui/data/pixmaps/flags/ls.png similarity index 100% rename from deluge/data/pixmaps/flags/ls.png rename to deluge/ui/data/pixmaps/flags/ls.png diff --git a/deluge/data/pixmaps/flags/lt.png b/deluge/ui/data/pixmaps/flags/lt.png similarity index 100% rename from deluge/data/pixmaps/flags/lt.png rename to deluge/ui/data/pixmaps/flags/lt.png diff --git a/deluge/data/pixmaps/flags/lu.png b/deluge/ui/data/pixmaps/flags/lu.png similarity index 100% rename from deluge/data/pixmaps/flags/lu.png rename to deluge/ui/data/pixmaps/flags/lu.png diff --git a/deluge/data/pixmaps/flags/lv.png b/deluge/ui/data/pixmaps/flags/lv.png similarity index 100% rename from deluge/data/pixmaps/flags/lv.png rename to deluge/ui/data/pixmaps/flags/lv.png diff --git a/deluge/data/pixmaps/flags/ly.png b/deluge/ui/data/pixmaps/flags/ly.png similarity index 100% rename from deluge/data/pixmaps/flags/ly.png rename to deluge/ui/data/pixmaps/flags/ly.png diff --git a/deluge/data/pixmaps/flags/ma.png b/deluge/ui/data/pixmaps/flags/ma.png similarity index 100% rename from deluge/data/pixmaps/flags/ma.png rename to deluge/ui/data/pixmaps/flags/ma.png diff --git a/deluge/data/pixmaps/flags/mc.png b/deluge/ui/data/pixmaps/flags/mc.png similarity index 100% rename from deluge/data/pixmaps/flags/mc.png rename to deluge/ui/data/pixmaps/flags/mc.png diff --git a/deluge/data/pixmaps/flags/md.png b/deluge/ui/data/pixmaps/flags/md.png similarity index 100% rename from deluge/data/pixmaps/flags/md.png rename to deluge/ui/data/pixmaps/flags/md.png diff --git a/deluge/data/pixmaps/flags/me.png b/deluge/ui/data/pixmaps/flags/me.png similarity index 100% rename from deluge/data/pixmaps/flags/me.png rename to deluge/ui/data/pixmaps/flags/me.png diff --git a/deluge/data/pixmaps/flags/mg.png b/deluge/ui/data/pixmaps/flags/mg.png similarity index 100% rename from deluge/data/pixmaps/flags/mg.png rename to deluge/ui/data/pixmaps/flags/mg.png diff --git a/deluge/data/pixmaps/flags/mh.png b/deluge/ui/data/pixmaps/flags/mh.png similarity index 100% rename from deluge/data/pixmaps/flags/mh.png rename to deluge/ui/data/pixmaps/flags/mh.png diff --git a/deluge/data/pixmaps/flags/mk.png b/deluge/ui/data/pixmaps/flags/mk.png similarity index 100% rename from deluge/data/pixmaps/flags/mk.png rename to deluge/ui/data/pixmaps/flags/mk.png diff --git a/deluge/data/pixmaps/flags/ml.png b/deluge/ui/data/pixmaps/flags/ml.png similarity index 100% rename from deluge/data/pixmaps/flags/ml.png rename to deluge/ui/data/pixmaps/flags/ml.png diff --git a/deluge/data/pixmaps/flags/mm.png b/deluge/ui/data/pixmaps/flags/mm.png similarity index 100% rename from deluge/data/pixmaps/flags/mm.png rename to deluge/ui/data/pixmaps/flags/mm.png diff --git a/deluge/data/pixmaps/flags/mn.png b/deluge/ui/data/pixmaps/flags/mn.png similarity index 100% rename from deluge/data/pixmaps/flags/mn.png rename to deluge/ui/data/pixmaps/flags/mn.png diff --git a/deluge/data/pixmaps/flags/mo.png b/deluge/ui/data/pixmaps/flags/mo.png similarity index 100% rename from deluge/data/pixmaps/flags/mo.png rename to deluge/ui/data/pixmaps/flags/mo.png diff --git a/deluge/data/pixmaps/flags/mp.png b/deluge/ui/data/pixmaps/flags/mp.png similarity index 100% rename from deluge/data/pixmaps/flags/mp.png rename to deluge/ui/data/pixmaps/flags/mp.png diff --git a/deluge/data/pixmaps/flags/mq.png b/deluge/ui/data/pixmaps/flags/mq.png similarity index 100% rename from deluge/data/pixmaps/flags/mq.png rename to deluge/ui/data/pixmaps/flags/mq.png diff --git a/deluge/data/pixmaps/flags/mr.png b/deluge/ui/data/pixmaps/flags/mr.png similarity index 100% rename from deluge/data/pixmaps/flags/mr.png rename to deluge/ui/data/pixmaps/flags/mr.png diff --git a/deluge/data/pixmaps/flags/ms.png b/deluge/ui/data/pixmaps/flags/ms.png similarity index 100% rename from deluge/data/pixmaps/flags/ms.png rename to deluge/ui/data/pixmaps/flags/ms.png diff --git a/deluge/data/pixmaps/flags/mt.png b/deluge/ui/data/pixmaps/flags/mt.png similarity index 100% rename from deluge/data/pixmaps/flags/mt.png rename to deluge/ui/data/pixmaps/flags/mt.png diff --git a/deluge/data/pixmaps/flags/mu.png b/deluge/ui/data/pixmaps/flags/mu.png similarity index 100% rename from deluge/data/pixmaps/flags/mu.png rename to deluge/ui/data/pixmaps/flags/mu.png diff --git a/deluge/data/pixmaps/flags/mv.png b/deluge/ui/data/pixmaps/flags/mv.png similarity index 100% rename from deluge/data/pixmaps/flags/mv.png rename to deluge/ui/data/pixmaps/flags/mv.png diff --git a/deluge/data/pixmaps/flags/mw.png b/deluge/ui/data/pixmaps/flags/mw.png similarity index 100% rename from deluge/data/pixmaps/flags/mw.png rename to deluge/ui/data/pixmaps/flags/mw.png diff --git a/deluge/data/pixmaps/flags/mx.png b/deluge/ui/data/pixmaps/flags/mx.png similarity index 100% rename from deluge/data/pixmaps/flags/mx.png rename to deluge/ui/data/pixmaps/flags/mx.png diff --git a/deluge/data/pixmaps/flags/my.png b/deluge/ui/data/pixmaps/flags/my.png similarity index 100% rename from deluge/data/pixmaps/flags/my.png rename to deluge/ui/data/pixmaps/flags/my.png diff --git a/deluge/data/pixmaps/flags/mz.png b/deluge/ui/data/pixmaps/flags/mz.png similarity index 100% rename from deluge/data/pixmaps/flags/mz.png rename to deluge/ui/data/pixmaps/flags/mz.png diff --git a/deluge/data/pixmaps/flags/na.png b/deluge/ui/data/pixmaps/flags/na.png similarity index 100% rename from deluge/data/pixmaps/flags/na.png rename to deluge/ui/data/pixmaps/flags/na.png diff --git a/deluge/data/pixmaps/flags/nc.png b/deluge/ui/data/pixmaps/flags/nc.png similarity index 100% rename from deluge/data/pixmaps/flags/nc.png rename to deluge/ui/data/pixmaps/flags/nc.png diff --git a/deluge/data/pixmaps/flags/ne.png b/deluge/ui/data/pixmaps/flags/ne.png similarity index 100% rename from deluge/data/pixmaps/flags/ne.png rename to deluge/ui/data/pixmaps/flags/ne.png diff --git a/deluge/data/pixmaps/flags/nf.png b/deluge/ui/data/pixmaps/flags/nf.png similarity index 100% rename from deluge/data/pixmaps/flags/nf.png rename to deluge/ui/data/pixmaps/flags/nf.png diff --git a/deluge/data/pixmaps/flags/ng.png b/deluge/ui/data/pixmaps/flags/ng.png similarity index 100% rename from deluge/data/pixmaps/flags/ng.png rename to deluge/ui/data/pixmaps/flags/ng.png diff --git a/deluge/data/pixmaps/flags/ni.png b/deluge/ui/data/pixmaps/flags/ni.png similarity index 100% rename from deluge/data/pixmaps/flags/ni.png rename to deluge/ui/data/pixmaps/flags/ni.png diff --git a/deluge/data/pixmaps/flags/nl.png b/deluge/ui/data/pixmaps/flags/nl.png similarity index 100% rename from deluge/data/pixmaps/flags/nl.png rename to deluge/ui/data/pixmaps/flags/nl.png diff --git a/deluge/data/pixmaps/flags/no.png b/deluge/ui/data/pixmaps/flags/no.png similarity index 100% rename from deluge/data/pixmaps/flags/no.png rename to deluge/ui/data/pixmaps/flags/no.png diff --git a/deluge/data/pixmaps/flags/np.png b/deluge/ui/data/pixmaps/flags/np.png similarity index 100% rename from deluge/data/pixmaps/flags/np.png rename to deluge/ui/data/pixmaps/flags/np.png diff --git a/deluge/data/pixmaps/flags/nr.png b/deluge/ui/data/pixmaps/flags/nr.png similarity index 100% rename from deluge/data/pixmaps/flags/nr.png rename to deluge/ui/data/pixmaps/flags/nr.png diff --git a/deluge/data/pixmaps/flags/nu.png b/deluge/ui/data/pixmaps/flags/nu.png similarity index 100% rename from deluge/data/pixmaps/flags/nu.png rename to deluge/ui/data/pixmaps/flags/nu.png diff --git a/deluge/data/pixmaps/flags/nz.png b/deluge/ui/data/pixmaps/flags/nz.png similarity index 100% rename from deluge/data/pixmaps/flags/nz.png rename to deluge/ui/data/pixmaps/flags/nz.png diff --git a/deluge/data/pixmaps/flags/om.png b/deluge/ui/data/pixmaps/flags/om.png similarity index 100% rename from deluge/data/pixmaps/flags/om.png rename to deluge/ui/data/pixmaps/flags/om.png diff --git a/deluge/data/pixmaps/flags/pa.png b/deluge/ui/data/pixmaps/flags/pa.png similarity index 100% rename from deluge/data/pixmaps/flags/pa.png rename to deluge/ui/data/pixmaps/flags/pa.png diff --git a/deluge/data/pixmaps/flags/pe.png b/deluge/ui/data/pixmaps/flags/pe.png similarity index 100% rename from deluge/data/pixmaps/flags/pe.png rename to deluge/ui/data/pixmaps/flags/pe.png diff --git a/deluge/data/pixmaps/flags/pf.png b/deluge/ui/data/pixmaps/flags/pf.png similarity index 100% rename from deluge/data/pixmaps/flags/pf.png rename to deluge/ui/data/pixmaps/flags/pf.png diff --git a/deluge/data/pixmaps/flags/pg.png b/deluge/ui/data/pixmaps/flags/pg.png similarity index 100% rename from deluge/data/pixmaps/flags/pg.png rename to deluge/ui/data/pixmaps/flags/pg.png diff --git a/deluge/data/pixmaps/flags/ph.png b/deluge/ui/data/pixmaps/flags/ph.png similarity index 100% rename from deluge/data/pixmaps/flags/ph.png rename to deluge/ui/data/pixmaps/flags/ph.png diff --git a/deluge/data/pixmaps/flags/pk.png b/deluge/ui/data/pixmaps/flags/pk.png similarity index 100% rename from deluge/data/pixmaps/flags/pk.png rename to deluge/ui/data/pixmaps/flags/pk.png diff --git a/deluge/data/pixmaps/flags/pl.png b/deluge/ui/data/pixmaps/flags/pl.png similarity index 100% rename from deluge/data/pixmaps/flags/pl.png rename to deluge/ui/data/pixmaps/flags/pl.png diff --git a/deluge/data/pixmaps/flags/pm.png b/deluge/ui/data/pixmaps/flags/pm.png similarity index 100% rename from deluge/data/pixmaps/flags/pm.png rename to deluge/ui/data/pixmaps/flags/pm.png diff --git a/deluge/data/pixmaps/flags/pn.png b/deluge/ui/data/pixmaps/flags/pn.png similarity index 100% rename from deluge/data/pixmaps/flags/pn.png rename to deluge/ui/data/pixmaps/flags/pn.png diff --git a/deluge/data/pixmaps/flags/pr.png b/deluge/ui/data/pixmaps/flags/pr.png similarity index 100% rename from deluge/data/pixmaps/flags/pr.png rename to deluge/ui/data/pixmaps/flags/pr.png diff --git a/deluge/data/pixmaps/flags/ps.png b/deluge/ui/data/pixmaps/flags/ps.png similarity index 100% rename from deluge/data/pixmaps/flags/ps.png rename to deluge/ui/data/pixmaps/flags/ps.png diff --git a/deluge/data/pixmaps/flags/pt.png b/deluge/ui/data/pixmaps/flags/pt.png similarity index 100% rename from deluge/data/pixmaps/flags/pt.png rename to deluge/ui/data/pixmaps/flags/pt.png diff --git a/deluge/data/pixmaps/flags/pw.png b/deluge/ui/data/pixmaps/flags/pw.png similarity index 100% rename from deluge/data/pixmaps/flags/pw.png rename to deluge/ui/data/pixmaps/flags/pw.png diff --git a/deluge/data/pixmaps/flags/py.png b/deluge/ui/data/pixmaps/flags/py.png similarity index 100% rename from deluge/data/pixmaps/flags/py.png rename to deluge/ui/data/pixmaps/flags/py.png diff --git a/deluge/data/pixmaps/flags/qa.png b/deluge/ui/data/pixmaps/flags/qa.png similarity index 100% rename from deluge/data/pixmaps/flags/qa.png rename to deluge/ui/data/pixmaps/flags/qa.png diff --git a/deluge/data/pixmaps/flags/re.png b/deluge/ui/data/pixmaps/flags/re.png similarity index 100% rename from deluge/data/pixmaps/flags/re.png rename to deluge/ui/data/pixmaps/flags/re.png diff --git a/deluge/data/pixmaps/flags/ro.png b/deluge/ui/data/pixmaps/flags/ro.png similarity index 100% rename from deluge/data/pixmaps/flags/ro.png rename to deluge/ui/data/pixmaps/flags/ro.png diff --git a/deluge/data/pixmaps/flags/rs.png b/deluge/ui/data/pixmaps/flags/rs.png similarity index 100% rename from deluge/data/pixmaps/flags/rs.png rename to deluge/ui/data/pixmaps/flags/rs.png diff --git a/deluge/data/pixmaps/flags/ru.png b/deluge/ui/data/pixmaps/flags/ru.png similarity index 100% rename from deluge/data/pixmaps/flags/ru.png rename to deluge/ui/data/pixmaps/flags/ru.png diff --git a/deluge/data/pixmaps/flags/rw.png b/deluge/ui/data/pixmaps/flags/rw.png similarity index 100% rename from deluge/data/pixmaps/flags/rw.png rename to deluge/ui/data/pixmaps/flags/rw.png diff --git a/deluge/data/pixmaps/flags/sa.png b/deluge/ui/data/pixmaps/flags/sa.png similarity index 100% rename from deluge/data/pixmaps/flags/sa.png rename to deluge/ui/data/pixmaps/flags/sa.png diff --git a/deluge/data/pixmaps/flags/sb.png b/deluge/ui/data/pixmaps/flags/sb.png similarity index 100% rename from deluge/data/pixmaps/flags/sb.png rename to deluge/ui/data/pixmaps/flags/sb.png diff --git a/deluge/data/pixmaps/flags/sc.png b/deluge/ui/data/pixmaps/flags/sc.png similarity index 100% rename from deluge/data/pixmaps/flags/sc.png rename to deluge/ui/data/pixmaps/flags/sc.png diff --git a/deluge/data/pixmaps/flags/sd.png b/deluge/ui/data/pixmaps/flags/sd.png similarity index 100% rename from deluge/data/pixmaps/flags/sd.png rename to deluge/ui/data/pixmaps/flags/sd.png diff --git a/deluge/data/pixmaps/flags/se.png b/deluge/ui/data/pixmaps/flags/se.png similarity index 100% rename from deluge/data/pixmaps/flags/se.png rename to deluge/ui/data/pixmaps/flags/se.png diff --git a/deluge/data/pixmaps/flags/sg.png b/deluge/ui/data/pixmaps/flags/sg.png similarity index 100% rename from deluge/data/pixmaps/flags/sg.png rename to deluge/ui/data/pixmaps/flags/sg.png diff --git a/deluge/data/pixmaps/flags/sh.png b/deluge/ui/data/pixmaps/flags/sh.png similarity index 100% rename from deluge/data/pixmaps/flags/sh.png rename to deluge/ui/data/pixmaps/flags/sh.png diff --git a/deluge/data/pixmaps/flags/si.png b/deluge/ui/data/pixmaps/flags/si.png similarity index 100% rename from deluge/data/pixmaps/flags/si.png rename to deluge/ui/data/pixmaps/flags/si.png diff --git a/deluge/data/pixmaps/flags/sj.png b/deluge/ui/data/pixmaps/flags/sj.png similarity index 100% rename from deluge/data/pixmaps/flags/sj.png rename to deluge/ui/data/pixmaps/flags/sj.png diff --git a/deluge/data/pixmaps/flags/sk.png b/deluge/ui/data/pixmaps/flags/sk.png similarity index 100% rename from deluge/data/pixmaps/flags/sk.png rename to deluge/ui/data/pixmaps/flags/sk.png diff --git a/deluge/data/pixmaps/flags/sl.png b/deluge/ui/data/pixmaps/flags/sl.png similarity index 100% rename from deluge/data/pixmaps/flags/sl.png rename to deluge/ui/data/pixmaps/flags/sl.png diff --git a/deluge/data/pixmaps/flags/sm.png b/deluge/ui/data/pixmaps/flags/sm.png similarity index 100% rename from deluge/data/pixmaps/flags/sm.png rename to deluge/ui/data/pixmaps/flags/sm.png diff --git a/deluge/data/pixmaps/flags/sn.png b/deluge/ui/data/pixmaps/flags/sn.png similarity index 100% rename from deluge/data/pixmaps/flags/sn.png rename to deluge/ui/data/pixmaps/flags/sn.png diff --git a/deluge/data/pixmaps/flags/so.png b/deluge/ui/data/pixmaps/flags/so.png similarity index 100% rename from deluge/data/pixmaps/flags/so.png rename to deluge/ui/data/pixmaps/flags/so.png diff --git a/deluge/data/pixmaps/flags/sr.png b/deluge/ui/data/pixmaps/flags/sr.png similarity index 100% rename from deluge/data/pixmaps/flags/sr.png rename to deluge/ui/data/pixmaps/flags/sr.png diff --git a/deluge/data/pixmaps/flags/st.png b/deluge/ui/data/pixmaps/flags/st.png similarity index 100% rename from deluge/data/pixmaps/flags/st.png rename to deluge/ui/data/pixmaps/flags/st.png diff --git a/deluge/data/pixmaps/flags/sv.png b/deluge/ui/data/pixmaps/flags/sv.png similarity index 100% rename from deluge/data/pixmaps/flags/sv.png rename to deluge/ui/data/pixmaps/flags/sv.png diff --git a/deluge/data/pixmaps/flags/sy.png b/deluge/ui/data/pixmaps/flags/sy.png similarity index 100% rename from deluge/data/pixmaps/flags/sy.png rename to deluge/ui/data/pixmaps/flags/sy.png diff --git a/deluge/data/pixmaps/flags/sz.png b/deluge/ui/data/pixmaps/flags/sz.png similarity index 100% rename from deluge/data/pixmaps/flags/sz.png rename to deluge/ui/data/pixmaps/flags/sz.png diff --git a/deluge/data/pixmaps/flags/tc.png b/deluge/ui/data/pixmaps/flags/tc.png similarity index 100% rename from deluge/data/pixmaps/flags/tc.png rename to deluge/ui/data/pixmaps/flags/tc.png diff --git a/deluge/data/pixmaps/flags/td.png b/deluge/ui/data/pixmaps/flags/td.png similarity index 100% rename from deluge/data/pixmaps/flags/td.png rename to deluge/ui/data/pixmaps/flags/td.png diff --git a/deluge/data/pixmaps/flags/tf.png b/deluge/ui/data/pixmaps/flags/tf.png similarity index 100% rename from deluge/data/pixmaps/flags/tf.png rename to deluge/ui/data/pixmaps/flags/tf.png diff --git a/deluge/data/pixmaps/flags/tg.png b/deluge/ui/data/pixmaps/flags/tg.png similarity index 100% rename from deluge/data/pixmaps/flags/tg.png rename to deluge/ui/data/pixmaps/flags/tg.png diff --git a/deluge/data/pixmaps/flags/th.png b/deluge/ui/data/pixmaps/flags/th.png similarity index 100% rename from deluge/data/pixmaps/flags/th.png rename to deluge/ui/data/pixmaps/flags/th.png diff --git a/deluge/data/pixmaps/flags/tj.png b/deluge/ui/data/pixmaps/flags/tj.png similarity index 100% rename from deluge/data/pixmaps/flags/tj.png rename to deluge/ui/data/pixmaps/flags/tj.png diff --git a/deluge/data/pixmaps/flags/tk.png b/deluge/ui/data/pixmaps/flags/tk.png similarity index 100% rename from deluge/data/pixmaps/flags/tk.png rename to deluge/ui/data/pixmaps/flags/tk.png diff --git a/deluge/data/pixmaps/flags/tl.png b/deluge/ui/data/pixmaps/flags/tl.png similarity index 100% rename from deluge/data/pixmaps/flags/tl.png rename to deluge/ui/data/pixmaps/flags/tl.png diff --git a/deluge/data/pixmaps/flags/tm.png b/deluge/ui/data/pixmaps/flags/tm.png similarity index 100% rename from deluge/data/pixmaps/flags/tm.png rename to deluge/ui/data/pixmaps/flags/tm.png diff --git a/deluge/data/pixmaps/flags/tn.png b/deluge/ui/data/pixmaps/flags/tn.png similarity index 100% rename from deluge/data/pixmaps/flags/tn.png rename to deluge/ui/data/pixmaps/flags/tn.png diff --git a/deluge/data/pixmaps/flags/to.png b/deluge/ui/data/pixmaps/flags/to.png similarity index 100% rename from deluge/data/pixmaps/flags/to.png rename to deluge/ui/data/pixmaps/flags/to.png diff --git a/deluge/data/pixmaps/flags/tp.png b/deluge/ui/data/pixmaps/flags/tp.png similarity index 100% rename from deluge/data/pixmaps/flags/tp.png rename to deluge/ui/data/pixmaps/flags/tp.png diff --git a/deluge/data/pixmaps/flags/tr.png b/deluge/ui/data/pixmaps/flags/tr.png similarity index 100% rename from deluge/data/pixmaps/flags/tr.png rename to deluge/ui/data/pixmaps/flags/tr.png diff --git a/deluge/data/pixmaps/flags/tt.png b/deluge/ui/data/pixmaps/flags/tt.png similarity index 100% rename from deluge/data/pixmaps/flags/tt.png rename to deluge/ui/data/pixmaps/flags/tt.png diff --git a/deluge/data/pixmaps/flags/tv.png b/deluge/ui/data/pixmaps/flags/tv.png similarity index 100% rename from deluge/data/pixmaps/flags/tv.png rename to deluge/ui/data/pixmaps/flags/tv.png diff --git a/deluge/data/pixmaps/flags/tw.png b/deluge/ui/data/pixmaps/flags/tw.png similarity index 100% rename from deluge/data/pixmaps/flags/tw.png rename to deluge/ui/data/pixmaps/flags/tw.png diff --git a/deluge/data/pixmaps/flags/tz.png b/deluge/ui/data/pixmaps/flags/tz.png similarity index 100% rename from deluge/data/pixmaps/flags/tz.png rename to deluge/ui/data/pixmaps/flags/tz.png diff --git a/deluge/data/pixmaps/flags/ua.png b/deluge/ui/data/pixmaps/flags/ua.png similarity index 100% rename from deluge/data/pixmaps/flags/ua.png rename to deluge/ui/data/pixmaps/flags/ua.png diff --git a/deluge/data/pixmaps/flags/ug.png b/deluge/ui/data/pixmaps/flags/ug.png similarity index 100% rename from deluge/data/pixmaps/flags/ug.png rename to deluge/ui/data/pixmaps/flags/ug.png diff --git a/deluge/data/pixmaps/flags/um.png b/deluge/ui/data/pixmaps/flags/um.png similarity index 100% rename from deluge/data/pixmaps/flags/um.png rename to deluge/ui/data/pixmaps/flags/um.png diff --git a/deluge/data/pixmaps/flags/us.png b/deluge/ui/data/pixmaps/flags/us.png similarity index 100% rename from deluge/data/pixmaps/flags/us.png rename to deluge/ui/data/pixmaps/flags/us.png diff --git a/deluge/data/pixmaps/flags/uy.png b/deluge/ui/data/pixmaps/flags/uy.png similarity index 100% rename from deluge/data/pixmaps/flags/uy.png rename to deluge/ui/data/pixmaps/flags/uy.png diff --git a/deluge/data/pixmaps/flags/uz.png b/deluge/ui/data/pixmaps/flags/uz.png similarity index 100% rename from deluge/data/pixmaps/flags/uz.png rename to deluge/ui/data/pixmaps/flags/uz.png diff --git a/deluge/data/pixmaps/flags/va.png b/deluge/ui/data/pixmaps/flags/va.png similarity index 100% rename from deluge/data/pixmaps/flags/va.png rename to deluge/ui/data/pixmaps/flags/va.png diff --git a/deluge/data/pixmaps/flags/vc.png b/deluge/ui/data/pixmaps/flags/vc.png similarity index 100% rename from deluge/data/pixmaps/flags/vc.png rename to deluge/ui/data/pixmaps/flags/vc.png diff --git a/deluge/data/pixmaps/flags/ve.png b/deluge/ui/data/pixmaps/flags/ve.png similarity index 100% rename from deluge/data/pixmaps/flags/ve.png rename to deluge/ui/data/pixmaps/flags/ve.png diff --git a/deluge/data/pixmaps/flags/vg.png b/deluge/ui/data/pixmaps/flags/vg.png similarity index 100% rename from deluge/data/pixmaps/flags/vg.png rename to deluge/ui/data/pixmaps/flags/vg.png diff --git a/deluge/data/pixmaps/flags/vi.png b/deluge/ui/data/pixmaps/flags/vi.png similarity index 100% rename from deluge/data/pixmaps/flags/vi.png rename to deluge/ui/data/pixmaps/flags/vi.png diff --git a/deluge/data/pixmaps/flags/vn.png b/deluge/ui/data/pixmaps/flags/vn.png similarity index 100% rename from deluge/data/pixmaps/flags/vn.png rename to deluge/ui/data/pixmaps/flags/vn.png diff --git a/deluge/data/pixmaps/flags/vu.png b/deluge/ui/data/pixmaps/flags/vu.png similarity index 100% rename from deluge/data/pixmaps/flags/vu.png rename to deluge/ui/data/pixmaps/flags/vu.png diff --git a/deluge/data/pixmaps/flags/wf.png b/deluge/ui/data/pixmaps/flags/wf.png similarity index 100% rename from deluge/data/pixmaps/flags/wf.png rename to deluge/ui/data/pixmaps/flags/wf.png diff --git a/deluge/data/pixmaps/flags/ws.png b/deluge/ui/data/pixmaps/flags/ws.png similarity index 100% rename from deluge/data/pixmaps/flags/ws.png rename to deluge/ui/data/pixmaps/flags/ws.png diff --git a/deluge/data/pixmaps/flags/ye.png b/deluge/ui/data/pixmaps/flags/ye.png similarity index 100% rename from deluge/data/pixmaps/flags/ye.png rename to deluge/ui/data/pixmaps/flags/ye.png diff --git a/deluge/data/pixmaps/flags/yt.png b/deluge/ui/data/pixmaps/flags/yt.png similarity index 100% rename from deluge/data/pixmaps/flags/yt.png rename to deluge/ui/data/pixmaps/flags/yt.png diff --git a/deluge/data/pixmaps/flags/yu.png b/deluge/ui/data/pixmaps/flags/yu.png similarity index 100% rename from deluge/data/pixmaps/flags/yu.png rename to deluge/ui/data/pixmaps/flags/yu.png diff --git a/deluge/data/pixmaps/flags/za.png b/deluge/ui/data/pixmaps/flags/za.png similarity index 100% rename from deluge/data/pixmaps/flags/za.png rename to deluge/ui/data/pixmaps/flags/za.png diff --git a/deluge/data/pixmaps/flags/zm.png b/deluge/ui/data/pixmaps/flags/zm.png similarity index 100% rename from deluge/data/pixmaps/flags/zm.png rename to deluge/ui/data/pixmaps/flags/zm.png diff --git a/deluge/data/pixmaps/flags/zw.png b/deluge/ui/data/pixmaps/flags/zw.png similarity index 100% rename from deluge/data/pixmaps/flags/zw.png rename to deluge/ui/data/pixmaps/flags/zw.png diff --git a/deluge/data/pixmaps/inactive.svg b/deluge/ui/data/pixmaps/inactive.svg similarity index 100% rename from deluge/data/pixmaps/inactive.svg rename to deluge/ui/data/pixmaps/inactive.svg diff --git a/deluge/data/pixmaps/inactive16.png b/deluge/ui/data/pixmaps/inactive16.png similarity index 100% rename from deluge/data/pixmaps/inactive16.png rename to deluge/ui/data/pixmaps/inactive16.png diff --git a/deluge/data/pixmaps/loading.gif b/deluge/ui/data/pixmaps/loading.gif similarity index 100% rename from deluge/data/pixmaps/loading.gif rename to deluge/ui/data/pixmaps/loading.gif diff --git a/deluge/data/pixmaps/lock48.png b/deluge/ui/data/pixmaps/lock48.png similarity index 100% rename from deluge/data/pixmaps/lock48.png rename to deluge/ui/data/pixmaps/lock48.png diff --git a/deluge/data/pixmaps/magnet.png b/deluge/ui/data/pixmaps/magnet.png similarity index 100% rename from deluge/data/pixmaps/magnet.png rename to deluge/ui/data/pixmaps/magnet.png diff --git a/deluge/data/pixmaps/queued.svg b/deluge/ui/data/pixmaps/queued.svg similarity index 100% rename from deluge/data/pixmaps/queued.svg rename to deluge/ui/data/pixmaps/queued.svg diff --git a/deluge/data/pixmaps/queued16.png b/deluge/ui/data/pixmaps/queued16.png similarity index 100% rename from deluge/data/pixmaps/queued16.png rename to deluge/ui/data/pixmaps/queued16.png diff --git a/deluge/data/pixmaps/seeding.svg b/deluge/ui/data/pixmaps/seeding.svg similarity index 100% rename from deluge/data/pixmaps/seeding.svg rename to deluge/ui/data/pixmaps/seeding.svg diff --git a/deluge/data/pixmaps/seeding16.png b/deluge/ui/data/pixmaps/seeding16.png similarity index 100% rename from deluge/data/pixmaps/seeding16.png rename to deluge/ui/data/pixmaps/seeding16.png diff --git a/deluge/ui/data/pixmaps/tracker_all16.png b/deluge/ui/data/pixmaps/tracker_all16.png new file mode 100644 index 000000000..36756bbcc Binary files /dev/null and b/deluge/ui/data/pixmaps/tracker_all16.png differ diff --git a/deluge/ui/data/pixmaps/tracker_warning16.png b/deluge/ui/data/pixmaps/tracker_warning16.png new file mode 100644 index 000000000..219432c44 Binary files /dev/null and b/deluge/ui/data/pixmaps/tracker_warning16.png differ diff --git a/deluge/data/pixmaps/traffic.svg b/deluge/ui/data/pixmaps/traffic.svg similarity index 100% rename from deluge/data/pixmaps/traffic.svg rename to deluge/ui/data/pixmaps/traffic.svg diff --git a/deluge/data/pixmaps/traffic16.png b/deluge/ui/data/pixmaps/traffic16.png similarity index 100% rename from deluge/data/pixmaps/traffic16.png rename to deluge/ui/data/pixmaps/traffic16.png diff --git a/deluge/ui/data/share/applications/deluge.desktop b/deluge/ui/data/share/applications/deluge.desktop new file mode 100644 index 000000000..b482a311f --- /dev/null +++ b/deluge/ui/data/share/applications/deluge.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Version=1.0 +Name=Deluge +GenericName=BitTorrent Client +X-GNOME-FullName=Deluge BitTorrent Client +Comment=Download and share files over BitTorrent +TryExec=deluge-gtk +Exec=deluge-gtk %U +Icon=deluge +Terminal=false +Type=Application +Categories=Network;FileTransfer;P2P;GTK +StartupNotify=true +MimeType=application/x-bittorrent;x-scheme-handler/magnet; diff --git a/deluge/ui/gtkui/aboutdialog.py b/deluge/ui/gtkui/aboutdialog.py index 7c549ae7b..3d91609ee 100644 --- a/deluge/ui/gtkui/aboutdialog.py +++ b/deluge/ui/gtkui/aboutdialog.py @@ -56,38 +56,240 @@ class AboutDialog: version = deluge.common.get_version() - self.about.set_copyright(u'Copyright \u00A9 2007-2009 Deluge Team') - self.about.set_comments("A peer-to-peer file sharing program\nutilizing the Bittorrent protocol.\n\nCore Version: %coreversion%\nlibtorrent version: %ltversion%") + self.about.set_copyright(u'Copyright \u00A9 2007-2011 Deluge Team') + self.about.set_comments( + "A peer-to-peer file sharing program\nutilizing the BitTorrent " + "protocol\n\n" + "Client Version: %s\n" % version) self.about.set_version(version) - self.about.set_authors(["Current Developers:", "Andrew Resch", "Damien Churchill", "John Garland", "", - "libtorrent (www.libtorrent.org):", "Arvid Norberg", "", "Past Developers or Contributors:", "Zach Tibbitts", "Alon Zakai", "Marcos Pinto", "Alex Dedul", "Sadrul Habib Chowdhury", "Ido Abramovich", "Martijn Voncken"]) + self.about.set_authors([ + "Current Developers:", "Andrew Resch", "Damien Churchill", + "John Garland", "", "libtorrent (www.libtorrent.org):", + "Arvid Norberg", "", "Past Developers or Contributors:", + "Zach Tibbitts", "Alon Zakai", "Marcos Pinto", "Alex Dedul", + "Sadrul Habib Chowdhury", "Ido Abramovich", "Martijn Voncken" + ]) self.about.set_artists(["Andrew Wedderburn", "Andrew Resch"]) - self.about.set_translator_credits("Aaron Wang Shi \nabbigss \nABCdatos \nAbcx \nActam \nAdam \nadaminikisi \nadi_oporanu \nAdrian Goll \nafby \nAhmades \nAhmad Farghal \nAhmad Gharbeia أحمد غربية \nakira \nAki Sivula \nAlan Pepelko \nAlberto \nAlberto Ferrer \nalcatr4z \nAlckO \nAleksej Korgenkov \nAlessio Treglia \nAlexander Ilyashov \nAlexander Matveev \nAlexander Saltykov \nAlexander Taubenkorb \nAlexander Telenga \nAlexander Yurtsev \nAlexandre Martani \nAlexandre Rosenfeld \nAlexandre Sapata Carbonell \nAlexey Osipov \nAlin Claudiu Radut \nallah \nAlSim \nAlvaro Carrillanca P. \nA.Matveev \nAndras Hipsag \nAndrás Kárász \nAndrea Ratto \nAndreas Johansson \nAndreas Str \nAndré F. Oliveira \nAndreiF \nandrewh \nAngel Guzman Maeso \nAníbal Deboni Neto \nanimarval \nAntonio Cono \nantoniojreyes \nAnton Shestakov \nAnton Yakutovich \nantou \nArkadiusz Kalinowski \nArtin \nartir \nAstur \nAthanasios Lefteris \nAthmane MOKRAOUI (ButterflyOfFire) \nAugusta Carla Klug \nAvoledo Marco \naxaard \nAxelRafn \nAxezium \nAyont \nb3rx \nBae Taegil \nBajusz Tamás \nBalaam's Miracle \nBallestein \nBent Ole Fosse \nberto89 \nbigx \nBjorn Inge Berg \nblackbird \nBlackeyed \nblackmx \nBlueSky \nBlutheo \nbmhm \nbob00work \nboenki \nBogdan Bădic-Spătariu \nbonpu \nBoone \nboss01 \nBranislav Jovanović \nbronze \nbrownie \nBrus46 \nbumper \nbutely \nBXCracer \nc0nfidencal \nCan Kaya \nCarlos Alexandro Becker \ncassianoleal \nCédric.h \nCésar Rubén \nchaoswizard \nChen Tao \nchicha \nChien Cheng Wei \nChristian Kopac \nChristian Widell \nChristoffer Brodd-Reijer \nchristooss \nCityAceE \nClopy \nClusty \ncnu \nCommandant \nConstantinos Koniaris \nCoolmax \ncosmix \nCostin Chirvasuta \nCoVaLiDiTy \ncow_2001 \nCrispin Kirchner \ncrom \nCruster \nCybolic \nDan Bishop \nDanek \nDani \nDaniel Demarco \nDaniel Ferreira \nDaniel Frank \nDaniel Holm \nDaniel Høyer Iversen \nDaniel Marynicz \nDaniel Nylander \nDaniel Patriche \nDaniel Schildt \nDaniil Sorokin \nDante Díaz \nDaria Michalska \nDarkenCZ \nDarren \nDaspah \nDavid Eurenius \ndavidhjelm \nDavid Machakhelidze \nDawid Dziurdzia \nDaya Adianto \ndcruz \nDeady \nDereck Wonnacott \nDevgru \nDevid Antonio Filoni \nDevilDogTG \ndi0rz` \nDialecti Valsamou \nDiego Medeiros \nDkzoffy \nDmitrij D. Czarkoff \nDmitriy Geels \nDmitry Olyenyov \nDominik Kozaczko \nDominik Lübben \ndoomster \nDorota Król \nDoyen Philippe \nDread Knight \nDreamSonic \nduan \nDuong Thanh An \nDvoglavaZver \ndwori \ndylansmrjones \nEbuntor \nEdgar Alejandro Jarquin Flores \nEetu \nekerazha \nElias Julkunen \nelparia \nEmberke \nEmiliano Goday Caneda \nEndelWar \neng.essam \nenubuntu \nercangun \nErdal Ronahi \nergin üresin \nEric \nÉric Lassauge \nErlend Finvåg \nErrdil \nethan shalev \nEvgeni Spasov \nezekielnin \nFabian Ordelmans \nFabio Mazanatti \nFábio Nogueira \nFaCuZ \nFelipe Lerena \nFernando Pereira \nfjetland \nFlorian Schäfer \nFoBoS \nFolke \nForce \nfosk \nfragarray \nfreddeg \nFrédéric Perrin \nFredrik Kilegran \nFreeAtMind \nFulvio Ciucci \nGabor Kelemen \nGalatsanos Panagiotis \nGaussian \ngdevitis \nGeorg Brzyk \nGeorge Dumitrescu \nGeorgi Arabadjiev \nGeorg Sieber \nGerd Radecke \nGermán Heusdens \nGianni Vialetto \nGigih Aji Ibrahim \nGiorgio Wicklein \nGiovanni Rapagnani \nGiuseppe \ngl \nglen \ngranjerox \nGreen Fish \ngreentea \nGreyhound \nG. U. \nGuillaume BENOIT \nGuillaume Pelletier \nGustavo Henrique Klug \ngutocarvalho \nGuybrush88 \nHans Rødtang \nHardDisk \nHargas Gábor \nHeitor Thury Barreiros Barbosa \nhelios91940 \nhelix84 \nHelton Rodrigues \nHendrik Luup \nHenrique Ferreiro \nHenry Goury-Laffont \nHezy Amiel \nhidro \nhoball \nhokten \nHolmsss \nhristo.num \nHubert Życiński \nHyo \nIarwain \nibe \nibear \nId2ndR \nIgor Zubarev \nIKON (Ion) \nimen \nIonuț Jula \nIsabelle STEVANT \nIstván Nyitrai \nIvan Petrovic \nIvan Prignano \nIvaSerge \njackmc \nJacks0nxD \nJack Shen \nJacky Yeung \nJacques Stadler \nJanek Thomaschewski \nJan Kaláb \nJan Niklas Hasse \nJasper Groenewegen \nJavi Rodríguez \nJayasimha (ಜಯಸಿಂಹ) \njeannich \nJeff Bailes \nJesse Zilstorff \nJoan Duran \nJoão Santos \nJoar Bagge \nJoe Anderson \nJoel Calado \nJohan Linde \nJohn Garland \nJojan \njollyr0ger \nJonas Bo Grimsgaard \nJonas Granqvist \nJonas Slivka \nJonathan Zeppettini \nJørgen \nJørgen Tellnes \njosé \nJosé Geraldo Gouvêa \nJosé Iván León Islas \nJosé Lou C. \nJose Sun \nJr. \nJukka Kauppinen \nJulián Alarcón \njulietgolf \nJusic \nJustzupi \nKaarel \nKai Thomsen \nKalman Tarnay \nKamil Páral \nKane_F \nkaotiks@gmail.com \nKateikyoushii \nkaxhinaz \nKazuhiro NISHIYAMA \nKerberos \nKeresztes Ákos \nkevintyk \nkiersie \nKimbo^ \nKim Lübbe \nkitzOgen \nKjetil Rydland \nkluon \nkmikz \nKnedlyk \nkoleoptero \nKőrösi Krisztián \nKouta \nKrakatos \nKrešo Kunjas \nkripken \nKristaps \nKristian Øllegaard \nKristoffer Egil Bonarjee \nKrzysztof Janowski \nKrzysztof Zawada \nLarry Wei Liu \nlaughterwym \nLaur Mõtus \nlazka \nleandrud \nlê bình \nLe Coz Florent \nLeo \nliorda \nLKRaider \nLoLo_SaG \nLong Tran \nLorenz \nLow Kian Seong \nLuca Andrea Rossi \nLuca Ferretti \nLucky LIX \nLuis Gomes \nLuis Reis \nŁukasz Wyszyński \nluojie-dune \nmaaark \nMaciej Chojnacki \nMaciej Meller \nMads Peter Rommedahl \nMajor Kong \nMalaki \nmalde \nMalte Lenz \nMantas Kriaučiūnas \nMara Sorella \nMarcin \nMarcin Falkiewicz \nmarcobra \nMarco da Silva \nMarco de Moulin \nMarco Rodrigues \nMarcos \nMarcos Escalier \nMarcos Pinto \nMarcus Ekstrom \nMarek Dębowski \nMário Buči \nMario Munda \nMarius Andersen \nMarius Hudea \nMarius Mihai \nMariusz Cielecki \nMark Krapivner \nmarko-markovic \nMarkus Brummer \nMarkus Sutter \nMartin \nMartin Dybdal \nMartin Iglesias \nMartin Lettner \nMartin Pihl \nMasoud Kalali \nmat02 \nMatej Urbančič \nMathias-K \nMathieu Arès \nMathieu D. (MatToufoutu) \nMathijs \nMatrik \nMatteo Renzulli \nMatteo Settenvini \nMatthew Gadd \nMatthias Benkard \nMatthias Mailänder \nMattias Ohlsson \nMauro de Carvalho \nMax Molchanov \nMe \nMercuryCC \nMert Bozkurt \nMert Dirik \nMFX \nmhietar \nmibtha \nMichael Budde \nMichael Kaliszka \nMichalis Makaronides \nMichał Tokarczyk \nMiguel Pires da Rosa \nMihai Capotă \nMiika Metsälä \nMikael Fernblad \nMike Sierra \nmikhalek \nMilan Prvulović \nMilo Casagrande \nMindaugas \nMiroslav Matejaš \nmisel \nmithras \nMitja Pagon \nM.Kitchen \nMohamed Magdy \nmoonkey \nMrBlonde \nmuczy \nMünir Ekinci \nMustafa Temizel \nmvoncken \nMytonn \nNagyMarton \nneaion \nNeil Lin \nNemo \nNerijus Arlauskas \nNicklas Larsson \nNicolaj Wyke \nNicola Piovesan \nNicolas Sabatier \nNicolas Velin \nNightfall \nNiKoB \nNikolai M. Riabov \nNiko_Thien \nniska \nNithir \nnoisemonkey \nnomemohes \nnosense \nnull \nNuno Estêvão \nNuno Santos \nnxxs \nnyo \nobo \nOjan \nOlav Andreas Lindekleiv \noldbeggar \nOlivier FAURAX \norphe \nosantana \nOsman Tosun \nOssiR \notypoks \nounn \nOz123 \nÖzgür BASKIN \nPablo Carmona A. \nPablo Ledesma \nPablo Navarro Castillo \nPaco Molinero \nPål-Eivind Johnsen \npano \nPaolo Naldini \nParacelsus \nPatryk13_03 \nPatryk Skorupa \nPattogoTehen \nPaul Lange \nPavcio \nPaweł Wysocki \nPedro Brites Moita \nPedro Clemente Pereira Neto \nPekka \"PEXI\" Niemistö \nPenegal \nPenzo \nperdido \nPeter Kotrcka \nPeter Skov \nPeter Van den Bosch \nPetter Eklund \nPetter Viklund \nphatsphere \nPhenomen \nPhilipi \nPhilippides Homer \nphoenix \npidi \nPierre Quillery \nPierre Rudloff \nPierre Slamich \nPietrao \nPiotr Strębski \nPiotr Wicijowski \nPittmann Tamás \nPlaymolas \nPrescott \nPrescott_SK \npronull \nPrzemysław Kulczycki \nPumy \npushpika \nPY \nqubicllj \nr21vo \nRafał Barański \nrainofchaos \nRajbir \nras0ir \nRat \nrd1381 \nRenato \nRene Hennig \nRene Pärts \nRicardo Duarte \nRichard \nRobert Hrovat \nRoberth Sjonøy \nRobert Lundmark \nRobin Jakobsson \nRobin Kåveland \nRodrigo Donado \nRoel Groeneveld \nrohmaru \nRolf Christensen \nRolf Leggewie \nRoni Kantis \nRonmi \nRostislav Raykov \nroyto \nRuiAmaro \nRui Araújo \nRui Moura \nRune Svendsen \nRusna \nRytis \nSabirov Mikhail \nsalseeg \nSami Koskinen \nSamir van de Sand \nSamuel Arroyo Acuña \nSamuel R. C. Vale \nSanel \nSanti \nSanti Martínez Cantelli \nSardan \nSargate Kanogan \nSarmad Jari \nSaša Bodiroža \nsat0shi \nSaulius Pranckevičius \nSavvas Radevic \nSebastian Krauß \nSebastián Porta \nSedir \nSefa Denizoğlu \nsekolands \nSelim Suerkan \nsemsomi \nSergii Golovatiuk \nsetarcos \nSheki \nShironeko \nShlomil \nsilfiriel \nSimone Tolotti \nSimone Vendemia \nsirkubador \nSławomir Więch \nslip \nslyon \nsmoke \nSonja \nspectral \nspin_555 \nspitf1r3 \nSpiziuz \nSpyros Theodoritsis \nSqUe \nSquigly \nsrtck \nStefan Horning \nStefano Maggiolo \nStefano Roberto Soleti \nsteinberger \nStéphane Travostino \nStephan Klein \nSteven De Winter \nStevie \nStian24 \nstylius \nSukarn Maini \nSunjae Park \nSusana Pereira \nszymon siglowy \ntakercena \nTAS \nTaygeto \ntemy4 \ntexxxxxx \nthamood \nThanos Chatziathanassiou \nTharawut Paripaiboon \nTheodoor \nThéophane Anestis \nThor Marius K. Høgås \nTiago Silva \nTiago Sousa \nTikkel \ntim__b \nTim Bordemann \nTim Fuchs \nTim Kornhammar \nTimo \nTimo Jyrinki \nTimothy Babych \nTitkosRejtozo \nTom \nTomas Gustavsson \nTomas Valentukevičius \nTomasz Dominikowski \nTomislav Plavčić \nTom Mannerhagen \nTommy Mikkelsen \nTom Verdaat \nTony Manco \nTor Erling H. Opsahl \nToudi \ntqm_z \nTrapanator \nTribaal \nTriton \nTuniX12 \nTuomo Sipola \nturbojugend_gr \nTurtle.net \ntwilight \ntymmej \nUlrik \nUmarzuki Mochlis \nunikob \nVadim Gusev \nVagi \nValentin Bora \nValmantas Palikša \nVASKITTU \nVassilis Skoullis \nvetal17 \nvicedo \nviki \nvillads hamann \nVincent Garibal \nVincent Ortalda \nvinchi007 \nVinícius de Figueiredo Silva \nVinzenz Vietzke \nvirtoo \nvirtual_spirit \nVitor Caike \nVitor Lamas Gatti \nVladimir Lazic \nVladimir Sharshov \nWanderlust \nWander Nauta \nWard De Ridder \nWebCrusader \nwebdr \nWentao Tang \nwilana \nWilfredo Ernesto Guerrero Campos \nWim Champagne \nWorld Sucks \nXabi Ezpeleta \nXavi de Moner \nXavierToo \nXChesser \nXiaodong Xu \nxyb \nYaron \nYasen Pramatarov \nYesPoX \nYuren Ju \nYves MATHIEU \nzekopeko \nzhuqin \nZissan \nΓιάννης Κατσαμπίρης \nАртём Попов \nМиша \nШаймарданов Максим \n蔡查理") + self.about.set_translator_credits("\n".join([ + "Aaron Wang Shi", "abbigss", "ABCdatos", "Abcx", "Actam", "Adam", + "adaminikisi", "adi_oporanu", "Adrian Goll", "afby", "Ahmades", + "Ahmad Farghal", "Ahmad Gharbeia أحمد غربية", "akira", "Aki Sivula", + "Alan Pepelko", "Alberto", "Alberto Ferrer", "alcatr4z", "AlckO", + "Aleksej Korgenkov", "Alessio Treglia", "Alexander Ilyashov", + "Alexander Matveev", "Alexander Saltykov", "Alexander Taubenkorb", + "Alexander Telenga", "Alexander Yurtsev", "Alexandre Martani", + "Alexandre Rosenfeld", "Alexandre Sapata Carbonell", + "Alexey Osipov", "Alin Claudiu Radut", "allah", "AlSim", + "Alvaro Carrillanca P.", "A.Matveev", "Andras Hipsag", + "András Kárász", "Andrea Ratto", "Andreas Johansson", "Andreas Str", + "André F. Oliveira", "AndreiF", "andrewh", "Angel Guzman Maeso", + "Aníbal Deboni Neto", "animarval", "Antonio Cono", "antoniojreyes", + "Anton Shestakov", "Anton Yakutovich", "antou", + "Arkadiusz Kalinowski", "Artin", "artir", "Astur", + "Athanasios Lefteris", "Athmane MOKRAOUI (ButterflyOfFire)", + "Augusta Carla Klug", "Avoledo Marco", "axaard", "AxelRafn", + "Axezium", "Ayont", "b3rx", "Bae Taegil", "Bajusz Tamás", + "Balaam's Miracle", "Ballestein", "Bent Ole Fosse", "berto89", + "bigx", "Bjorn Inge Berg", "blackbird", "Blackeyed", "blackmx", + "BlueSky", "Blutheo", "bmhm", "bob00work", "boenki", + "Bogdan Bădic-Spătariu", "bonpu", "Boone", "boss01", + "Branislav Jovanović", "bronze", "brownie", "Brus46", "bumper", + "butely", "BXCracer", "c0nfidencal", "Can Kaya", + "Carlos Alexandro Becker", "cassianoleal", "Cédric.h", + "César Rubén", "chaoswizard", "Chen Tao", "chicha", + "Chien Cheng Wei", "Christian Kopac", "Christian Widell", + "Christoffer Brodd-Reijer", "christooss", "CityAceE", "Clopy", + "Clusty", "cnu", "Commandant", "Constantinos Koniaris", "Coolmax", + "cosmix", "Costin Chirvasuta", "CoVaLiDiTy", "cow_2001", + "Crispin Kirchner", "crom", "Cruster", "Cybolic", "Dan Bishop", + "Danek", "Dani", "Daniel Demarco", "Daniel Ferreira", + "Daniel Frank", "Daniel Holm", "Daniel Høyer Iversen", + "Daniel Marynicz", "Daniel Nylander", "Daniel Patriche", + "Daniel Schildt", "Daniil Sorokin", "Dante Díaz", "Daria Michalska", + "DarkenCZ", "Darren", "Daspah", "David Eurenius", "davidhjelm", + "David Machakhelidze", "Dawid Dziurdzia", "Daya Adianto ", "dcruz", + "Deady", "Dereck Wonnacott", "Devgru", "Devid Antonio Filoni" + "DevilDogTG", "di0rz`", "Dialecti Valsamou", "Diego Medeiros", + "Dkzoffy", "Dmitrij D. Czarkoff", "Dmitriy Geels", + "Dmitry Olyenyov", "Dominik Kozaczko", "Dominik Lübben", "doomster", + "Dorota Król", "Doyen Philippe", "Dread Knight", "DreamSonic", + "duan", "Duong Thanh An", "DvoglavaZver", "dwori", "dylansmrjones", + "Ebuntor", "Edgar Alejandro Jarquin Flores", "Eetu", "ekerazha", + "Elias Julkunen", "elparia", "Emberke", "Emiliano Goday Caneda", + "EndelWar", "eng.essam", "enubuntu", "ercangun", "Erdal Ronahi", + "ergin üresin", "Eric", "Éric Lassauge", "Erlend Finvåg", "Errdil", + "ethan shalev", "Evgeni Spasov", "ezekielnin", "Fabian Ordelmans", + "Fabio Mazanatti", "Fábio Nogueira", "FaCuZ", "Felipe Lerena", + "Fernando Pereira", "fjetland", "Florian Schäfer", "FoBoS", "Folke", + "Force", "fosk", "fragarray", "freddeg", "Frédéric Perrin", + "Fredrik Kilegran", "FreeAtMind", "Fulvio Ciucci", "Gabor Kelemen", + "Galatsanos Panagiotis", "Gaussian", "gdevitis", "Georg Brzyk", + "George Dumitrescu", "Georgi Arabadjiev", "Georg Sieber", + "Gerd Radecke", "Germán Heusdens", "Gianni Vialetto", + "Gigih Aji Ibrahim", "Giorgio Wicklein", "Giovanni Rapagnani", + "Giuseppe", "gl", "glen", "granjerox", "Green Fish", "greentea", + "Greyhound", "G. U.", "Guillaume BENOIT", "Guillaume Pelletier", + "Gustavo Henrique Klug", "gutocarvalho", "Guybrush88", + "Hans Rødtang", "HardDisk", "Hargas Gábor", + "Heitor Thury Barreiros Barbosa", "helios91940", "helix84", + "Helton Rodrigues", "Hendrik Luup", "Henrique Ferreiro", + "Henry Goury-Laffont", "Hezy Amiel", "hidro", "hoball", "hokten", + "Holmsss", "hristo.num", "Hubert Życiński", "Hyo", "Iarwain", "ibe", + "ibear", "Id2ndR", "Igor Zubarev", "IKON (Ion)", "imen", + "Ionuț Jula", "Isabelle STEVANT", "István Nyitrai", "Ivan Petrovic", + "Ivan Prignano", "IvaSerge", "jackmc", "Jacks0nxD", "Jack Shen", + "Jacky Yeung","Jacques Stadler", "Janek Thomaschewski", "Jan Kaláb", + "Jan Niklas Hasse", "Jasper Groenewegen", "Javi Rodríguez", + "Jayasimha (ಜಯಸಿಂಹ)", "jeannich", "Jeff Bailes", "Jesse Zilstorff", + "Joan Duran", "João Santos", "Joar Bagge", "Joe Anderson", + "Joel Calado", "Johan Linde", "John Garland", "Jojan", "jollyr0ger", + "Jonas Bo Grimsgaard", "Jonas Granqvist", "Jonas Slivka", + "Jonathan Zeppettini", "Jørgen", "Jørgen Tellnes", "josé", + "José Geraldo Gouvêa", "José Iván León Islas", "José Lou C.", + "Jose Sun", "Jr.", "Jukka Kauppinen", "Julián Alarcón", + "julietgolf", "Jusic", "Justzupi", "Kaarel", "Kai Thomsen", + "Kalman Tarnay", "Kamil Páral", "Kane_F", "kaotiks@gmail.com", + "Kateikyoushii", "kaxhinaz", "Kazuhiro NISHIYAMA", "Kerberos", + "Keresztes Ákos", "kevintyk", "kiersie", "Kimbo^", "Kim Lübbe", + "kitzOgen", "Kjetil Rydland", "kluon", "kmikz", "Knedlyk", + "koleoptero", "Kőrösi Krisztián", "Kouta", "Krakatos", + "Krešo Kunjas", "kripken", "Kristaps", "Kristian Øllegaard", + "Kristoffer Egil Bonarjee", "Krzysztof Janowski", + "Krzysztof Zawada", "Larry Wei Liu", "laughterwym", "Laur Mõtus", + "lazka", "leandrud", "lê bình", "Le Coz Florent", "Leo", "liorda", + "LKRaider", "LoLo_SaG", "Long Tran", "Lorenz", "Low Kian Seong", + "Luca Andrea Rossi", "Luca Ferretti", "Lucky LIX", "Luis Gomes", + "Luis Reis", "Łukasz Wyszyński", "luojie-dune", "maaark", + "Maciej Chojnacki", "Maciej Meller", "Mads Peter Rommedahl", + "Major Kong", "Malaki", "malde", "Malte Lenz", "Mantas Kriaučiūnas", + "Mara Sorella", "Marcin", "Marcin Falkiewicz", "marcobra", + "Marco da Silva", "Marco de Moulin", "Marco Rodrigues", "Marcos", + "Marcos Escalier", "Marcos Pinto", "Marcus Ekstrom", + "Marek Dębowski", "Mário Buči", "Mario Munda", "Marius Andersen", + "Marius Hudea", "Marius Mihai", "Mariusz Cielecki", + "Mark Krapivner", "marko-markovic", "Markus Brummer", + "Markus Sutter", "Martin", "Martin Dybdal", "Martin Iglesias", + "Martin Lettner", "Martin Pihl", "Masoud Kalali", "mat02", + "Matej Urbančič", "Mathias-K", "Mathieu Arès", + "Mathieu D. (MatToufoutu)", "Mathijs", "Matrik", "Matteo Renzulli", + "Matteo Settenvini", "Matthew Gadd", "Matthias Benkard", + "Matthias Mailänder", "Mattias Ohlsson", "Mauro de Carvalho", + "Max Molchanov", "Me", "MercuryCC", "Mert Bozkurt", "Mert Dirik", + "MFX", "mhietar", "mibtha", "Michael Budde", "Michael Kaliszka", + "Michalis Makaronides", "Michał Tokarczyk", "Miguel Pires da Rosa", + "Mihai Capotă", "Miika Metsälä", "Mikael Fernblad", "Mike Sierra", + "mikhalek", "Milan Prvulović", "Milo Casagrande", "Mindaugas", + "Miroslav Matejaš", "misel", "mithras", "Mitja Pagon", "M.Kitchen", + "Mohamed Magdy", "moonkey", "MrBlonde", "muczy", "Münir Ekinci", + "Mustafa Temizel", "mvoncken", "Mytonn", "NagyMarton", "neaion", + "Neil Lin", "Nemo", "Nerijus Arlauskas", "Nicklas Larsson", + "Nicolaj Wyke", "Nicola Piovesan", "Nicolas Sabatier", + "Nicolas Velin", "Nightfall", "NiKoB", "Nikolai M. Riabov", + "Niko_Thien", "niska", "Nithir", "noisemonkey", "nomemohes", + "nosense", "null", "Nuno Estêvão", "Nuno Santos", "nxxs", "nyo", + "obo", "Ojan", "Olav Andreas Lindekleiv", "oldbeggar", + "Olivier FAURAX", "orphe", "osantana", "Osman Tosun", "OssiR", + "otypoks", "ounn", "Oz123", "Özgür BASKIN", "Pablo Carmona A.", + "Pablo Ledesma", "Pablo Navarro Castillo", "Paco Molinero", + "Pål-Eivind Johnsen", "pano", "Paolo Naldini", "Paracelsus", + "Patryk13_03", "Patryk Skorupa", "PattogoTehen", "Paul Lange", + "Pavcio", "Paweł Wysocki", "Pedro Brites Moita", + "Pedro Clemente Pereira Neto", "Pekka \"PEXI\" Niemistö", "Penegal", + "Penzo", "perdido", "Peter Kotrcka", "Peter Skov", + "Peter Van den Bosch", "Petter Eklund", "Petter Viklund", + "phatsphere", "Phenomen", "Philipi", "Philippides Homer", "phoenix", + "pidi", "Pierre Quillery", "Pierre Rudloff", "Pierre Slamich", + "Pietrao", "Piotr Strębski", "Piotr Wicijowski", "Pittmann Tamás", + "Playmolas", "Prescott", "Prescott_SK", "pronull", + "Przemysław Kulczycki", "Pumy", "pushpika", "PY", "qubicllj", + "r21vo", "Rafał Barański", "rainofchaos", "Rajbir", "ras0ir", "Rat", + "rd1381", "Renato", "Rene Hennig", "Rene Pärts", "Ricardo Duarte", + "Richard", "Robert Hrovat", "Roberth Sjonøy", "Robert Lundmark", + "Robin Jakobsson", "Robin Kåveland", "Rodrigo Donado", + "Roel Groeneveld", "rohmaru", "Rolf Christensen", "Rolf Leggewie", + "Roni Kantis", "Ronmi", "Rostislav Raykov", "royto", "RuiAmaro", + "Rui Araújo", "Rui Moura", "Rune Svendsen", "Rusna", "Rytis", + "Sabirov Mikhail", "salseeg", "Sami Koskinen", "Samir van de Sand", + "Samuel Arroyo Acuña", "Samuel R. C. Vale", "Sanel", "Santi", + "Santi Martínez Cantelli", "Sardan", "Sargate Kanogan", + "Sarmad Jari", "Saša Bodiroža", "sat0shi", "Saulius Pranckevičius", + "Savvas Radevic", "Sebastian Krauß", "Sebastián Porta", "Sedir", + "Sefa Denizoğlu", "sekolands", "Selim Suerkan", "semsomi", + "Sergii Golovatiuk", "setarcos", "Sheki", "Shironeko", "Shlomil", + "silfiriel", "Simone Tolotti", "Simone Vendemia", "sirkubador", + "Sławomir Więch", "slip", "slyon", "smoke", "Sonja", "spectral", + "spin_555", "spitf1r3", "Spiziuz", "Spyros Theodoritsis", "SqUe", + "Squigly", "srtck", "Stefan Horning", "Stefano Maggiolo", + "Stefano Roberto Soleti", "steinberger", "Stéphane Travostino", + "Stephan Klein", "Steven De Winter", "Stevie", "Stian24", "stylius", + "Sukarn Maini", "Sunjae Park", "Susana Pereira", "szymon siglowy", + "takercena", "TAS", "Taygeto", "temy4", "texxxxxx", "thamood", + "Thanos Chatziathanassiou", "Tharawut Paripaiboon", "Theodoor", + "Théophane Anestis", "Thor Marius K. Høgås", "Tiago Silva", + "Tiago Sousa", "Tikkel", "tim__b", "Tim Bordemann", "Tim Fuchs", + "Tim Kornhammar", "Timo", "Timo Jyrinki", "Timothy Babych", + "TitkosRejtozo", "Tom", "Tomas Gustavsson", "Tomas Valentukevičius", + "Tomasz Dominikowski", "Tomislav Plavčić", "Tom Mannerhagen", + "Tommy Mikkelsen", "Tom Verdaat", "Tony Manco", + "Tor Erling H. Opsahl", "Toudi", "tqm_z", "Trapanator", "Tribaal", + "Triton", "TuniX12", "Tuomo Sipola", "turbojugend_gr", "Turtle.net", + "twilight", "tymmej", "Ulrik", "Umarzuki Mochlis", "unikob", + "Vadim Gusev", "Vagi", "Valentin Bora", "Valmantas Palikša", + "VASKITTU", "Vassilis Skoullis", "vetal17", "vicedo", "viki", + "villads hamann", "Vincent Garibal", "Vincent Ortalda", "vinchi007", + "Vinícius de Figueiredo Silva", "Vinzenz Vietzke", "virtoo", + "virtual_spirit", "Vitor Caike", "Vitor Lamas Gatti", + "Vladimir Lazic", "Vladimir Sharshov", "Wanderlust", "Wander Nauta", + "Ward De Ridder", "WebCrusader", "webdr", "Wentao Tang", "wilana", + "Wilfredo Ernesto Guerrero Campos", "Wim Champagne", "World Sucks", + "Xabi Ezpeleta", "Xavi de Moner", "XavierToo", "XChesser", + "Xiaodong Xu", "xyb", "Yaron", "Yasen Pramatarov", "YesPoX", + "Yuren Ju", "Yves MATHIEU", "zekopeko", "zhuqin", "Zissan", + "Γιάννης Κατσαμπίρης", "Артём Попов", "Миша", "Шаймарданов Максим", + "蔡查理" + ])) self.about.set_wrap_license(True) - self.about.set_license(_("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 . \ -In addition, as a special exception, the copyright holders give \ -permission to link the code of portions of this program with the OpenSSL library. \ -You must obey the GNU General Public License in all respects for all of \ -the code used other than OpenSSL. If you modify file(s) with this \ -exception, you may extend this exception to your version of the file(s), \ -but you are not obligated to do so. If you do not wish to do so, delete \ -this exception statement from your version. If you delete this exception \ -statement from all source files in the program, then also delete it here.")) + self.about.set_license(_( + "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. \n\n" + "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. \n\n" + "You should have received " + "a copy of the GNU General Public License along with this program; " + "if not, see . \n\n" + "In addition, as a " + "special exception, the copyright holders give permission to link " + "the code of portions of this program with the OpenSSL library. " + "You must obey the GNU General Public License in all respects for " + "all of the code used other than OpenSSL. \n\n" + "If you modify file(s) " + "with this exception, you may extend this exception to your " + "version of the file(s), but you are not obligated to do so. If " + "you do not wish to do so, delete this exception statement from " + "your version. If you delete this exception statement from all " + "source files in the program, then also delete it here." + )) self.about.set_website("http://deluge-torrent.org") - self.about.set_website_label("http://deluge-torrent.org") + self.about.set_website_label("www.deluge-torrent.org") self.about.set_icon(common.get_deluge_icon()) self.about.set_logo(gtk.gdk.pixbuf_new_from_file( - deluge.common.get_pixmap("deluge-about.png"))) + deluge.common.get_pixmap("deluge-about.png") + )) if client.connected(): + if not client.is_classicmode(): + self.about.set_comments( + self.about.get_comments() + "Server Version: %coreversion%\n") + + self.about.set_comments( + self.about.get_comments() + "Libtorrent Version: %ltversion%\n") + def on_lt_version(result): c = self.about.get_comments() c = c.replace("%ltversion%", result) @@ -99,7 +301,10 @@ statement from all source files in the program, then also delete it here.")) self.about.set_comments(c) client.core.get_libtorrent_version().addCallback(on_lt_version) - client.daemon.info().addCallback(on_info) + if not client.is_classicmode(): + client.daemon.info().addCallback(on_info) + else: + client.core.get_libtorrent_version().addCallback(on_lt_version) def run(self): self.about.show_all() diff --git a/deluge/ui/gtkui/addtorrentdialog.py b/deluge/ui/gtkui/addtorrentdialog.py index a4fb421f0..e062b47ba 100644 --- a/deluge/ui/gtkui/addtorrentdialog.py +++ b/deluge/ui/gtkui/addtorrentdialog.py @@ -43,10 +43,14 @@ import gobject import base64 import logging import os +from urlparse import urljoin import pkg_resources +import twisted.web.client +import twisted.web.error from deluge.ui.client import client +from deluge.httpdownloader import download_file import deluge.component as component import listview from deluge.configmanager import ConfigManager @@ -77,7 +81,8 @@ class AddTorrentDialog(component.Component): "on_button_cancel_clicked": self._on_button_cancel_clicked, "on_button_add_clicked": self._on_button_add_clicked, "on_button_apply_clicked": self._on_button_apply_clicked, - "on_button_revert_clicked": self._on_button_revert_clicked + "on_button_revert_clicked": self._on_button_revert_clicked, + "on_alocation_toggled": self._on_alocation_toggled }) self.torrent_liststore = gtk.ListStore(str, str, str) @@ -140,6 +145,7 @@ class AddTorrentDialog(component.Component): "max_upload_speed_per_torrent", "max_download_speed_per_torrent", "prioritize_first_last_pieces", + "sequential_download", "download_location", "add_paused" ] @@ -207,7 +213,11 @@ class AddTorrentDialog(component.Component): if info.info_hash in self.files: log.debug("Trying to add a duplicate torrent!") - dialogs.ErrorDialog(_("Duplicate Torrent"), _("You cannot add the same torrent twice."), self.dialog).run() + dialogs.ErrorDialog( + _("Duplicate Torrent"), + _("You cannot add the same torrent twice."), + self.dialog + ).run() continue name = "%s (%s)" % (info.name, os.path.split(filename)[-1]) @@ -292,7 +302,9 @@ class AddTorrentDialog(component.Component): split_files = { } i = 0 for file in files: - self.prepare_file(file, file["path"], i, file["download"], split_files) + self.prepare_file( + file, file["path"], i, file["download"], split_files + ) i += 1 self.add_files(None, split_files) self.listview_files.set_model(self.files_treestore) @@ -319,8 +331,10 @@ class AddTorrentDialog(component.Component): self.files_treestore.set(chunk_iter, 2, chunk_size) ret += chunk_size else: - self.files_treestore.append(parent_iter, [value[2], key, - value[1]["size"], value[0], False, gtk.STOCK_FILE]) + self.files_treestore.append(parent_iter, [ + value[2], key, value[1]["size"], + value[0], False, gtk.STOCK_FILE + ]) if parent_iter and self.files_treestore.iter_has_child(parent_iter): # Iterate through the children and see what we should label the @@ -376,12 +390,15 @@ class AddTorrentDialog(component.Component): options["add_paused"]) self.glade.get_widget("chk_prioritize").set_active( options["prioritize_first_last_pieces"]) + self.glade.get_widget("chk_sequential_download").set_active( + options["sequential_download"]) def save_torrent_options(self, row=None): # Keeps the torrent options dictionary up-to-date with what the user has # selected. if row is None: - if self.previous_selected_torrent and self.torrent_liststore.iter_is_valid(self.previous_selected_torrent): + if self.previous_selected_torrent and \ + self.torrent_liststore.iter_is_valid(self.previous_selected_torrent): row = self.previous_selected_torrent else: return @@ -421,12 +438,16 @@ class AddTorrentDialog(component.Component): self.glade.get_widget("chk_paused").get_active() options["prioritize_first_last_pieces"] = \ self.glade.get_widget("chk_prioritize").get_active() + options["sequential_download"] = \ + self.glade.get_widget("radio_full").get_active() and \ + self.glade.get_widget("chk_sequential_download").get_active() or False self.options[torrent_id] = options # Save the file priorities files_priorities = self.build_priorities( - self.files_treestore.get_iter_first(), {}) + self.files_treestore.get_iter_first(), {} + ) if len(files_priorities) > 0: for i, file_dict in enumerate(self.files[torrent_id]): @@ -438,7 +459,8 @@ class AddTorrentDialog(component.Component): self.build_priorities(self.files_treestore.iter_children(iter), priorities) elif not self.files_treestore.get_value(iter, 1).endswith(os.path.sep): - priorities[self.files_treestore.get_value(iter, 3)] = self.files_treestore.get_value(iter, 0) + priorities[self.files_treestore.get_value(iter, 3)] = \ + self.files_treestore.get_value(iter, 0) iter = self.files_treestore.iter_next(iter) return priorities @@ -466,6 +488,8 @@ class AddTorrentDialog(component.Component): self.core_config["add_paused"]) self.glade.get_widget("chk_prioritize").set_active( self.core_config["prioritize_first_last_pieces"]) + self.glade.get_widget("chk_sequential_download").set_active( + self.core_config["sequential_download"]) def get_file_priorities(self, torrent_id): # A list of priorities @@ -488,7 +512,12 @@ class AddTorrentDialog(component.Component): self.options[model[row][0]]["compact_allocation"] = False self.update_torrent_options(model[row][0]) - d = dialogs.YesNoDialog(_("Unable to set file priority!"), _("File prioritization is unavailable when using Compact allocation. Would you like to switch to Full allocation?"), self.dialog).run() + d = dialogs.YesNoDialog( + _("Unable to set file priority!"), + _("File prioritization is unavailable when using Compact " + "allocation. Would you like to switch to Full allocation?"), + self.dialog + ).run() d.addCallback(on_answer) return @@ -537,11 +566,13 @@ class AddTorrentDialog(component.Component): def _on_button_file_clicked(self, widget): log.debug("_on_button_file_clicked") # Setup the filechooserdialog - chooser = gtk.FileChooserDialog(_("Choose a .torrent file"), + chooser = gtk.FileChooserDialog( + _("Choose a .torrent file"), None, gtk.FILE_CHOOSER_ACTION_OPEN, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, - gtk.RESPONSE_OK)) + gtk.RESPONSE_OK) + ) chooser.set_transient_for(self.dialog) chooser.set_select_multiple(True) @@ -602,7 +633,7 @@ class AddTorrentDialog(component.Component): response = dialog.run() if response == gtk.RESPONSE_OK: - url = entry.get_text().decode("utf_8") + url = entry.get_text().decode("utf-8") else: url = None @@ -618,7 +649,11 @@ class AddTorrentDialog(component.Component): elif deluge.common.is_magnet(url): self.add_from_magnets([url]) else: - dialogs.ErrorDialog(_("Invalid URL"), _("%s is not a valid URL." % url), self.dialog).run() + dialogs.ErrorDialog( + _("Invalid URL"), + _("%s is not a valid URL." % url), + self.dialog + ).run() def add_from_url(self, url): dialog = gtk.Dialog( @@ -654,14 +689,24 @@ class AddTorrentDialog(component.Component): dialog.destroy() def on_download_fail(result): - log.debug("Download failed: %s", result) - dialog.destroy() - dialogs.ErrorDialog(_("Download Failed"), _("Failed to download : %s" % url), details=result.getErrorMessage(), parent=self.dialog).run() + if result.check(twisted.web.error.PageRedirect): + new_url = urljoin(url, result.getErrorMessage().split(" to ")[1]) + result = download_file(new_url, tmp_file, on_part) + result.addCallbacks(on_download_success, on_download_fail) + elif result.check(twisted.web.client.PartialDownloadError): + result = download_file(url, tmp_file, on_part, allow_compression=False) + result.addCallbacks(on_download_success, on_download_fail) + else: + log.debug("Download failed: %s", result) + dialog.destroy() + dialogs.ErrorDialog( + _("Download Failed"), _("Failed to download : %s" % url), + details=result.getErrorMessage(), parent=self.dialog + ).run() + return result - import deluge.httpdownloader - d = deluge.httpdownloader.download_file(url, tmp_file, on_part) - d.addCallback(on_download_success) - d.addErrback(on_download_fail) + d = download_file(url, tmp_file, on_part) + d.addCallbacks(on_download_success, on_download_fail) def _on_button_hash_clicked(self, widget): log.debug("_on_button_hash_clicked") @@ -686,7 +731,7 @@ class AddTorrentDialog(component.Component): # handle this way. log.debug("trackers: %s", trackers) magnet = deluge.common.create_magnet_uri( - infohash=entry.get_text().decode("utf_8"), + infohash=entry.get_text().decode("utf-8"), trackers=trackers) log.debug("magnet uri: %s", magnet) self.add_from_magnets([magnet]) @@ -836,7 +881,9 @@ class AddTorrentDialog(component.Component): # Get the file path base once, since it will be the same for # all siblings - file_path_base = self.get_file_path(self.files_treestore.iter_parent(row)) + file_path_base = self.get_file_path( + self.files_treestore.iter_parent(row) + ) # Iterate through all the siblings at this level while row: @@ -868,8 +915,9 @@ class AddTorrentDialog(component.Component): for s in split_text[:-1]: # We don't iterate over the last item because we'll just use # the existing itr and change the text - parent = self.files_treestore.append(parent, - [True, s + os.path.sep, 0, -1, False, gtk.STOCK_DIRECTORY]) + parent = self.files_treestore.append(parent, [ + True, s + os.path.sep, 0, -1, False, gtk.STOCK_DIRECTORY + ]) self.files_treestore[itr][1] = split_text[-1] + os.path.sep @@ -888,3 +936,8 @@ class AddTorrentDialog(component.Component): # Walk through the tree from 'itr' and add all the new file paths # to the 'mapped_files' option walk_tree(itr) + + def _on_alocation_toggled(self, widget): + full_allocation_active = self.glade.get_widget("radio_full").get_active() + self.glade.get_widget("chk_prioritize").set_sensitive(full_allocation_active) + self.glade.get_widget("chk_sequential_download").set_sensitive(full_allocation_active) diff --git a/deluge/ui/gtkui/connectionmanager.py b/deluge/ui/gtkui/connectionmanager.py index 96fef9d1d..c76c80c8e 100644 --- a/deluge/ui/gtkui/connectionmanager.py +++ b/deluge/ui/gtkui/connectionmanager.py @@ -33,22 +33,22 @@ # # +import os import gtk import pkg_resources -import urlparse import time import hashlib import logging from twisted.internet import reactor import deluge.component as component -import deluge.common import common import deluge.configmanager +from deluge.ui.common import get_localhost_auth from deluge.ui.client import client import deluge.ui.client -import deluge.ui.common from deluge.configmanager import ConfigManager +from deluge.error import AuthenticationRequired, BadLoginError, IncompatibleClient import dialogs log = logging.getLogger(__name__) @@ -56,10 +56,6 @@ log = logging.getLogger(__name__) DEFAULT_HOST = "127.0.0.1" DEFAULT_PORT = 58846 -DEFAULT_CONFIG = { - "hosts": [(hashlib.sha1(str(time.time())).hexdigest(), DEFAULT_HOST, DEFAULT_PORT, "", "")] -} - HOSTLIST_COL_ID = 0 HOSTLIST_COL_HOST = 1 HOSTLIST_COL_PORT = 2 @@ -98,9 +94,7 @@ class ConnectionManager(component.Component): def __init__(self): component.Component.__init__(self, "ConnectionManager") self.gtkui_config = ConfigManager("gtkui.conf") - - self.config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) - + self.config = self.__load_config() self.running = False # Component overrides @@ -115,11 +109,32 @@ class ConnectionManager(component.Component): def shutdown(self): pass + def __load_config(self): + auth_file = deluge.configmanager.get_config_dir("auth") + if not os.path.exists(auth_file): + from deluge.common import create_localclient_account + create_localclient_account() + + localclient_username, localclient_password = get_localhost_auth() + DEFAULT_CONFIG = { + "hosts": [( + hashlib.sha1(str(time.time())).hexdigest(), + DEFAULT_HOST, + DEFAULT_PORT, + localclient_username, + localclient_password + )] + } + config = ConfigManager("hostlist.conf.1.2", DEFAULT_CONFIG) + config.run_converter((0, 1), 2, self.__migrate_config_1_to_2) + return config + # Public methods def show(self): """ Show the ConnectionManager dialog. """ + self.config = self.__load_config() # Get the glade file for the connection manager self.glade = gtk.glade.XML( pkg_resources.resource_filename("deluge.ui.gtkui", @@ -134,12 +149,21 @@ class ConnectionManager(component.Component): self.glade.get_widget("image1").set_from_pixbuf(common.get_logo(32)) + self.askpassword_dialog = self.glade.get_widget("askpassword_dialog") + self.askpassword_dialog.set_transient_for(self.connection_manager) + self.askpassword_dialog.set_icon(common.get_deluge_icon()) + self.askpassword_dialog_entry = self.glade.get_widget("askpassword_dialog_entry") + self.hostlist = self.glade.get_widget("hostlist") # Create status pixbufs if not HOSTLIST_PIXBUFS: for stock_id in (gtk.STOCK_NO, gtk.STOCK_YES, gtk.STOCK_CONNECT): - HOSTLIST_PIXBUFS.append(self.connection_manager.render_icon(stock_id, gtk.ICON_SIZE_MENU)) + HOSTLIST_PIXBUFS.append( + self.connection_manager.render_icon( + stock_id, gtk.ICON_SIZE_MENU + ) + ) # Create the host list gtkliststore # id-hash, hostname, port, status, username, password, version @@ -148,33 +172,37 @@ class ConnectionManager(component.Component): # Setup host list treeview self.hostlist.set_model(self.liststore) render = gtk.CellRendererPixbuf() - column = gtk.TreeViewColumn("Status", render) + column = gtk.TreeViewColumn(_("Status"), render) column.set_cell_data_func(render, cell_render_status, 3) self.hostlist.append_column(column) render = gtk.CellRendererText() - column = gtk.TreeViewColumn("Host", render, text=HOSTLIST_COL_HOST) + column = gtk.TreeViewColumn(_("Host"), render, text=HOSTLIST_COL_HOST) column.set_cell_data_func(render, cell_render_host, (1, 2, 4)) column.set_expand(True) self.hostlist.append_column(column) render = gtk.CellRendererText() - column = gtk.TreeViewColumn("Version", render, text=HOSTLIST_COL_VERSION) + column = gtk.TreeViewColumn(_("Version"), render, text=HOSTLIST_COL_VERSION) self.hostlist.append_column(column) + # Connect the signals to the handlers + self.glade.signal_autoconnect(self) + self.hostlist.get_selection().connect( + "changed", self.on_hostlist_selection_changed + ) + # Load any saved host entries self.__load_hostlist() self.__load_options() - - # Select the first host if possible - if len(self.liststore) > 0: - self.hostlist.get_selection().select_path("0") - - # Connect the signals to the handlers - self.glade.signal_autoconnect(self) - self.hostlist.get_selection().connect("changed", self.on_hostlist_selection_changed) - self.__update_list() self.running = True + # Trigger the on_selection_changed code and select the first host + # if possible + self.hostlist.get_selection().unselect_all() + if len(self.liststore) > 0: + self.hostlist.get_selection().select_path("0") + + # Run the dialog response = self.connection_manager.run() self.running = False @@ -202,7 +230,8 @@ class ConnectionManager(component.Component): # Check to see if there is already an entry for this host and return # if thats the case for entry in self.liststore: - if [entry[HOSTLIST_COL_HOST], entry[HOSTLIST_COL_PORT], entry[HOSTLIST_COL_USER]] == [host, port, username]: + if [entry[HOSTLIST_COL_HOST], entry[HOSTLIST_COL_PORT], + entry[HOSTLIST_COL_USER]] == [host, port, username]: raise Exception("Host already in list!") # Host isn't in the list, so lets add it @@ -230,7 +259,11 @@ class ConnectionManager(component.Component): # Grab the hosts from the liststore self.config["hosts"] = [] for row in self.liststore: - self.config["hosts"].append((row[HOSTLIST_COL_ID], row[HOSTLIST_COL_HOST], row[HOSTLIST_COL_PORT], row[HOSTLIST_COL_USER], row[HOSTLIST_COL_PASS])) + self.config["hosts"].append((row[HOSTLIST_COL_ID], + row[HOSTLIST_COL_HOST], + row[HOSTLIST_COL_PORT], + row[HOSTLIST_COL_USER], + row[HOSTLIST_COL_PASS])) self.config.save() @@ -246,6 +279,7 @@ class ConnectionManager(component.Component): self.liststore[new_row][HOSTLIST_COL_USER] = host[3] self.liststore[new_row][HOSTLIST_COL_PASS] = host[4] self.liststore[new_row][HOSTLIST_COL_STATUS] = _("Offline") + self.liststore[new_row][HOSTLIST_COL_VERSION] = "" def __get_host_row(self, host_id): """ @@ -296,6 +330,7 @@ class ConnectionManager(component.Component): row = self.__get_host_row(host_id) if row: row[HOSTLIST_COL_STATUS] = _("Offline") + row[HOSTLIST_COL_VERSION] = "" self.__update_buttons() for row in self.liststore: @@ -303,22 +338,24 @@ class ConnectionManager(component.Component): host = row[HOSTLIST_COL_HOST] port = row[HOSTLIST_COL_PORT] user = row[HOSTLIST_COL_USER] - password = row[HOSTLIST_COL_PASS] if client.connected() and \ (host, port, "localclient" if not user and host in ("127.0.0.1", "localhost") else user) == client.connection_info(): def on_info(info): if not self.running: return + log.debug("Client connected, query info: %s", info) row[HOSTLIST_COL_VERSION] = info self.__update_buttons() + row[HOSTLIST_COL_STATUS] = _("Connected") + log.debug("Query daemon's info") client.daemon.info().addCallback(on_info) continue # Create a new Client instance c = deluge.ui.client.Client() - d = c.connect(host, port, user, password) + d = c.connect(host, port, skip_authentication=True) d.addCallback(on_connect, c, host_id) d.addErrback(on_connect_failed, host_id) @@ -326,9 +363,15 @@ class ConnectionManager(component.Component): """ Set the widgets to show the correct options from the config. """ - self.glade.get_widget("chk_autoconnect").set_active(self.gtkui_config["autoconnect"]) - self.glade.get_widget("chk_autostart").set_active(self.gtkui_config["autostart_localhost"]) - self.glade.get_widget("chk_donotshow").set_active(not self.gtkui_config["show_connection_manager_on_start"]) + self.glade.get_widget("chk_autoconnect").set_active( + self.gtkui_config["autoconnect"] + ) + self.glade.get_widget("chk_autostart").set_active( + self.gtkui_config["autostart_localhost"] + ) + self.glade.get_widget("chk_donotshow").set_active( + not self.gtkui_config["show_connection_manager_on_start"] + ) def __save_options(self): """ @@ -353,11 +396,17 @@ class ConnectionManager(component.Component): model, row = self.hostlist.get_selection().get_selected() if not row: + self.glade.get_widget("button_edithost").set_sensitive(False) return + self.glade.get_widget("button_edithost").set_sensitive(True) + # Get some values about the selected host status = model[row][HOSTLIST_COL_STATUS] host = model[row][HOSTLIST_COL_HOST] + port = model[row][HOSTLIST_COL_PORT] + user = model[row][HOSTLIST_COL_USER] + passwd = model[row][HOSTLIST_COL_PASS] log.debug("Status: %s", status) # Check to see if we have a localhost entry selected @@ -385,7 +434,7 @@ class ConnectionManager(component.Component): self.glade.get_widget("image_startdaemon").set_from_stock( gtk.STOCK_STOP, gtk.ICON_SIZE_MENU) self.glade.get_widget("label_startdaemon").set_text( - "_Stop Daemon") + _("_Stop Daemon")) # Update the start daemon button if the selected host is localhost if localhost and status == _("Offline"): @@ -393,15 +442,20 @@ class ConnectionManager(component.Component): self.glade.get_widget("image_startdaemon").set_from_stock( gtk.STOCK_EXECUTE, gtk.ICON_SIZE_MENU) self.glade.get_widget("label_startdaemon").set_text( - "_Start Daemon") + _("_Start Daemon")) - if not localhost: - # An offline host + if client.connected() and (host, port, user) == client.connection_info(): + # If we're connected, we can stop the dameon + self.glade.get_widget("button_startdaemon").set_sensitive(True) + elif user and passwd: + # In this case we also have all the info to shutdown the dameon + self.glade.get_widget("button_startdaemon").set_sensitive(True) + else: + # Can't stop non localhost daemons, specially without the necessary info self.glade.get_widget("button_startdaemon").set_sensitive(False) # Make sure label is displayed correctly using mnemonics - self.glade.get_widget("label_startdaemon").set_use_underline( - True) + self.glade.get_widget("label_startdaemon").set_use_underline(True) def start_daemon(self, port, config): """ @@ -414,8 +468,10 @@ class ConnectionManager(component.Component): if e.errno == 2: dialogs.ErrorDialog( _("Unable to start daemon!"), - _("Deluge cannot find the 'deluged' executable, it is likely \ -that you forgot to install the deluged package or it's not in your PATH.")).run() + _("Deluge cannot find the 'deluged' executable, it is " + "likely that you forgot to install the deluged package " + "or it's not in your PATH.")).run() + return False else: raise e except Exception, e: @@ -428,12 +484,68 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( details=traceback.format_exc(tb[2])).run() # Signal handlers - def __on_connected(self, connector, host_id): + def __connect(self, host_id, host, port, username, password, + skip_authentication=False, try_counter=0): + def do_connect(*args): + d = client.connect(host, port, username, password, skip_authentication) + d.addCallback(self.__on_connected, host_id) + d.addErrback(self.__on_connected_failed, host_id, host, port, + username, password, try_counter) + return d + + if client.connected(): + return client.disconnect().addCallback(do_connect) + else: + return do_connect() + + def __on_connected(self, daemon_info, host_id): if self.gtkui_config["autoconnect"]: self.gtkui_config["autoconnect_host_id"] = host_id - + if self.running: + # When connected to a client, and then trying to connect to another, + # this component will be stopped(while the connect deferred is + # runing), so, self.connection_manager will be deleted. + # If that's not the case, close the dialog. + self.connection_manager.response(gtk.RESPONSE_OK) component.start() + def __on_connected_failed(self, reason, host_id, host, port, user, passwd, + try_counter): + log.debug("Failed to connect: %s", reason.value) + + if reason.check(AuthenticationRequired, BadLoginError): + log.debug("PasswordRequired exception") + dialog = dialogs.AuthenticationDialog( + reason.value.message, reason.value.username + ) + def dialog_finished(response_id, host, port, user): + if response_id == gtk.RESPONSE_OK: + self.__connect(host_id, host, port, + user and user or dialog.get_username(), + dialog.get_password()) + d = dialog.run().addCallback(dialog_finished, host, port, user) + return d + + elif reason.trap(IncompatibleClient): + dialog = dialogs.ErrorDialog( + _("Incompatible Client"), reason.value.message + ) + return dialog.run() + + + if try_counter: + log.info("Retrying connection.. Retries left: %s", try_counter) + return reactor.callLater( + 0.5, self.__connect, host_id, host, port, user, passwd, + try_counter=try_counter-1 + ) + + msg = str(reason.value) + if not self.glade.get_widget("chk_autostart").get_active(): + msg += '\n' + _("Auto-starting the daemon localy is not enabled. " + "See \"Options\" on the \"Connection Manager\".") + dialogs.ErrorDialog(_("Failed To Connect"), msg).run() + def on_button_connect_clicked(self, widget=None): model, row = self.hostlist.get_selection().get_selected() if not row: @@ -451,39 +563,16 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( user = model[row][HOSTLIST_COL_USER] password = model[row][HOSTLIST_COL_PASS] - if status == _("Offline") and self.glade.get_widget("chk_autostart").get_active() and\ - host in ("127.0.0.1", "localhost"): - # We need to start this localhost - self.start_daemon(port, deluge.configmanager.get_config_dir()) - - def on_connect_fail(result, try_counter): - log.error("Connection to host failed..") - # We failed connecting to the daemon, but lets try again - if try_counter: - log.info("Retrying connection.. Retries left: %s", try_counter) - try_counter -= 1 - import time - time.sleep(0.5) - do_retry_connect(try_counter) - return result - def do_retry_connect(try_counter): - log.debug("user: %s pass: %s", user, password) - d = client.connect(host, port, user, password) - d.addCallback(self.__on_connected, host_id) - d.addErrback(on_connect_fail, try_counter) - - do_retry_connect(6) - - - def do_connect(*args): - client.connect(host, port, user, password).addCallback(self.__on_connected, host_id) - - if client.connected(): - client.disconnect().addCallback(do_connect) - else: - do_connect() - - self.connection_manager.response(gtk.RESPONSE_OK) + if status == _("Offline") and \ + self.glade.get_widget("chk_autostart").get_active() and \ + host in ("127.0.0.1", "localhost"): + if not self.start_daemon(port, deluge.configmanager.get_config_dir()): + log.debug("Failed to auto-start daemon") + return + return self.__connect( + host_id, host, port, user, password, try_counter=6 + ) + return self.__connect(host_id, host, port, user, password) def on_button_close_clicked(self, widget): self.connection_manager.response(gtk.RESPONSE_CLOSE) @@ -497,6 +586,10 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( port_spinbutton = self.glade.get_widget("spinbutton_port") username_entry = self.glade.get_widget("entry_username") password_entry = self.glade.get_widget("entry_password") + button_addhost_save = self.glade.get_widget("button_addhost_save") + button_addhost_save.hide() + button_addhost_add = self.glade.get_widget("button_addhost_add") + button_addhost_add.show() response = dialog.run() if response == 1: username = username_entry.get_text() @@ -505,7 +598,8 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( # We add the host try: - self.add_host(hostname, port_spinbutton.get_value_as_int(), username, password) + self.add_host(hostname, port_spinbutton.get_value_as_int(), + username, password) except Exception, e: from deluge.ui.gtkui.dialogs import ErrorDialog ErrorDialog(_("Error Adding Host"), e).run() @@ -516,6 +610,54 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( port_spinbutton.set_value(58846) dialog.hide() + def on_button_edithost_clicked(self, widget=None): + log.debug("on_button_edithost_clicked") + model, row = self.hostlist.get_selection().get_selected() + status = model[row][HOSTLIST_COL_STATUS] + if status == _("Connected"): + def on_disconnect(reason): + self.__update_list() + client.disconnect().addCallback(on_disconnect) + return + + dialog = self.glade.get_widget("addhost_dialog") + dialog.set_transient_for(self.connection_manager) + dialog.set_position(gtk.WIN_POS_CENTER_ON_PARENT) + hostname_entry = self.glade.get_widget("entry_hostname") + port_spinbutton = self.glade.get_widget("spinbutton_port") + username_entry = self.glade.get_widget("entry_username") + password_entry = self.glade.get_widget("entry_password") + button_addhost_save = self.glade.get_widget("button_addhost_save") + button_addhost_save.show() + button_addhost_add = self.glade.get_widget("button_addhost_add") + button_addhost_add.hide() + + username_entry.set_text(self.liststore[row][HOSTLIST_COL_USER]) + password_entry.set_text(self.liststore[row][HOSTLIST_COL_PASS]) + hostname_entry.set_text(self.liststore[row][HOSTLIST_COL_HOST]) + port_spinbutton.set_value(self.liststore[row][HOSTLIST_COL_PORT]) + + response = dialog.run() + + if response == 2: + self.liststore[row][HOSTLIST_COL_HOST] = hostname_entry.get_text() + self.liststore[row][HOSTLIST_COL_PORT] = port_spinbutton.get_value_as_int() + self.liststore[row][HOSTLIST_COL_USER] = username_entry.get_text() + self.liststore[row][HOSTLIST_COL_PASS] = password_entry.get_text() + self.liststore[row][HOSTLIST_COL_STATUS] = _("Offline") + + # Save the host list to file + self.__save_hostlist() + + # Update the status of the hosts + self.__update_list() + + username_entry.set_text("") + password_entry.set_text("") + hostname_entry.set_text("") + port_spinbutton.set_value(58846) + dialog.hide() + def on_button_removehost_clicked(self, widget): log.debug("on_button_removehost_clicked") # Get the selected rows @@ -533,9 +675,11 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( log.debug("on_button_startdaemon_clicked") if self.liststore.iter_n_children(None) < 1: # There is nothing in the list, so lets create a localhost entry - self.add_host(DEFAULT_HOST, DEFAULT_PORT) + self.add_host(DEFAULT_HOST, DEFAULT_PORT, *get_localhost_auth()) # ..and start the daemon. - self.start_daemon(DEFAULT_PORT, deluge.configmanager.get_config_dir()) + self.start_daemon( + DEFAULT_PORT, deluge.configmanager.get_config_dir() + ) return paths = self.hostlist.get_selection().get_selected_rows()[1] @@ -557,9 +701,10 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( def on_daemon_shutdown(d): # Update display to show change self.__update_list() + if client.connected() and client.connection_info() == (host, port, user): client.daemon.shutdown().addCallback(on_daemon_shutdown) - else: + elif user and password: # Create a new client instance c = deluge.ui.client.Client() def on_connect(d, c): @@ -580,3 +725,23 @@ that you forgot to install the deluged package or it's not in your PATH.")).run( def on_hostlist_selection_changed(self, treeselection): self.__update_buttons() + + def on_askpassword_dialog_connect_button_clicked(self, widget): + log.debug("on on_askpassword_dialog_connect_button_clicked") + self.askpassword_dialog.response(gtk.RESPONSE_OK) + + def on_askpassword_dialog_entry_activate(self, entry): + self.askpassword_dialog.response(gtk.RESPONSE_OK) + + def __migrate_config_1_to_2(self, config): + localclient_username, localclient_password = get_localhost_auth() + if not localclient_username: + # Nothing to do here, there's no auth file + return + for idx, (_, host, _, username, _) in enumerate(config["hosts"][:]): + if host in ("127.0.0.1", "localhost"): + if not username: + config["hosts"][idx][3] = localclient_username + config["hosts"][idx][4] = localclient_password + return config + diff --git a/deluge/ui/gtkui/createtorrentdialog.py b/deluge/ui/gtkui/createtorrentdialog.py index 2fb7b689a..786347e55 100644 --- a/deluge/ui/gtkui/createtorrentdialog.py +++ b/deluge/ui/gtkui/createtorrentdialog.py @@ -285,14 +285,11 @@ class CreateTorrentDialog: tracker = None else: # Create a list of lists [[tier0, ...], [tier1, ...], ...] + tier_dict = {} for tier, tracker in self.trackers_liststore: - try: - tier_list = trackers[tier] - except IndexError: - trackers.insert(tier, []) - - trackers[tier].append(tracker) + tier_dict.setdefault(tier, []).append(tracker) + trackers = [tier_dict[tier] for tier in sorted(tier_dict)] # Get the first tracker in the first tier tracker = trackers[0][0] @@ -369,7 +366,7 @@ class CreateTorrentDialog: if add_to_session: client.core.add_torrent_file( os.path.split(target)[-1], - base64.encodestring(open(target).read()), + base64.encodestring(open(target, "rb").read()), {"download_location": os.path.split(path)[0]}) def _on_create_torrent_progress(self, value, num_pieces): @@ -382,6 +379,8 @@ class CreateTorrentDialog: def _on_button_up_clicked(self, widget): log.debug("_on_button_up_clicked") row = self.glade.get_widget("tracker_treeview").get_selection().get_selected()[1] + if row is None: + return if self.trackers_liststore[row][0] == 0: return else: @@ -390,6 +389,8 @@ class CreateTorrentDialog: def _on_button_down_clicked(self, widget): log.debug("_on_button_down_clicked") row = self.glade.get_widget("tracker_treeview").get_selection().get_selected()[1] + if row is None: + return self.trackers_liststore[row][0] += 1 def _on_button_add_clicked(self, widget): @@ -433,4 +434,6 @@ class CreateTorrentDialog: def _on_button_remove_clicked(self, widget): log.debug("_on_button_remove_clicked") row = self.glade.get_widget("tracker_treeview").get_selection().get_selected()[1] + if row is None: + return self.trackers_liststore.remove(row) diff --git a/deluge/ui/gtkui/details_tab.py b/deluge/ui/gtkui/details_tab.py index 96f39cfbf..4a79f21fc 100644 --- a/deluge/ui/gtkui/details_tab.py +++ b/deluge/ui/gtkui/details_tab.py @@ -66,7 +66,7 @@ class DetailsTab(Tab): (glade.get_widget("summary_hash"), str, ("hash",)), (glade.get_widget("summary_comments"), str, ("comment",)), (glade.get_widget("summary_owner"), str, ("owner",)), - (glade.get_widget("summary_public"), str, ("public",)) + (glade.get_widget("summary_shared"), str, ("shared",)) ] def update(self): @@ -82,9 +82,9 @@ class DetailsTab(Tab): return # Get the torrent status - status_keys = ["name", "total_size", "num_files", - "tracker", "save_path", "message", "hash", "comment", "owner", - "public"] + status_keys = ["name", "total_size", "num_files", "tracker", + "save_path", "message", "hash", "comment", "owner", + "shared"] session = component.get("SessionProxy") session.get_torrent_status(selected, status_keys).addCallback(self._on_get_torrent_status) diff --git a/deluge/ui/gtkui/dialogs.py b/deluge/ui/gtkui/dialogs.py index cf1cf910c..ead22f349 100644 --- a/deluge/ui/gtkui/dialogs.py +++ b/deluge/ui/gtkui/dialogs.py @@ -36,6 +36,7 @@ import gtk from twisted.internet import defer +from deluge.ui.gtkui import common import deluge.component as component @@ -58,6 +59,8 @@ class BaseDialog(gtk.Dialog): flags=gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT | gtk.DIALOG_NO_SEPARATOR, buttons=buttons) + self.set_icon(common.get_deluge_icon()) + self.connect("delete-event", self._on_delete_event) self.connect("response", self._on_response) @@ -147,13 +150,16 @@ class ErrorDialog(BaseDialog): When run(), it will return a gtk.RESPONSE_CLOSE. """ - def __init__(self, header, text, parent=None, details=None): + def __init__(self, header, text, parent=None, details=None, traceback=False): """ :param header: see `:class:BaseDialog` :param text: see `:class:BaseDialog` :param parent: see `:class:BaseDialog` - :param details: str, extra information that will be displayed in a + :param details: extra information that will be displayed in a scrollable textview + :type details: string + :param traceback: show the traceback information in the details area + :type traceback: bool """ super(ErrorDialog, self).__init__( header, @@ -162,8 +168,18 @@ class ErrorDialog(BaseDialog): (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE), parent) + if traceback: + import traceback + import sys + tb = sys.exc_info() + tb = traceback.format_exc(tb[2]) + if details: + details += "\n" + tb + else: + details = tb + if details: - self.set_default_size(500, 400) + self.set_default_size(600, 400) textview = gtk.TextView() textview.set_editable(False) textview.get_buffer().set_text(details) @@ -176,3 +192,142 @@ class ErrorDialog(BaseDialog): self.vbox.pack_start(label, False, False) self.vbox.pack_start(sw) self.vbox.show_all() + +class AuthenticationDialog(BaseDialog): + """ + Displays a dialog with entry fields asking for username and password. + + When run(), it will return either a gtk.RESPONSE_CANCEL or a + gtk.RESPONSE_OK. + """ + def __init__(self, err_msg="", username=None, parent=None): + """ + :param err_msg: the error message we got back from the server + :type err_msg: string + """ + super(AuthenticationDialog, self).__init__( + _("Authenticate"), err_msg, + gtk.STOCK_DIALOG_AUTHENTICATION, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_CONNECT, gtk.RESPONSE_OK), + parent) + + table = gtk.Table(2, 2, False) + self.username_label = gtk.Label() + self.username_label.set_markup(_("Username:")) + self.username_label.set_alignment(1.0, 0.5) + self.username_label.set_padding(5, 5) + self.username_entry = gtk.Entry() + table.attach(self.username_label, 0, 1, 0, 1) + table.attach(self.username_entry, 1, 2, 0, 1) + + self.password_label = gtk.Label() + self.password_label.set_markup(_("Password:")) + self.password_label.set_alignment(1.0, 0.5) + self.password_label.set_padding(5, 5) + self.password_entry = gtk.Entry() + self.password_entry.set_visibility(False) + self.password_entry.connect("activate", self.on_password_activate) + table.attach(self.password_label, 0, 1, 1, 2) + table.attach(self.password_entry, 1, 2, 1, 2) + + self.vbox.pack_start(table, False, False, padding=5) + self.set_focus(self.password_entry) + if username: + self.username_entry.set_text(username) + self.username_entry.set_editable(False) + self.set_focus(self.password_entry) + else: + self.set_focus(self.username_entry) + self.show_all() + + def get_username(self): + return self.username_entry.get_text() + + def get_password(self): + return self.password_entry.get_text() + + def on_password_activate(self, widget): + self.response(gtk.RESPONSE_OK) + +class AccountDialog(BaseDialog): + def __init__(self, username=None, password=None, authlevel=None, + levels_mapping=None, parent=None): + if username: + super(AccountDialog, self).__init__( + _("Edit Account"), + _("Edit existing account"), + gtk.STOCK_DIALOG_INFO, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_APPLY, gtk.RESPONSE_OK), + parent) + else: + super(AccountDialog, self).__init__( + _("New Account"), + _("Create a new account"), + gtk.STOCK_DIALOG_INFO, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_ADD, gtk.RESPONSE_OK), + parent) + + self.levels_mapping = levels_mapping + + table = gtk.Table(2, 3, False) + self.username_label = gtk.Label() + self.username_label.set_markup(_("Username:")) + self.username_label.set_alignment(1.0, 0.5) + self.username_label.set_padding(5, 5) + self.username_entry = gtk.Entry() + table.attach(self.username_label, 0, 1, 0, 1) + table.attach(self.username_entry, 1, 2, 0, 1) + + self.authlevel_label = gtk.Label() + self.authlevel_label.set_markup(_("Authentication Level:")) + self.authlevel_label.set_alignment(1.0, 0.5) + self.authlevel_label.set_padding(5, 5) + + self.authlevel_combo = gtk.combo_box_new_text() + active_idx = None + for idx, level in enumerate(levels_mapping.keys()): + self.authlevel_combo.append_text(level) + if authlevel and authlevel==level: + active_idx = idx + elif not authlevel and level == 'DEFAULT': + active_idx = idx + + if active_idx is not None: + self.authlevel_combo.set_active(active_idx) + + table.attach(self.authlevel_label, 0, 1, 1, 2) + table.attach(self.authlevel_combo, 1, 2, 1, 2) + + self.password_label = gtk.Label() + self.password_label.set_markup(_("Password:")) + self.password_label.set_alignment(1.0, 0.5) + self.password_label.set_padding(5, 5) + self.password_entry = gtk.Entry() + self.password_entry.set_visibility(False) + table.attach(self.password_label, 0, 1, 2, 3) + table.attach(self.password_entry, 1, 2, 2, 3) + + self.vbox.pack_start(table, False, False, padding=5) + if username: + self.username_entry.set_text(username) + self.username_entry.set_editable(False) + else: + self.set_focus(self.username_entry) + + if password: + self.password_entry.set_text(username) + + self.show_all() + + def get_username(self): + return self.username_entry.get_text() + + def get_password(self): + return self.password_entry.get_text() + + def get_authlevel(self): + combobox = self.authlevel_combo + level = combobox.get_model()[combobox.get_active()][0] + return level diff --git a/deluge/ui/gtkui/edittrackersdialog.py b/deluge/ui/gtkui/edittrackersdialog.py index 2c9dc73a6..aa109dd7a 100644 --- a/deluge/ui/gtkui/edittrackersdialog.py +++ b/deluge/ui/gtkui/edittrackersdialog.py @@ -39,6 +39,7 @@ import gtk.glade import logging import pkg_resources +from twisted.internet import defer import deluge.common import common from deluge.ui.client import client @@ -73,8 +74,6 @@ class EditTrackersDialog: "on_button_edit_ok_clicked": self.on_button_edit_ok_clicked, "on_button_remove_clicked": self.on_button_remove_clicked, "on_button_down_clicked": self.on_button_down_clicked, - "on_button_ok_clicked": self.on_button_ok_clicked, - "on_button_cancel_clicked": self.on_button_cancel_clicked, "on_button_add_ok_clicked": self.on_button_add_ok_clicked, "on_button_add_cancel_clicked": self.on_button_add_cancel_clicked }) @@ -91,6 +90,9 @@ class EditTrackersDialog: self.treeview.set_model(self.liststore) self.liststore.set_sort_column_id(0, gtk.SORT_ASCENDING) + self.dialog.connect("delete-event", self._on_delete_event) + self.dialog.connect("response", self._on_response) + def run(self): # Make sure we have a torrent_id.. if not just return if self.torrent_id == None: @@ -98,14 +100,43 @@ class EditTrackersDialog: # Get the trackers for this torrent session = component.get("SessionProxy") - session.get_torrent_status(self.torrent_id, ["trackers"]).addCallback(self._on_get_torrent_status) + session.get_torrent_status( + self.torrent_id, ["trackers"] + ).addCallback(self._on_get_torrent_status) client.force_call() + self.deferred = defer.Deferred() + return self.deferred + + + def _on_delete_event(self, widget, event): + self.deferred.callback(gtk.RESPONSE_DELETE_EVENT) + self.dialog.destroy() + + def _on_response(self, widget, response): + if response == 1: + self.trackers = [] + def each(model, path, iter, data): + tracker = {} + tracker["tier"] = model.get_value(iter, 0) + tracker["url"] = model.get_value(iter, 1) + self.trackers.append(tracker) + self.liststore.foreach(each, None) + if self.old_trackers != self.trackers: + # Set the torrens trackers + client.core.set_torrent_trackers(self.torrent_id, self.trackers) + self.deferred.callback(gtk.RESPONSE_OK) + else: + self.deferred.callback(gtk.RESPONSE_CANCEL) + else: + self.deferred.callback(gtk.RESPONSE_CANCEL) + self.dialog.destroy() + def _on_get_torrent_status(self, status): """Display trackers dialog""" - for tracker in status["trackers"]: + self.old_trackers = list(status["trackers"]) + for tracker in self.old_trackers: self.add_tracker(tracker["tier"], tracker["url"]) - self.dialog.show() def add_tracker(self, tier, url): @@ -116,16 +147,6 @@ class EditTrackersDialog: """Returns the selected tracker""" return self.treeview.get_selection().get_selected()[1] - def on_button_up_clicked(self, widget): - log.debug("on_button_up_clicked") - selected = self.get_selected() - num_rows = self.liststore.iter_n_children(None) - if selected != None and num_rows > 1: - tier = self.liststore.get_value(selected, 0) - new_tier = tier + 1 - # Now change the tier for this tracker - self.liststore.set_value(selected, 0, new_tier) - def on_button_add_clicked(self, widget): log.debug("on_button_add_clicked") # Show the add tracker dialog @@ -159,8 +180,8 @@ class EditTrackersDialog: self.liststore.set_value(selected, 1, tracker) self.edit_tracker_entry.hide() - def on_button_down_clicked(self, widget): - log.debug("on_button_down_clicked") + def on_button_up_clicked(self, widget): + log.debug("on_button_up_clicked") selected = self.get_selected() num_rows = self.liststore.iter_n_children(None) if selected != None and num_rows > 1: @@ -171,22 +192,15 @@ class EditTrackersDialog: # Now change the tier for this tracker self.liststore.set_value(selected, 0, new_tier) - def on_button_ok_clicked(self, widget): - log.debug("on_button_ok_clicked") - self.trackers = [] - def each(model, path, iter, data): - tracker = {} - tracker["tier"] = model.get_value(iter, 0) - tracker["url"] = model.get_value(iter, 1) - self.trackers.append(tracker) - self.liststore.foreach(each, None) - # Set the torrens trackers - client.core.set_torrent_trackers(self.torrent_id, self.trackers) - self.dialog.destroy() - - def on_button_cancel_clicked(self, widget): - log.debug("on_button_cancel_clicked") - self.dialog.destroy() + def on_button_down_clicked(self, widget): + log.debug("on_button_down_clicked") + selected = self.get_selected() + num_rows = self.liststore.iter_n_children(None) + if selected != None and num_rows > 1: + tier = self.liststore.get_value(selected, 0) + new_tier = tier + 1 + # Now change the tier for this tracker + self.liststore.set_value(selected, 0, new_tier) def on_button_add_ok_clicked(self, widget): log.debug("on_button_add_ok_clicked") diff --git a/deluge/ui/gtkui/files_tab.py b/deluge/ui/gtkui/files_tab.py index 1fc0ccba8..a9476caac 100644 --- a/deluge/ui/gtkui/files_tab.py +++ b/deluge/ui/gtkui/files_tab.py @@ -53,13 +53,29 @@ import common log = logging.getLogger(__name__) +def _(message): return message + +TRANSLATE = { + "Do Not Download": _("Do Not Download"), + "Normal Priority": _("Normal Priority"), + "High Priority": _("High Priority"), + "Highest Priority": _("Highest Priority"), +} + +del _ + +def _t(text): + if text in TRANSLATE: + text = TRANSLATE[text] + return _(text) + def cell_priority(column, cell, model, row, data): if model.get_value(row, 5) == -1: # This is a folder, so lets just set it blank for now cell.set_property("text", "") return priority = model.get_value(row, data) - cell.set_property("text", deluge.common.FILE_PRIORITY[priority]) + cell.set_property("text", _t(deluge.common.FILE_PRIORITY[priority])) def cell_priority_icon(column, cell, model, row, data): if model.get_value(row, 5) == -1: @@ -67,13 +83,13 @@ def cell_priority_icon(column, cell, model, row, data): cell.set_property("stock-id", None) return priority = model.get_value(row, data) - if deluge.common.FILE_PRIORITY[priority] == _("Do Not Download"): + if deluge.common.FILE_PRIORITY[priority] == "Do Not Download": cell.set_property("stock-id", gtk.STOCK_NO) - elif deluge.common.FILE_PRIORITY[priority] == _("Normal Priority"): + elif deluge.common.FILE_PRIORITY[priority] == "Normal Priority": cell.set_property("stock-id", gtk.STOCK_YES) - elif deluge.common.FILE_PRIORITY[priority] == _("High Priority"): + elif deluge.common.FILE_PRIORITY[priority] == "High Priority": cell.set_property("stock-id", gtk.STOCK_GO_UP) - elif deluge.common.FILE_PRIORITY[priority] == _("Highest Priority"): + elif deluge.common.FILE_PRIORITY[priority] == "Highest Priority": cell.set_property("stock-id", gtk.STOCK_GOTO_TOP) def cell_filename(column, cell, model, row, data): @@ -106,7 +122,8 @@ class FilesTab(Tab): self._editing_index = None # Filename column - column = gtk.TreeViewColumn(_("Filename")) + self.filename_column_name = _("Filename") + column = gtk.TreeViewColumn(self.filename_column_name) render = gtk.CellRendererPixbuf() column.pack_start(render, False) column.add_attribute(render, "stock-id", 6) @@ -164,6 +181,8 @@ class FilesTab(Tab): column.set_resizable(True) column.set_expand(False) column.set_min_width(100) + # Bugfix: Last column needs max_width set to stop scrollbar appearing + column.set_max_width(200) column.set_reorderable(True) self.listview.append_column(column) @@ -422,9 +441,8 @@ class FilesTab(Tab): """ Go through the tree and update the folder complete percentages. """ - root = self.treestore.get_iter_root() - if self.treestore[root][5] != -1: + if root is None or self.treestore[root][5] != -1: return def get_completed_bytes(row): @@ -438,7 +456,11 @@ class FilesTab(Tab): row = self.treestore.iter_next(row) - value = (float(bytes) / float(self.treestore[parent][1])) * 100 + try: + value = (float(bytes) / float(self.treestore[parent][1])) * 100 + except ZeroDivisionError: + # Catch the unusal error found when moving folders around + value = 0 self.treestore[parent][3] = value self.treestore[parent][2] = "%.2f%%" % value return bytes @@ -463,7 +485,10 @@ class FilesTab(Tab): if self._editing_index == row[5]: continue - progress_string = "%.2f%%" % (status["file_progress"][index] * 100) + try: + progress_string = "%.2f%%" % (status["file_progress"][index] * 100) + except IndexError: + continue if row[2] != progress_string: row[2] = progress_string progress_value = status["file_progress"][index] * 100 @@ -482,17 +507,15 @@ class FilesTab(Tab): # We only care about right-clicks if event.button == 3: x, y = event.get_coords() - path = self.listview.get_path_at_pos(int(x), int(y)) - if not path: + cursor_path = self.listview.get_path_at_pos(int(x), int(y)) + if not cursor_path: return - row = self.treestore.get_iter(path[0]) - if self.get_selected_files(): - if self.treestore.get_value(row, 5) not in self.get_selected_files(): + paths = self.listview.get_selection().get_selected_rows()[1] + if cursor_path[0] not in paths: + row = self.treestore.get_iter(cursor_path[0]) self.listview.get_selection().unselect_all() self.listview.get_selection().select_iter(row) - else: - self.listview.get_selection().select_iter(row) for widget in self.file_menu_priority_items: widget.set_sensitive(not self.__compact) @@ -501,16 +524,24 @@ class FilesTab(Tab): return True def _on_key_press_event(self, widget, event): - # Menu key - if gtk.gdk.keyval_name(event.keyval) != "Menu": - return - - if not self.get_selected_files(): - return + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname is not None: + func = getattr(self, 'keypress_' + keyname, None) + selected_rows = self.listview.get_selection().get_selected_rows()[1] + if func and selected_rows: + return func(event) + def keypress_Menu(self, event): self.file_menu.popup(None, None, None, 3, event.time) return True + def keypress_F2(self, event): + path, col = self.listview.get_cursor() + for column in self.listview.get_columns(): + if column.get_title() == self.filename_column_name: + self.listview.set_cursor(path, column, True) + return True + def _on_menuitem_open_file_activate(self, menuitem): self._on_row_activated(None, None, None) @@ -605,34 +636,34 @@ class FilesTab(Tab): def _on_filename_editing_canceled(self, renderer): self._editing_index = None - def _on_torrentfilerenamed_event(self, event): - log.debug("index: %s name: %s", event.index, event.filename) + def _on_torrentfilerenamed_event(self, torrent_id, index, name): + log.debug("index: %s name: %s", index, name) - if event.torrent_id not in self.files_list: + if torrent_id not in self.files_list: return - old_name = self.files_list[event.torrent_id][event.index]["path"] - self.files_list[event.torrent_id][event.index]["path"] = event.filename + old_name = self.files_list[torrent_id][index]["path"] + self.files_list[torrent_id][index]["path"] = name # We need to update the filename displayed if we're currently viewing # this torrents files. - if event.torrent_id == self.torrent_id: + if torrent_id == self.torrent_id: old_name_len = len(old_name.split("/")) - name_len = len(event.filename.split("/")) + name_len = len(name.split("/")) if old_name_len != name_len: # The parent path list changes depending on which way the file # is moving in the tree if old_name_len < name_len: parent_path = [o for o in old_name.split("/")[:-1]] else: - parent_path = [o for o in event.filename.split("/")[:-1]] + parent_path = [o for o in name.split("/")[:-1]] # Find the iter to the parent folder we need to add a new folder # to. def find_parent(model, path, itr, user_data): if model[itr][0] == parent_path[0] + "/": if len(parent_path) == 1: # This is the parent iter - to_create = event.filename.split("/")[len(old_name.split("/")[:-1]):-1] + to_create = name.split("/")[len(old_name.split("/")[:-1]):-1] parent_iter = itr for tc in to_create: @@ -651,8 +682,8 @@ class FilesTab(Tab): # Find the iter for the file that needs to be moved def get_file_iter(model, path, itr, user_data): - if model[itr][5] == event.index: - model[itr][0] = event.filename.split("/")[-1] + if model[itr][5] == index: + model[itr][0] = name.split("/")[-1] t = self.treestore.append( parent_iter, self.treestore.get(itr, @@ -671,7 +702,7 @@ class FilesTab(Tab): if parent_path: self.treestore.foreach(find_parent, None) else: - new_folders = event.filename.split("/")[:-1] + new_folders = name.split("/")[:-1] parent_iter = None for f in new_folders: parent_iter = self.treestore.append(parent_iter, @@ -685,8 +716,8 @@ class FilesTab(Tab): else: # This is just changing a filename without any folder changes def set_file_name(model, path, itr, user_data): - if model[itr][5] == event.index: - model[itr][0] = os.path.split(event.filename)[-1] + if model[itr][5] == index: + model[itr][0] = os.path.split(name)[-1] return True self.treestore.foreach(set_file_name, None) @@ -732,40 +763,40 @@ class FilesTab(Tab): self.treestore.remove(itr) itr = parent - def _on_torrentfolderrenamed_event(self, event): + def _on_torrentfolderrenamed_event(self, torrent_id, old_folder, new_folder): log.debug("on_torrent_folder_renamed_signal") - log.debug("old_folder: %s new_folder: %s", event.old, event.new) + log.debug("old_folder: %s new_folder: %s", old_folder, new_folder) - if event.torrent_id not in self.files_list: + if torrent_id not in self.files_list: return - if event.old[-1] != "/": - event.old += "/" - if event.new[-1] != "/": - event.new += "/" + if old_folder[-1] != "/": + old_folder += "/" + if new_folder[-1] != "/": + new_folder += "/" - for fd in self.files_list[event.torrent_id]: - if fd["path"].startswith(event.old): - fd["path"] = fd["path"].replace(event.old, event.new, 1) + for fd in self.files_list[torrent_id]: + if fd["path"].startswith(old_folder): + fd["path"] = fd["path"].replace(old_folder, new_folder, 1) - if event.torrent_id == self.torrent_id: + if torrent_id == self.torrent_id: - old_split = event.old.split("/") + old_split = old_folder.split("/") try: old_split.remove("") except: pass - new_split = event.new.split("/") + new_split = new_folder.split("/") try: new_split.remove("") except: pass - old_folder_iter = self.get_iter_at_path(event.old) + old_folder_iter = self.get_iter_at_path(old_folder) old_folder_iter_parent = self.treestore.iter_parent(old_folder_iter) - new_folder_iter = self.get_iter_at_path(event.new) + new_folder_iter = self.get_iter_at_path(new_folder) if len(new_split) == len(old_split): # These are at the same tree depth, so it's a simple rename self.treestore[old_folder_iter][0] = new_split[-1] + "/" @@ -785,9 +816,9 @@ class FilesTab(Tab): # and if so, we delete it self.remove_childless_folders(old_folder_iter_parent) - def _on_torrentremoved_event(self, event): - if event.torrent_id in self.files_list: - del self.files_list[event.torrent_id] + def _on_torrentremoved_event(self, torrent_id): + if torrent_id in self.files_list: + del self.files_list[torrent_id] def _on_drag_data_get_data(self, treeview, context, selection, target_id, etime): paths = self.listview.get_selection().get_selected_rows()[1] diff --git a/deluge/ui/gtkui/filtertreeview.py b/deluge/ui/gtkui/filtertreeview.py index d7a242840..1c1309e6f 100644 --- a/deluge/ui/gtkui/filtertreeview.py +++ b/deluge/ui/gtkui/filtertreeview.py @@ -39,6 +39,8 @@ import gtk import gtk.glade import logging import pkg_resources +import glib +import warnings import deluge.component as component import deluge.common @@ -58,21 +60,38 @@ STATE_PIX = { "Active": "active" } - -TRANSLATE = { - "state": "States", - "tracker_host": "Trackers", - "label": "Labels", - "owner": "Owner" +TRACKER_PIX = { + "All": "tracker_all", + "Error": "tracker_warning", } -FILTER_COLUMN = 5 +def _(message): return message + +TRANSLATE = { + "state": _("States"), + "tracker_host": _("Trackers"), + "label": _("Labels"), + "owner": _("Owner"), + "All": _("All"), + "Downloading": _("Downloading"), + "Seeding": _("Seeding"), + "Paused": _("Paused"), + "Checking": _("Checking"), + "Queued": _("Queued"), + "Error": _("Error"), + "Active": _("Active"), + "none": _("None"), + "no_label": _("No Label"), +} + +del _ def _t(text): if text in TRANSLATE: text = TRANSLATE[text] return _(text) +FILTER_COLUMN = 5 #sidebar-treeview class FilterTreeView(component.Component): @@ -87,7 +106,7 @@ class FilterTreeView(component.Component): self.tracker_icons = component.get("TrackerIcons") self.label_view = gtk.TreeView() - self.sidebar.add_tab(self.label_view, "filters", _("Filters")) + self.sidebar.add_tab(self.label_view, "filters", "Filters") #set filter to all when hidden: self.sidebar.notebook.connect("hide", self._on_hide) @@ -109,7 +128,7 @@ class FilterTreeView(component.Component): self.treestore = gtk.TreeStore(str, str, str, int, gtk.gdk.Pixbuf, bool) # Create the column - column = gtk.TreeViewColumn(_("Filters")) + column = gtk.TreeViewColumn("Filters") column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) render = gtk.CellRendererPixbuf() self.renderpix = render @@ -147,7 +166,7 @@ class FilterTreeView(component.Component): self.filters = {} #initial order of state filter: - self.cat_nodes["state"] = self.treestore.append(None, ["cat", "state", _("State"), 0, None, False]) + self.cat_nodes["state"] = self.treestore.append(None, ["cat", "state", _t("State"), 0, None, False]) self.update_row("state", "All" , 0) self.update_row("state", "Downloading" , 0) self.update_row("state", "Seeding" , 0) @@ -155,6 +174,11 @@ class FilterTreeView(component.Component): self.update_row("state", "Paused" , 0) self.update_row("state", "Queued" , 0) + self.cat_nodes["tracker_host"] = self.treestore.append(None, ["cat", "tracker_host", _t("Trackers"), 0, None, False]) + self.update_row("tracker_host", "All" , 0) + self.update_row("tracker_host", "Error" , 0) + self.update_row("tracker_host", "" , 0) + # We set to this expand the rows on start-up self.expand_rows = True @@ -211,8 +235,15 @@ class FilterTreeView(component.Component): else: pix = self.get_pixmap(cat, value) label = value - if cat == "state": - label = _(value) + + if label == "": + if cat == "tracker_host": + label = _t("none") + elif cat == "label": + label = _t("no_label") + elif cat == "state" or cat == "tracker_host": + label = _t(value) + row = self.treestore.append(self.cat_nodes[cat],[cat, value, label, count , pix, True]) self.filters[(cat, value)] = row @@ -229,10 +260,11 @@ class FilterTreeView(component.Component): value = model.get_value(row, 1) label = model.get_value(row, 2) count = model.get_value(row, 3) - pix = model.get_value(row, 4) - if label == "" and cat == "label": - label = _("no label") + #Supress Warning: g_object_set_qdata: assertion `G_IS_OBJECT (object)' failed + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + pix = model.get_value(row, 4) if pix: self.renderpix.set_property("visible", True) @@ -251,11 +283,23 @@ class FilterTreeView(component.Component): cell.set_property('text', txt) def get_pixmap(self, cat, value): + pix = None if cat == "state": - pix = STATE_PIX.get(value, "dht") - return gtk.gdk.pixbuf_new_from_file(deluge.common.get_pixmap("%s16.png" % pix)) + pix = STATE_PIX.get(value, None) + elif cat == "tracker_host": + pix = TRACKER_PIX.get(value, None) - return None + if pix: + try: + return gtk.gdk.pixbuf_new_from_file(deluge.common.get_pixmap("%s16.png" % pix)) + except glib.GError as e: + log.warning(e) + return self.get_transparent_pix(16, 16) + + def get_transparent_pix(self, width, height): + pix = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, width, height) + pix.fill(0x0000000) + return pix def set_row_image(self, cat, value, filename): pix = None @@ -265,8 +309,7 @@ class FilterTreeView(component.Component): log.debug(e) if not pix: - pix = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 16, 16) - pix.fill(0x00000000) + pix = self.get_transparent_pix(16, 16) row = self.filters[(cat, value)] self.treestore.set_value(row, 4, pix) return False diff --git a/deluge/ui/gtkui/glade/add_torrent_dialog.glade b/deluge/ui/gtkui/glade/add_torrent_dialog.glade index d59c24d20..98d7aafc6 100644 --- a/deluge/ui/gtkui/glade/add_torrent_dialog.glade +++ b/deluge/ui/gtkui/glade/add_torrent_dialog.glade @@ -1,8 +1,9 @@ - + - + + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Add Torrents @@ -12,24 +13,25 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 2 True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 @@ -37,8 +39,8 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -58,12 +60,15 @@ + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK center @@ -72,16 +77,19 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 4 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-open 1 @@ -95,6 +103,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _File True @@ -120,16 +129,19 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 4 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-network 1 @@ -143,6 +155,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _URL True @@ -168,16 +181,19 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 4 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-revert-to-saved 1 @@ -191,6 +207,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Info_hash True @@ -216,15 +233,18 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 4 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-remove @@ -237,6 +257,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Remove True @@ -270,6 +291,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Torrents</b> True @@ -313,25 +335,32 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-open + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Fi_les True + True + True 5 1 @@ -345,19 +374,21 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 - vertical 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 5 @@ -365,14 +396,18 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK select-folder Select A Folder + True + True 0 @@ -380,8 +415,14 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True + True + True 1 @@ -392,6 +433,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Download Location</b> True @@ -410,25 +452,28 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 10 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Full @@ -436,8 +481,10 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True True + False @@ -452,8 +499,10 @@ True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True radio_full + False @@ -468,6 +517,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Allocation</b> True @@ -486,18 +536,21 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 4 2 @@ -508,7 +561,11 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 - -1 -1 9999 1 10 0 + False + False + True + True + 0 -1 9999 1 10 0 1 @@ -520,6 +577,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Max Down Speed: @@ -532,6 +590,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Max Up Speed: @@ -546,6 +605,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Max Connections: @@ -560,6 +620,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Max Upload Slots: @@ -577,7 +638,11 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 - -1 -1 9999 1 10 0 + False + False + True + True + 0 -1 9999 1 10 0 if-valid @@ -595,7 +660,11 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 - -1 -1 9999 1 10 0 + False + False + True + True + 0 -1 9999 1 10 0 1 @@ -612,7 +681,11 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 - -1 -1 9999 1 10 0 + False + False + True + True + 0 -1 9999 1 10 0 1 @@ -630,6 +703,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Bandwidth</b> True @@ -648,29 +722,30 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 5 - - Add In _Paused State + + Prioritize First/Last Pieces True True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True + False True @@ -680,12 +755,19 @@ - - Prioritize First/Last Pieces + + Sequential Download True True False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + When enabled, the piece picker will pick pieces in +sequence instead of rarest first. + +Enabling sequential download will affect the piece +distribution negatively in the swarm. It should be +used sparingly. + False True @@ -694,6 +776,23 @@ 1 + + + Add In _Paused State + True + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + True + + + False + False + 2 + + @@ -701,6 +800,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>General</b> True @@ -726,63 +826,13 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-revert-to-saved - - - False - False - 0 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Revert To Defaults - - - False - False - 5 - 1 - - - - - - - - - False - False - end - 1 - - True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -790,14 +840,17 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 gtk-apply @@ -811,6 +864,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Apply To All @@ -834,6 +888,63 @@ 0 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-revert-to-saved + + + False + False + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Revert To Defaults + + + False + False + 5 + 1 + + + + + + + + + False + False + end + 1 + + False @@ -849,25 +960,32 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-properties + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Options True + True + True 5 1 @@ -887,12 +1005,15 @@ + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK end @@ -902,6 +1023,7 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -918,6 +1040,7 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -930,6 +1053,7 @@ False + True end 1 @@ -937,130 +1061,37 @@ - + 462 + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 - Add URL + Add Infohash center-on-parent True dialog False - False - + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 2 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-add - - - False - False - 0 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - <b>From URL</b> - True - - - False - False - 1 - - - - - False - False - 0 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - False - 1 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - URL: - - - False - False - 0 - - - - - True - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - 1 - - - - - False - False - 2 - - - - - 1 - - - + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK end - + gtk-cancel -6 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -1070,7 +1101,7 @@ - + gtk-ok -5 True @@ -1079,6 +1110,7 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -1090,43 +1122,27 @@ False + True end 0 - - - - - 462 - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - Add Infohash - center-on-parent - True - dialog - False - False - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-revert-to-saved @@ -1139,6 +1155,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>From Infohash</b> True @@ -1159,21 +1176,25 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Infohash: @@ -1191,8 +1212,14 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False + False + True + True + True + True 1 @@ -1206,10 +1233,12 @@ True + False 5 True + False 0 Trackers: @@ -1234,32 +1263,59 @@ + True + True 1 + True + True 3 + True + True 1 + + + + + 462 + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + Add URL + center-on-parent + True + dialog + False + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 2 - + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK end - + gtk-cancel -6 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -1269,7 +1325,7 @@ - + gtk-ok -5 True @@ -1278,6 +1334,7 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -1289,10 +1346,121 @@ False + True end 0 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-add + + + False + False + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + <b>From URL</b> + True + + + False + False + 1 + + + + + False + False + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + False + True + 1 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + URL: + + + False + False + 0 + + + + + True + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True + False + False + True + True + + + True + True + 1 + + + + + False + False + 2 + + + + + True + True + 1 + + diff --git a/deluge/ui/gtkui/glade/connection_manager.glade b/deluge/ui/gtkui/glade/connection_manager.glade index af4129507..cd343c81e 100644 --- a/deluge/ui/gtkui/glade/connection_manager.glade +++ b/deluge/ui/gtkui/glade/connection_manager.glade @@ -1,40 +1,109 @@ - - - + + + + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Add Host True - GTK_WIN_POS_CENTER + center True - GDK_WINDOW_TYPE_HINT_DIALOG - False + dialog True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + end + + + gtk-cancel + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + False + False + 0 + + + + + gtk-add + 1 + True + True + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + False + False + 1 + + + + + gtk-save + 2 + True + True + False + True + + + False + False + 2 + + + + + False + True + end + 0 + + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Hostname: False False + 0 True + False 1 @@ -42,16 +111,23 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False + False + True + True + True + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Port: @@ -69,6 +145,10 @@ 5 5 1 + False + False + True + True 58846 0 65535 1 10 0 1 True @@ -89,17 +169,23 @@ True + False 2 2 True + False 5 True True False + False + False + True + True @@ -113,11 +199,16 @@ True + False 5 True True + False + False + True + True @@ -129,6 +220,7 @@ True + False Password: @@ -140,6 +232,7 @@ True + False Username: @@ -149,102 +242,176 @@ False + True 2 + + + + + False + 5 + Password Required + True + center-on-parent + 320 + True + dialog + True + + + True + False + 2 - + True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_END + False + end - - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-cancel - True - 0 - - - - - True - True - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-add - True + + gtk-connect 1 + True + True + True + False + True + - 1 + False + False + 0 False - GTK_PACK_END + True + end + 0 + + + + + True + False + + + True + False + gtk-dialog-authentication + 6 + + + True + True + 0 + + + + + True + True + False + + False + False + True + True + + + + True + True + 1 + + + + + True + True + 1 + False True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Connection Manager True - GTK_WIN_POS_CENTER_ON_PARENT + center-on-parent 350 300 True - GDK_WINDOW_TYPE_HINT_DIALOG - False + dialog True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 + + + False + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + end + + + False + False + end + 0 + + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-missing-image False False + 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <big><b>Connection Manager</b></big> True False + True 1 False + True 5 1 @@ -252,20 +419,22 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_RESIZE_QUEUE + queue True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC + automatic + automatic True @@ -277,63 +446,78 @@ + + True + True + 0 + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_START + start + gtk-add True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-add + False True - 0 + + False + False + 0 + + + + + gtk-edit + True + True + True + False + True + + + + False + False + 1 + + gtk-remove True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-remove + False True - 0 - 1 + False + False + 2 False False - - - - - True - True - True - gtk-refresh - True - 0 - - - - False - False - 2 + 0 @@ -342,28 +526,38 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-execute + + True + True + 0 + True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Start local daemon True + True + True 1 @@ -373,10 +567,26 @@ False False - GTK_PACK_END + end 1 + + + gtk-refresh + True + True + True + False + True + + + + False + False + 2 + + False @@ -386,6 +596,8 @@ + True + True 10 2 @@ -398,6 +610,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 5 @@ -406,43 +619,56 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + Automatically connect to selected host on start-up True True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Automatically connect to selected host on start-up - 0 + False True + + True + True + 0 + + Automatically start localhost if needed True True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Automatically start localhost if needed - 0 + False True + True + True 1 + Do not show this dialog on start-up True True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - Do not show this dialog on start-up - 0 + False True + True + True 2 @@ -453,6 +679,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Options @@ -463,37 +690,40 @@ False + True 3 True - GTK_BUTTONBOX_END + False + end + gtk-close True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-close + False True - -7 False False + 0 + gtk-connect True True True - gtk-connect + False True - 0 @@ -509,18 +739,6 @@ 4 - - - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_END - - - False - False - GTK_PACK_END - - diff --git a/deluge/ui/gtkui/glade/edit_trackers.glade b/deluge/ui/gtkui/glade/edit_trackers.glade index 2a6269196..14d802a21 100644 --- a/deluge/ui/gtkui/glade/edit_trackers.glade +++ b/deluge/ui/gtkui/glade/edit_trackers.glade @@ -1,16 +1,16 @@ - - - + + + 400 GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Edit Trackers - GTK_WIN_POS_CENTER_ON_PARENT + center-on-parent 400 True - GDK_WINDOW_TYPE_HINT_DIALOG + dialog False @@ -36,6 +36,7 @@ False False + 0 @@ -57,6 +58,7 @@ False False + 0 @@ -69,9 +71,9 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + automatic + automatic + in True @@ -80,81 +82,92 @@ + + 0 + True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 - GTK_BUTTONBOX_CENTER + center + gtk-go-up True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-go-up True - 0 + + False + False + 0 + + gtk-add True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-add True - 0 + False + False 1 + gtk-edit True True True - gtk-edit True - 0 + False + False 2 + gtk-remove True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-remove True - 0 + False + False 3 + gtk-go-down True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-go-down True - 0 + False + False 4 @@ -179,38 +192,45 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_END + end + gtk-cancel True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-cancel True - 0 + + False + False + 0 + + gtk-ok + 1 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-ok True - 0 + False + False 1 False - GTK_PACK_END + end + 0 @@ -222,9 +242,9 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Add Tracker - GTK_WIN_POS_CENTER_ON_PARENT + center-on-parent True - GDK_WINDOW_TYPE_HINT_DIALOG + dialog False False @@ -251,6 +271,7 @@ False False + 0 @@ -270,6 +291,7 @@ False False + 0 @@ -297,15 +319,16 @@ False + 0 True True - GTK_POLICY_AUTOMATIC - GTK_POLICY_AUTOMATIC - GTK_SHADOW_IN + automatic + automatic + in True @@ -334,40 +357,48 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_END + end + gtk-cancel + -6 True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-cancel True - -6 + + False + False + 0 + + gtk-ok + -5 True True True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-ok True - -5 + False + False 1 False - GTK_PACK_END + end + 0 @@ -378,9 +409,9 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Edit Tracker - GTK_WIN_POS_CENTER_ON_PARENT + center-on-parent True - GDK_WINDOW_TYPE_HINT_DIALOG + dialog False False @@ -407,6 +438,7 @@ False False + 0 @@ -426,6 +458,7 @@ False False + 0 @@ -452,6 +485,7 @@ False False + 0 @@ -483,40 +517,47 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - GTK_BUTTONBOX_END + end + gtk-cancel True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-cancel True - 0 + + False + False + 0 + + gtk-ok + 1 True True True True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-ok True - 0 + False + False 1 False - GTK_PACK_END + end + 0 diff --git a/deluge/ui/gtkui/glade/filtertree_menu.glade b/deluge/ui/gtkui/glade/filtertree_menu.glade index a981875f0..2e9ccdd15 100644 --- a/deluge/ui/gtkui/glade/filtertree_menu.glade +++ b/deluge/ui/gtkui/glade/filtertree_menu.glade @@ -23,7 +23,7 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - _Pause All + _Pause All True diff --git a/deluge/ui/gtkui/glade/main_window.glade b/deluge/ui/gtkui/glade/main_window.glade index c3fd1f6ad..000a6babd 100644 --- a/deluge/ui/gtkui/glade/main_window.glade +++ b/deluge/ui/gtkui/glade/main_window.glade @@ -1,8 +1,9 @@ - + - + + False Deluge @@ -10,79 +11,76 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False True + False + False _File True + False - _Add Torrent + _Add Torrent True False + False + False True False - - - True - gtk-add - 1 - - + - _Create Torrent + _Create Torrent True + False + False True False - - - True - gtk-new - 1 - - + - + + False + - Quit & _Shutdown Daemon + Quit & _Shutdown Daemon + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True False - - - True - gtk-quit - 1 - - + True + False gtk-quit True + False + False True True @@ -95,35 +93,37 @@ True + False + False _Edit True True + False gtk-preferences True + False + False True True + - _Connection Manager + _Connection Manager True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True False - - - True - gtk-network - 1 - - + @@ -132,6 +132,8 @@ + False + False _Torrent True @@ -139,14 +141,19 @@ True + False + False _View True True + False True + False + False _Toolbar True True @@ -156,7 +163,9 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False _Sidebar True True @@ -166,6 +175,8 @@ True + False + False Status_bar True True @@ -175,11 +186,14 @@ True + False True + False + False T_abs True @@ -187,23 +201,42 @@ True + False + False _Columns True + + + True + False + False + False + _Find ... + True + + + + True + False True + False S_idebar True True + False True True + False + False Show _Zero Hits True True @@ -213,6 +246,8 @@ True + False + False Show _Trackers True True @@ -230,72 +265,63 @@ True + False + False _Help True + False - _Homepage + _Homepage True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True False - - - True - gtk-home - 1 - - - _FAQ + _FAQ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Frequently Asked Questions + False True False - - - True - gtk-dialog-question - 1 - - + - _Community + _Community True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True False - - - True - 0.4699999988079071 - gtk-info - 1 - - True + False gtk-about True + False + False True True @@ -308,17 +334,22 @@ False + True 0 True + False True False + False + True Add torrent + False Add Torrent True gtk-add @@ -333,6 +364,10 @@ True False + False + True + Remove torrent + False Remove Torrent gtk-remove @@ -342,9 +377,29 @@ True
+ + + True + False + False + Filter torrents by name. +This will filter torrents for the current selection on the sidebar. + False + _Filter + True + gtk-find + + + + + False + True + + True + False False @@ -354,7 +409,10 @@ True False + False + True Pause the selected torrents + False Pause True gtk-media-pause @@ -369,8 +427,11 @@ True False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True Resume the selected torrents + False Resume gtk-media-play @@ -383,6 +444,7 @@ True + False False @@ -392,8 +454,11 @@ True False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True Queue Torrent Up + False Queue Up gtk-go-up @@ -407,8 +472,11 @@ True False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True Queue Torrent Down + False Queue Down gtk-go-down @@ -421,6 +489,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -430,7 +499,10 @@ True + False + True Preferences + False Preferences True gtk-preferences @@ -444,8 +516,11 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + True Connection Manager + False Connection Manager gtk-network @@ -458,18 +533,20 @@ False + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 True - vertical + False True @@ -486,24 +563,130 @@ - + True False - automatic - automatic - out - + + False + + + True + True + True + True + Close + False + none + + + + True + False + gtk-close + 2 + + + + + False + False + 0 + + + + + True + False + Filter: + + + False + False + 1 + 1 + + + + + 350 + True + True + True + Filter torrents by name. +This will filter torrents for the current selection on the sidebar. + + True + True + False + gtk-clear + True + True + False + True + Clear the search + Clear the search + + + + + False + False + 1 + 2 + + + + + _Match Case + True + True + False + False + True + True + + + + True + True + 1 + 3 + + + + + False + True + 0 + + + + True True - True - False + automatic + automatic + out + + + True + True + True + False + + + + True + True + 1 + True - False + True @@ -515,6 +698,7 @@ True + False False @@ -526,12 +710,15 @@ + True + True 2 True + False False @@ -544,10 +731,13 @@ True + False gtk-open True + False + False True True @@ -556,18 +746,22 @@ True + False - _Expand All + _Expand All True + False + False True False True + False gtk-zoom-fit 1 @@ -577,18 +771,22 @@ True + False - _Do Not Download + _Do Not Download True + False + False True False True + False gtk-no 1 @@ -597,14 +795,17 @@ - _Normal Priority + _Normal Priority True + False + False True False True + False gtk-yes 1 @@ -613,14 +814,17 @@ - _High Priority + _High Priority True + False + False True False True + False gtk-go-up 1 @@ -629,14 +833,17 @@ - Hi_ghest Priority + Hi_ghest Priority True + False + False True False True + False gtk-goto-top 1 @@ -644,7 +851,471 @@ + + True + False + + + _Add Peer + True + False + Add a peer by its IP + False + True + False + + + + True + False + gtk-add + 1 + + + + + + + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + Remove Torrent? + False + center-on-parent + dialog + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 2 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + center + + + gtk-cancel + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + False + False + 0 + + + + + Remove Selected Torrent + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + + + False + False + 1 + + + + + False + True + end + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 10 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-dialog-warning + 6 + + + False + False + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + <big><b>Are you sure you want to remove the selected torrent?</b></big> + True + True + + + False + False + 1 + + + + + False + False + 0 + + + + + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + False + True + 1 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 15 + + + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-dialog-warning + + + False + False + 1 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + <i>The associated .torrent will be deleted!</i> + True + + + True + True + 1 + + + + + + + True + True + 2 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 15 + + + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 5 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + gtk-dialog-warning + + + False + False + 0 + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + <i>The downloaded data will be deleted!</i> + True + + + True + True + 1 + + + + + + + True + True + 3 + + + + + False + False + 5 + 1 + + + + + + + False + 5 + New Release + center-on-parent + deluge + dialog + + + True + False + 2 + + + True + False + end + + + gtk-close + True + True + True + False + True + + + False + False + 0 + + + + + _Goto Website + True + True + True + False + True + + + + False + False + 1 + + + + + False + False + end + 0 + + + + + True + False + 10 + 5 + + + True + False + 5 + + + True + False + gtk-missing-image + + + False + False + 0 + + + + + True + False + <b><big>New Release Available!</big></b> + True + + + False + False + 1 + + + + + False + False + 0 + + + + + True + False + + + False + True + 1 + + + + + True + False + 5 + + + True + False + 2 + 2 + 10 + 2 + + + True + False + + + 1 + 2 + 1 + 2 + + + + + + True + False + 0 + <i>Available Version:</i> + True + + + 1 + 2 + GTK_FILL + + + + + True + False + + + 1 + 2 + + + + + + True + False + 0 + <i>Current Version:</i> + True + + + GTK_FILL + + + + + + + True + True + 2 + + + + + True + False + 5 + + + Do not show this dialog in the future + True + True + False + False + True + + + + + True + True + 3 + + + + + False + False + 1 + + + + + + False True @@ -659,12 +1330,14 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False 10 10 15 @@ -672,24 +1345,40 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 5 - + True - True - 0.10000000149 + False + + + True + False + True + 0.10000000149 + + + False + False + 0 + + + + + False - False + True 0 True + False 5 8 10 @@ -697,6 +1386,7 @@ True + False 0 @@ -710,6 +1400,7 @@ True + False 0 <b>Auto Managed:</b> True @@ -725,6 +1416,7 @@ True + False 0 @@ -738,6 +1430,7 @@ True + False 0 @@ -751,6 +1444,7 @@ True + False 0 <b>Seed Rank:</b> True @@ -766,6 +1460,7 @@ True + False 0 <b>Seeding Time:</b> True @@ -781,6 +1476,7 @@ True + False 0 @@ -792,6 +1488,7 @@ True + False 0 <b>Active Time:</b> True @@ -805,6 +1502,7 @@ True + False 0 True @@ -819,6 +1517,7 @@ True + False 0 @@ -832,6 +1531,7 @@ True + False 0 True char @@ -848,6 +1548,7 @@ True + False 0 0 <b>Tracker Status:</b> @@ -863,6 +1564,7 @@ True + False 0 True word-char @@ -878,6 +1580,7 @@ True + False 0 1 <b>Availability:</b> @@ -894,6 +1597,7 @@ True + False 0 @@ -907,6 +1611,7 @@ True + False 0 @@ -920,6 +1625,7 @@ True + False 0 @@ -933,6 +1639,7 @@ True + False 0 <b>Peers:</b> True @@ -948,6 +1655,7 @@ True + False 0 @@ -959,6 +1667,7 @@ True + False 0 <b>Seeders:</b> True @@ -972,11 +1681,13 @@ True + False 15 5 True + False 0 <b>Pieces:</b> True @@ -994,11 +1705,13 @@ True + False 15 5 True + False 0 <b>ETA:</b> True @@ -1016,11 +1729,13 @@ True + False 15 5 True + False 0 <b>Speed:</b> True @@ -1038,11 +1753,13 @@ True + False 15 5 True + False 0 <b>Speed:</b> True @@ -1058,10 +1775,12 @@ True + False 5 True + False 0 <b>Next Announce:</b> True @@ -1077,10 +1796,12 @@ True + False 5 True + False 0 <b>Share Ratio:</b> True @@ -1096,10 +1817,12 @@ True + False 5 True + False 0 <b>Uploaded:</b> True @@ -1115,10 +1838,12 @@ True + False 5 True + False 0 <b>Downloaded:</b> True @@ -1132,6 +1857,7 @@ True + False 0 @@ -1145,6 +1871,7 @@ True + False 0 @@ -1158,6 +1885,7 @@ True + False 0 @@ -1169,6 +1897,7 @@ True + False 0 @@ -1180,6 +1909,7 @@ True + False 0 <b>Date Added:</b> True @@ -1195,6 +1925,7 @@ True + False 0 @@ -1223,26 +1954,33 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-dialog-info + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Status True + True + True 1 @@ -1262,12 +2000,14 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 10 10 @@ -1276,6 +2016,7 @@ True + False 10 4 5 @@ -1283,6 +2024,7 @@ True + False 0 char True @@ -1298,6 +2040,7 @@ True + False 0 1 <b>Comments:</b> @@ -1313,6 +2056,7 @@ True + False 0 True @@ -1327,6 +2071,7 @@ True + False 0 1 <b># of files:</b> @@ -1342,6 +2087,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 True @@ -1359,6 +2105,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 <b>Hash:</b> @@ -1374,6 +2121,7 @@ True + False 0 char True @@ -1389,6 +2137,7 @@ True + False 0 1 <b>Tracker:</b> @@ -1404,11 +2153,13 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False 0 1 <b>Total Size:</b> @@ -1426,6 +2177,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_STRUCTURE_MASK 0 True @@ -1441,11 +2193,13 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False 0 0 1 @@ -1462,11 +2216,13 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 <b>Path:</b> @@ -1484,6 +2240,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 True @@ -1501,6 +2258,7 @@ True + False 0 1 <b>Status:</b> @@ -1516,6 +2274,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 True @@ -1531,6 +2290,7 @@ True + False 0 True @@ -1545,6 +2305,7 @@ True + False 0 1 <b>Owner:</b> @@ -1560,9 +2321,11 @@ True + False + Torrent is shared between other Deluge users or not. 0 1 - <b>Public:</b> + <b>Shared:</b> True @@ -1575,6 +2338,7 @@ True + False 0 char True @@ -1588,32 +2352,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + True + False + Torrent is shared between other Deluge users or not. 0 char True @@ -1626,6 +2368,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -1640,26 +2406,33 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-properties + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Details True + True + True 1 @@ -1692,26 +2465,33 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-copy + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Files True + True + True 1 @@ -1744,26 +2524,33 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK gtk-network + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK _Peers True + True + True 1 @@ -1783,24 +2570,29 @@ True + False 5 queue none True + False True + False 0 none True + False 12 True + False 5 4 3 @@ -1811,7 +2603,12 @@ True 6 1 - -1 -1 999999 1 10 0 + False + False + True + True + 0 -1 999999 1 10 0 + 1 @@ -1828,8 +2625,13 @@ True 6 1 - -1 -1 99999 1 10 0 + False + False + True + True + 0 -1 99999 1 10 0 1 + 1 @@ -1846,8 +2648,13 @@ True 6 1 - -1 -1 999999 1 10 0 + False + False + True + True + 0 -1 999999 1 10 0 1 + 1 @@ -1859,6 +2666,7 @@ True + False 0 Max Connections: @@ -1872,6 +2680,7 @@ True + False 0 Max Upload Speed: @@ -1885,6 +2694,7 @@ True + False 0 Max Download Speed: @@ -1896,6 +2706,7 @@ True + False KiB/s @@ -1908,6 +2719,7 @@ True + False KiB/s @@ -1922,6 +2734,7 @@ True + False 0 Max Upload Slots: @@ -1938,7 +2751,12 @@ True 6 1 - -1 -1 999999 1 10 0 + False + False + True + True + 0 -1 999999 1 10 0 + 1 @@ -1962,6 +2780,7 @@ True + False <b>Bandwidth</b> True @@ -1979,24 +2798,28 @@ True + False 0 none True + False 5 12 True - vertical + False Auto Managed True True False + False True + False @@ -2007,10 +2830,11 @@ True - vertical + False True + False 5 @@ -2018,6 +2842,7 @@ True True False + False True @@ -2032,9 +2857,14 @@ True True 1 + False + False + True + True 2 0 99999 0.10000000000000001 10 0 1 True + False @@ -2052,6 +2882,7 @@ True + False 10 @@ -2059,7 +2890,9 @@ True True False + False True + @@ -2075,6 +2908,7 @@ True True False + False True @@ -2087,13 +2921,16 @@ True + False True False - False + False select-folder + False Select A Folder + False @@ -2105,6 +2942,10 @@ False True + False + False + True + True False @@ -2121,6 +2962,8 @@ + True + True 1 @@ -2131,6 +2974,7 @@ True + False <b>Queue</b> True @@ -2148,15 +2992,17 @@ True - vertical + False True + False 0 none True + False 0 0 5 @@ -2164,7 +3010,7 @@ True - vertical + False Private @@ -2172,7 +3018,10 @@ False True False + If checked this torrent won't be shared among trackers, DHT nodes, etc... + False True + False @@ -2186,7 +3035,9 @@ True True False + False True + False @@ -2194,21 +3045,64 @@ 1 + + + Sequential Download + True + True + False + True + When enabled, the piece picker will pick pieces in +sequence instead of rarest first. + +Enabling sequential download will affect the piece +distribution negatively in the swarm. It should be +used sparingly. + False + True + + + + True + True + 2 + + + + + Shared + True + True + False + Torrent is shared between other Deluge users or not. + False + True + + + + True + True + 3 + + True True True + False 0 0 True + False 5 True + False gtk-edit @@ -2220,6 +3114,7 @@ True + False _Edit Trackers True @@ -2235,7 +3130,7 @@ False False - 2 + 4 @@ -2245,6 +3140,7 @@ True + False <b>General</b> True @@ -2262,11 +3158,13 @@ True + False 0 none True + False 0 0 12 @@ -2274,8 +3172,10 @@ gtk-apply True + False True True + False True @@ -2314,23 +3214,30 @@ True + False 2 True + False gtk-preferences + True + True 0 True + False _Options True + True + True 1 @@ -2344,418 +3251,4 @@ - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - Remove Torrent? - False - center-on-parent - dialog - False - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 2 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical - 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 10 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-dialog-warning - 6 - - - False - False - 0 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - <big><b>Are you sure you want to remove the selected torrent?</b></big> - True - True - - - False - False - 1 - - - - - False - False - 0 - - - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - False - 1 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 15 - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-dialog-warning - - - False - False - 1 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - <i>The associated .torrent will be deleted!</i> - True - - - 1 - - - - - - - 2 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 15 - - - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 5 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-dialog-warning - - - False - False - 0 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 0 - <i>The downloaded data will be deleted!</i> - True - - - 1 - - - - - - - 3 - - - - - False - False - 5 - 1 - - - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - center - - - gtk-cancel - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - False - False - 0 - - - - - Remove Selected Torrent - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - False - False - 1 - - - - - False - end - 0 - - - - - - - 5 - New Release - center-on-parent - deluge - dialog - False - - - True - vertical - 2 - - - True - 10 - vertical - 5 - - - True - 5 - - - True - gtk-missing-image - - - False - False - 0 - - - - - True - <b><big>New Release Available!</big></b> - True - - - False - False - 1 - - - - - False - False - 0 - - - - - True - - - False - 1 - - - - - True - 5 - - - True - 2 - 2 - 10 - 2 - - - True - - - 1 - 2 - 1 - 2 - - - - - - True - 0 - <i>Available Version:</i> - True - - - 1 - 2 - GTK_FILL - - - - - True - - - 1 - 2 - - - - - - True - 0 - <i>Current Version:</i> - True - - - GTK_FILL - - - - - - - 2 - - - - - True - 5 - - - Do not show this dialog in the future - True - True - False - True - - - - - 3 - - - - - False - False - 1 - - - - - True - end - - - gtk-close - True - True - True - True - - - False - False - 0 - - - - - _Goto Website - True - True - True - True - - - - False - False - 1 - - - - - False - False - end - 0 - - - - - - - True - - - _Add Peer - True - Add a peer by its IP - True - False - - - - True - gtk-add - 1 - - - - -
diff --git a/deluge/ui/gtkui/glade/preferences_dialog.glade b/deluge/ui/gtkui/glade/preferences_dialog.glade index d4cdc6115..6f1b7c7ac 100644 --- a/deluge/ui/gtkui/glade/preferences_dialog.glade +++ b/deluge/ui/gtkui/glade/preferences_dialog.glade @@ -1,8 +1,9 @@ - + - + + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 Preferences @@ -11,14 +12,78 @@ 530 True dialog - False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 2 + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + end + + + gtk-cancel + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + False + False + 0 + + + + + gtk-apply + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + False + False + 1 + + + + + gtk-ok + True + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + True + + + + False + False + 2 + + + + + False + True + end + 0 + + True @@ -27,6 +92,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue @@ -50,7 +116,7 @@ False True - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -59,17 +125,19 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -79,62 +147,57 @@ False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False 0 none True + False 2 2 12 True + False 5 2 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - select-folder - - - 0 - + - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - - - 1 - + @@ -149,26 +212,37 @@ True + False True + False 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK select-folder Select A Folder + True + True 0 True + False + False + True + True + True + True 1 @@ -182,28 +256,13 @@ 2 - - - Auto add .torrents from: - True - True - False - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - 2 - 3 - GTK_FILL - - Move completed to: True True False + False True @@ -216,17 +275,21 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True False + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK select-folder Select A Folder + True + True 0 @@ -234,8 +297,14 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True + True + True 1 @@ -248,6 +317,7 @@ True + False 0 Download to: @@ -261,6 +331,7 @@ True True False + False True @@ -273,20 +344,25 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK select-folder Select A Folder + True + True 0 @@ -294,9 +370,15 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - + + False + False + True + True + True + True 1 @@ -317,6 +399,7 @@ True False Delete the copy of the torrent file created when the torrent is removed + False True @@ -326,6 +409,9 @@ 15 + + + @@ -333,6 +419,7 @@ True + False <b>Folders</b> True @@ -351,17 +438,20 @@ True + False 0 none True + False 2 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 10 @@ -372,8 +462,10 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Full allocation preallocates all of the space that is needed for the torrent and prevents disk fragmentation + False True True + False @@ -389,8 +481,10 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Compact allocation only allocates space as needed + False True radio_full_allocation + False @@ -405,6 +499,7 @@ True + False <b>Allocation</b> True @@ -423,17 +518,20 @@ True + False 0 none True + False 2 2 12 True + False Prioritize first and last pieces of torrent @@ -442,23 +540,49 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Prioritize first and last pieces of files in torrent + False True False + True 0 + + + Sequential download + True + True + False + When enabled, the piece picker will pick pieces in +sequence instead of rarest first. + +Enabling sequential download will affect the piece +distribution negatively in the swarm. It should be +used sparingly. + False + True + + + True + True + 1 + + Add torrents in Paused state True True False + False True - 1 + True + True + 2 @@ -468,6 +592,7 @@ True + False <b>Options</b> True @@ -492,6 +617,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK page 6 @@ -501,7 +627,7 @@ - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -510,17 +636,19 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -530,37 +658,43 @@ False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False 0 none True + False 2 2 12 True - vertical + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 20 @@ -571,11 +705,13 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Deluge will automatically choose a different port to use every time. + False True False + True 5 0 @@ -583,16 +719,19 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False 1 Active Port: right False + True 5 0 @@ -600,12 +739,14 @@ True + False 0 0000 5 False + True 5 1 @@ -613,12 +754,15 @@ False + True 5 1 + True + True 5 0 @@ -626,13 +770,16 @@ True + False True + False From: False + True 0 @@ -643,6 +790,10 @@ True 5 1 + False + False + True + True 0 0 65535 1 10 0 1 True @@ -650,6 +801,7 @@ False + True 5 1 @@ -657,6 +809,7 @@ True + False 5 To: @@ -673,6 +826,10 @@ True 5 1 + False + False + True + True 0 0 65535 1 10 0 1 True @@ -680,6 +837,7 @@ False + True 5 3 @@ -691,6 +849,7 @@ True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False @@ -702,20 +861,25 @@ True + False 5 + False gtk-missing-image False + True 5 + True + True 5 1 @@ -727,6 +891,7 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Incoming Ports</b> True @@ -738,6 +903,7 @@ False + True 5 2 @@ -745,17 +911,19 @@ True + False 0 none True + False 5 12 True - vertical + False 5 @@ -763,6 +931,7 @@ True True False + False True @@ -775,10 +944,12 @@ True + False 5 True + False From: @@ -794,6 +965,10 @@ True 5 1 + False + False + True + True 0 0 65535 1 10 0 1 True @@ -801,6 +976,7 @@ False + True 5 1 @@ -808,6 +984,7 @@ True + False To: @@ -823,6 +1000,10 @@ True 5 1 + False + False + True + True 0 0 65535 1 10 0 1 True @@ -830,12 +1011,15 @@ False + True 5 3 + True + True 1 @@ -846,6 +1030,7 @@ True + False <b>Outgoing Ports</b> True @@ -863,25 +1048,32 @@ True + False 0 none True + False 2 2 12 True + False True True Enter the IP address of the interface to listen for incoming bittorrent connections on. Leave this empty if you want to use the default. 60 - + 30 + False + False + True + True False @@ -899,6 +1091,7 @@ True + False <b>Interface</b> True @@ -916,25 +1109,29 @@ True + False 0 none True + False 2 2 12 True - vertical + False True + False 5 True + False The TOS byte set in the IP header of every packet sent to peers (including web seeds). Expects a Hex value. Peer TOS Byte: @@ -950,6 +1147,10 @@ True 4 0x00 + False + False + True + True False @@ -959,6 +1160,8 @@ + True + True 0 @@ -969,6 +1172,7 @@ True + False <b>TOS</b> True @@ -979,6 +1183,7 @@ False + True 5 5 @@ -986,17 +1191,20 @@ True + False 0 none True + False 2 2 12 True + False 2 3 5 @@ -1007,6 +1215,7 @@ True False Universal Plug and Play + False True True True @@ -1022,6 +1231,7 @@ True False NAT Port Mapping Protocol + False True True True @@ -1039,6 +1249,7 @@ True False Peer Exchange + False True True True @@ -1057,6 +1268,7 @@ False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Local Service Discovery finds local peers on your network. + False True @@ -1072,6 +1284,7 @@ True False Distributed hash table may improve the amount of active connections. + False True True @@ -1093,6 +1306,7 @@ True + False <b>Network Extras</b> True @@ -1103,6 +1317,7 @@ False + True 5 6 @@ -1110,41 +1325,50 @@ True + False 0 none True + False 2 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False 1 Inbound: + True + True 0 True + False 0 Level: + True + True 1 @@ -1158,27 +1382,33 @@ True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False Forced Enabled Disabled + True + True 0 True + False Handshake Full Stream Either + True + True 1 @@ -1192,15 +1422,17 @@ Either True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False 1 Outbound: @@ -1213,17 +1445,21 @@ Either True + False Forced Enabled Disabled False + True 1 + True + True 0 @@ -1233,17 +1469,21 @@ Disabled True True False + False True True False + True 3 1 + True + True 2 @@ -1254,6 +1494,7 @@ Disabled True + False <b>Encryption</b> True @@ -1281,6 +1522,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK page 7 @@ -1291,7 +1533,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -1300,17 +1542,19 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -1320,38 +1564,44 @@ Disabled False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False 0 none True + False 2 2 12 True - vertical + False 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 6 2 @@ -1361,7 +1611,11 @@ Disabled True True 1 - -1 -1 9999 1 10 0 + False + False + True + True + 0 -1 9999 1 10 0 True @@ -1377,7 +1631,11 @@ Disabled True True 1 - -1 -1 9999 1 10 0 + False + False + True + True + 0 -1 9999 1 10 0 True @@ -1391,6 +1649,7 @@ Disabled True + False 0 Maximum Connection Attempts per Second: @@ -1403,6 +1662,7 @@ Disabled True + False 0 Maximum Half-Open Connections: @@ -1415,6 +1675,7 @@ Disabled True + False The maximum upload speed for all torrents. Set -1 for unlimited. 0 Maximum Upload Speed (KiB/s): @@ -1428,6 +1689,7 @@ Disabled True + False The maximum number of connections allowed. Set -1 for unlimited. 0 Maximum Connections: @@ -1439,6 +1701,7 @@ Disabled True + False The maximum upload slots for all torrents. Set -1 for unlimited. 0 Maximum Upload Slots: @@ -1456,7 +1719,11 @@ Disabled The maximum number of connections allowed. Set -1 for unlimited. 4 1 - -1 -1 9000 1 10 0 + False + False + True + True + 0 -1 9000 1 10 0 1 True True @@ -1471,6 +1738,7 @@ Disabled True + False The maximum download speed for all torrents. Set -1 for unlimited. 0 Maximum Download Speed (KiB/s): @@ -1488,7 +1756,11 @@ Disabled GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK The maximum download speed for all torrents. Set -1 for unlimited. 1 - -1 -1 60000 1 10 0 + False + False + True + True + 0 -1 60000 1 10 0 1 1 True @@ -1507,7 +1779,11 @@ Disabled True The maximum upload speed for all torrents. Set -1 for unlimited. 1 - -1 -1 60000 1 10 0 + False + False + True + True + 0 -1 60000 1 10 0 1 1 True @@ -1526,7 +1802,11 @@ Disabled True The maximum upload slots for all torrents. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + False + False + True + True + 0 -1 9000 1 10 0 1 True True @@ -1541,12 +1821,15 @@ Disabled + True + True 0 True + False 5 @@ -1554,18 +1837,22 @@ Disabled True True False + False True True + True + True 1 True + False 5 @@ -1574,12 +1861,15 @@ Disabled True False If checked, the estimated TCP/IP overhead is drained from the rate limiters, to avoid exceeding the limits with the total traffic + False True True + True + True 2 @@ -1590,6 +1880,7 @@ Disabled True + False <b>Global Bandwidth Usage</b> True @@ -1608,12 +1899,14 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 2 @@ -1621,6 +1914,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 4 2 @@ -1631,7 +1925,11 @@ Disabled True The maximum upload slots per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + False + False + True + True + 0 -1 9000 1 10 0 1 True True @@ -1650,7 +1948,11 @@ Disabled True The maximum number of connections per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + False + False + True + True + 0 -1 9000 1 10 0 True True @@ -1663,6 +1965,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Maximum Connections: @@ -1674,6 +1977,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Maximum Upload Slots: @@ -1687,6 +1991,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Maximum Download Speed (KiB/s): @@ -1700,6 +2005,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Maximum Upload Speed (KiB/s): @@ -1716,7 +2022,11 @@ Disabled True The maximum number of connections per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + False + False + True + True + 0 -1 9000 1 10 0 1 True @@ -1734,7 +2044,11 @@ Disabled True The maximum number of connections per torrent. Set -1 for unlimited. 1 - -1 -1 9000 1 10 0 + False + False + True + True + 0 -1 9000 1 10 0 1 True @@ -1753,6 +2067,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Per Torrent Bandwidth Usage</b> True @@ -1764,6 +2079,7 @@ Disabled False + True 5 3 @@ -1780,6 +2096,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK page 8 @@ -1790,7 +2107,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -1799,17 +2116,19 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -1819,28 +2138,33 @@ Disabled False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 2 @@ -1853,6 +2177,7 @@ Disabled False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Classic Mode will hide most of the daemon functionality and will make Deluge appear to be a single application. Use this if you do not want to take advantage of running Deluge as a daemon. You need to restart Deluge for this setting to take effect. + False True @@ -1861,6 +2186,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Classic Mode</b> True @@ -1880,19 +2206,21 @@ Disabled True + False 0 none True + False 2 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Show session speed in titlebar @@ -1900,12 +2228,283 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + True + True 0 + + + True + True + False + Besides being experimental, using the pieces bar +will increase the bandwidth used between client +and daemon(does not apply in classic mode). +Use at your own risk if you wish to help us debug +this new feature. + False + True + + + + True + False + Show a pieces bar in the torrent's +status tab (<b>EXPERIMENTAL!!!</b>) + True + + + + + True + True + 1 + + + + + True + + + True + False + 25 + + + True + False + 4 + 3 + 5 + 1 + + + True + False + 1 + Completed: + + + GTK_FILL + + + + + True + True + True + False + 0 + #000000000000 + + + + 1 + 2 + + + + + + True + False + 1 + Downloading: + + + 1 + 2 + GTK_FILL + + + + + True + True + True + False + 0 + #000000000000 + + + + 1 + 2 + 1 + 2 + + + + + + True + False + 1 + Waiting: + + + 2 + 3 + GTK_FILL + + + + + True + True + True + False + 0 + #000000000000 + + + + 1 + 2 + 2 + 3 + + + + + + True + False + 1 + Missing: + + + 3 + 4 + GTK_FILL + + + + + True + True + True + False + 0 + #000000000000 + + + + 1 + 2 + 3 + 4 + + + + + + gtk-revert-to-saved + True + False + True + True + Revert color to default + False + True + right + + + + 2 + 3 + + + + + + gtk-revert-to-saved + True + False + True + True + Revert color to default + False + True + right + + + + 2 + 3 + 1 + 2 + + + + + + gtk-revert-to-saved + True + False + True + True + Revert color to default + False + True + right + + + + 2 + 3 + 2 + 3 + + + + + + gtk-revert-to-saved + True + False + True + True + Revert color to default + False + True + right + + + + 2 + 3 + 3 + 4 + + + + + + + + + + True + False + Piece Colors + + + label_item + + + + + False + False + 2 + + @@ -1913,6 +2512,7 @@ Disabled True + False <b>Main Window</b> True @@ -1931,19 +2531,21 @@ Disabled True + False 0 none True + False 2 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Always show @@ -1951,16 +2553,20 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -1969,11 +2575,14 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + True + True 1 @@ -1984,6 +2593,7 @@ Disabled True + False <b>Add Torrents Dialog</b> True @@ -2002,35 +2612,41 @@ Disabled True + False 0 none True + False 2 2 12 True - vertical + False Enable system tray icon True False False + False True True + True + True 0 True + False 10 @@ -2039,18 +2655,22 @@ Disabled False False False + False True True + True + True 1 True + False 10 @@ -2059,18 +2679,46 @@ Disabled False False False + False True True + True + True 2 + + + True + False + 10 + + + Enable Application Indicator + True + False + False + False + False + True + True + + + + + True + True + 3 + + True + False 3 10 @@ -2080,6 +2728,7 @@ Disabled False True False + False True True @@ -2088,27 +2737,32 @@ Disabled False - 3 + True + 4 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 20 True + False 5 True False + False 0 Password: False + True 0 @@ -2121,9 +2775,14 @@ Disabled False 16 ******** + False + False + True + True False + True 1 @@ -2131,7 +2790,9 @@ Disabled - 4 + True + True + 5 @@ -2141,6 +2802,7 @@ Disabled True + False <b>System Tray</b> True @@ -2151,6 +2813,7 @@ Disabled False + True 5 5 @@ -2167,6 +2830,7 @@ Disabled True + False page 10 @@ -2176,7 +2840,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2185,17 +2849,19 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -2205,39 +2871,45 @@ Disabled False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2247,6 +2919,7 @@ Disabled False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Deluge will check our servers and will tell you if a newer version has been released + False True @@ -2264,6 +2937,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Updates</b> True @@ -2283,23 +2957,26 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Help us improve Deluge by sending us your Python version, PyGTK version, OS and processor types. Absolutely no other information is sent. @@ -2315,6 +2992,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 10 @@ -2324,12 +3002,14 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True False + True 1 @@ -2340,6 +3020,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>System Information</b> True @@ -2359,31 +3040,36 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK True + False 5 True + False Location: @@ -2397,9 +3083,15 @@ Disabled True True If Deluge cannot find the database file at this location it will fallback to using DNS to resolve the peer's country. - + + False + False + True + True + True + True 1 @@ -2419,6 +3111,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>GeoIP Database</b> True @@ -2438,36 +3131,46 @@ Disabled True + False 12 True + False start True True True + False True + False 2 True + False gtk-missing-image + True + True 0 True + False Associate Magnet links with Deluge + True + True 1 @@ -2501,6 +3204,7 @@ Disabled True + False page 11 @@ -2510,7 +3214,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2519,17 +3223,19 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -2539,44 +3245,51 @@ Disabled False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 2 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Daemon port: @@ -2592,6 +3305,10 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 + False + False + True + True 0 0 65535 1 10 0 @@ -2614,6 +3331,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Port</b> True @@ -2633,12 +3351,14 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 10 @@ -2649,6 +3369,7 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -2657,6 +3378,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Connections</b> True @@ -2676,12 +3398,14 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 10 @@ -2692,6 +3416,7 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -2700,6 +3425,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Other</b> True @@ -2716,6 +3442,124 @@ Disabled 4 + + + True + False + 0 + none + + + True + False + 12 + + + True + False + + + True + True + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + + + True + True + 0 + + + + + True + False + 5 + True + start + + + gtk-add + True + True + True + False + True + + + + False + False + 0 + + + + + gtk-edit + True + False + True + True + False + True + + + + False + False + 1 + + + + + gtk-delete + True + False + True + True + False + True + + + + False + False + 2 + + + + + False + True + 4 + 1 + + + + + + + + + True + False + GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + 0 + 10 + 10 + <b>Accounts</b> + True + + + label_item + + + + + True + True + 5 + + @@ -2728,6 +3572,7 @@ Disabled True + False page 11 @@ -2737,7 +3582,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -2746,17 +3591,19 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -2766,42 +3613,47 @@ Disabled False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical Queue new torrents to top @@ -2809,9 +3661,12 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + True + True 0 @@ -2822,6 +3677,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>General</b> True @@ -2840,23 +3696,26 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 True - vertical + False 5 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 3 2 @@ -2867,6 +3726,10 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 + False + False + True + True 0 -1 9999 1 10 0 True True @@ -2883,6 +3746,10 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 + False + False + True + True 0 -1 9999 1 10 0 True True @@ -2898,6 +3765,7 @@ Disabled True + False 0 Total active seeding: @@ -2910,6 +3778,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Total active: @@ -2924,6 +3793,10 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 + False + False + True + True 0 -1 9999 1 10 0 True True @@ -2939,6 +3812,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Total active downloading: @@ -2951,6 +3825,8 @@ Disabled + True + True 0 @@ -2960,9 +3836,12 @@ Disabled True True False + False True + True + True 1 @@ -2973,6 +3852,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Active Torrents</b> True @@ -2991,30 +3871,34 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical 2 True + False 3 2 10 True + False 0 Share Ratio Limit: @@ -3025,6 +3909,7 @@ Disabled True + False 0 Seed Time Ratio: @@ -3037,6 +3922,7 @@ Disabled True + False 0 Seed Time (m): @@ -3052,6 +3938,10 @@ Disabled True 6 1 + False + False + True + True 1.5 -1 100 0.10000000000000001 10 0 2 @@ -3067,6 +3957,10 @@ Disabled True 6 1 + False + False + True + True 6 -1 100 0.10000000000000001 10 0 2 @@ -3084,6 +3978,10 @@ Disabled True 6 1 + False + False + True + True 6 -1 10000 1 10 0 @@ -3096,12 +3994,15 @@ Disabled + True + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 @@ -3111,6 +4012,7 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True @@ -3127,6 +4029,10 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 1 + False + False + True + True 2 0.5 100 0.10000000000000001 1 0 2 True @@ -3147,6 +4053,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 10 @@ -3157,11 +4064,14 @@ Disabled True False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False True + True + True 2 @@ -3172,6 +4082,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK <b>Seeding</b> True @@ -3189,6 +4100,8 @@ Disabled + True + True 5 2 @@ -3205,6 +4118,7 @@ Disabled True + False page 12 @@ -3214,7 +4128,7 @@ Disabled - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -3223,17 +4137,19 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -3243,36 +4159,42 @@ Disabled False + True 0 True + False False + True 1 True - vertical + False 5 True + False 0 none True + False 12 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 2 @@ -3280,6 +4202,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Password: @@ -3296,6 +4219,10 @@ Disabled True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + False + False + True + True 1 @@ -3308,6 +4235,7 @@ Disabled True + False 0 Host: @@ -3322,6 +4250,10 @@ Disabled True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3333,6 +4265,7 @@ Disabled True + False 0 Port: @@ -3345,6 +4278,7 @@ Disabled True + False 0 0 @@ -3352,7 +4286,11 @@ Disabled True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + False + False + True + True + 100 0 65535 1 10 0 True @@ -3370,6 +4308,10 @@ Disabled True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3382,6 +4324,7 @@ Disabled True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK None Socksv4 @@ -3401,6 +4344,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Type: @@ -3413,6 +4357,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Username: @@ -3430,6 +4375,7 @@ HTTP W/ Auth True + False <b>Peer</b> True @@ -3447,16 +4393,19 @@ HTTP W/ Auth True + False 0 none True + False 12 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 2 @@ -3464,6 +4413,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Password: @@ -3480,6 +4430,10 @@ HTTP W/ Auth True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + False + False + True + True 1 @@ -3492,6 +4446,7 @@ HTTP W/ Auth True + False 0 Host: @@ -3506,6 +4461,10 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3517,6 +4476,7 @@ HTTP W/ Auth True + False 0 Port: @@ -3529,6 +4489,7 @@ HTTP W/ Auth True + False 0 0 @@ -3536,7 +4497,11 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + False + False + True + True + 100 0 65535 1 10 0 True @@ -3554,6 +4519,10 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3566,6 +4535,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK None Socksv4 @@ -3585,6 +4555,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Type: @@ -3597,6 +4568,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Username: @@ -3614,6 +4586,7 @@ HTTP W/ Auth True + False <b>Web Seed</b> True @@ -3631,16 +4604,19 @@ HTTP W/ Auth True + False 0 none True + False 12 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 2 @@ -3648,6 +4624,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Password: @@ -3664,6 +4641,10 @@ HTTP W/ Auth True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + False + False + True + True 1 @@ -3676,6 +4657,7 @@ HTTP W/ Auth True + False 0 Host: @@ -3690,6 +4672,10 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3701,6 +4687,7 @@ HTTP W/ Auth True + False 0 Port: @@ -3713,6 +4700,7 @@ HTTP W/ Auth True + False 0 0 @@ -3720,7 +4708,11 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + False + False + True + True + 100 0 65535 1 10 0 True @@ -3738,6 +4730,10 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3750,6 +4746,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK None Socksv4 @@ -3769,6 +4766,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Type: @@ -3781,6 +4779,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Username: @@ -3798,6 +4797,7 @@ HTTP W/ Auth True + False <b>Tracker</b> True @@ -3815,16 +4815,19 @@ HTTP W/ Auth True + False 0 none True + False 12 12 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 5 2 @@ -3832,6 +4835,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Password: @@ -3849,6 +4853,10 @@ HTTP W/ Auth True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + False + False + True + True 1 @@ -3861,6 +4869,7 @@ HTTP W/ Auth True + False 0 Host: @@ -3876,6 +4885,10 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3887,6 +4900,7 @@ HTTP W/ Auth True + False 0 Port: @@ -3900,6 +4914,7 @@ HTTP W/ Auth True + False 0 0 @@ -3907,7 +4922,11 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - 8080 0 65535 1 10 0 + False + False + True + True + 100 0 65535 1 10 0 True @@ -3925,6 +4944,10 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK + False + False + True + True 1 @@ -3937,6 +4960,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK None Socksv4 @@ -3956,6 +4980,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Type: @@ -3968,6 +4993,7 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 Username: @@ -3986,6 +5012,7 @@ HTTP W/ Auth True + False <b>DHT</b> True @@ -4002,6 +5029,8 @@ HTTP W/ Auth + True + True 5 2 @@ -4022,7 +5051,7 @@ HTTP W/ Auth - + True True automatic @@ -4030,15 +5059,17 @@ HTTP W/ Auth True + False queue none True - vertical + False True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -4048,45 +5079,53 @@ HTTP W/ Auth False + True 0 True + False False + True 1 True + False 12 True - vertical + False True + False 0 none True + False 5 12 True + False 2 2 5 True + False 0 Cache Size (16 KiB blocks): @@ -4097,6 +5136,7 @@ HTTP W/ Auth True + False True The number of seconds from the last cached write to a piece in the write cache, to when it's forcefully flushed to disk. Default is 60 seconds. 0 @@ -4112,9 +5152,13 @@ HTTP W/ Auth True True - + 1 - 512 0 99999 1 10 0 + False + False + True + True + 100 0 99999 1 10 0 True if-valid @@ -4129,9 +5173,13 @@ HTTP W/ Auth True True 5 - + 5 1 + False + False + True + True 60 1 32000 1 10 0 @@ -4149,6 +5197,7 @@ HTTP W/ Auth True + False <b>Settings</b> True @@ -4167,35 +5216,41 @@ HTTP W/ Auth True + False 0 none True + False 5 12 True - vertical + False True + False 0 none True + False 12 True + False 3 2 5 True + False The total number of 16 KiB blocks written to disk since this session was started. 0 Blocks Written: @@ -4207,6 +5262,7 @@ HTTP W/ Auth True + False The total number of write operations performed since this session was started. 0 Writes: @@ -4220,6 +5276,7 @@ HTTP W/ Auth True + False The ratio (blocks_written - writes) / blocks_written represents the number of saved write operations per total write operations, i.e. a kind of cache hit ratio for the write cache. 0 Write Cache Hit Ratio: @@ -4233,6 +5290,7 @@ HTTP W/ Auth True + False 1 @@ -4244,6 +5302,7 @@ HTTP W/ Auth True + False 1 @@ -4257,6 +5316,7 @@ HTTP W/ Auth True + False 1 @@ -4274,6 +5334,7 @@ HTTP W/ Auth True + False <b>Write</b> True @@ -4283,27 +5344,33 @@ HTTP W/ Auth + True + True 0 True + False 0 none True + False 12 True + False 4 2 5 True + False The number of blocks that were requested from the bittorrent engine (from peers), that were served from disk or cache. 0 Blocks Read: @@ -4315,6 +5382,7 @@ HTTP W/ Auth True + False The number of blocks that were served from cache. 0 Blocks Read Hit: @@ -4328,6 +5396,7 @@ HTTP W/ Auth True + False The cache hit ratio for the read cache. 0 Read Cache Hit Ratio: @@ -4341,6 +5410,7 @@ HTTP W/ Auth True + False 1 @@ -4352,6 +5422,7 @@ HTTP W/ Auth True + False 1 @@ -4365,6 +5436,7 @@ HTTP W/ Auth True + False 1 @@ -4378,6 +5450,7 @@ HTTP W/ Auth True + False True The total number of read operations performed since this session was started. 0 @@ -4392,6 +5465,7 @@ HTTP W/ Auth True + False 1 @@ -4408,6 +5482,7 @@ HTTP W/ Auth True + False <b>Read</b> True @@ -4417,27 +5492,33 @@ HTTP W/ Auth + True + True 1 True + False 0 none True + False 12 True + False 2 2 5 True + False The number of 16 KiB blocks currently in the disk cache. This includes both read and write cache. 0 Cache Size: @@ -4449,6 +5530,7 @@ HTTP W/ Auth True + False 0 Read Cache Size: @@ -4461,6 +5543,7 @@ HTTP W/ Auth True + False 1 @@ -4472,6 +5555,7 @@ HTTP W/ Auth True + False 1 @@ -4489,6 +5573,7 @@ HTTP W/ Auth True + False <b>Size</b> True @@ -4498,12 +5583,15 @@ HTTP W/ Auth + True + True 2 True + False start @@ -4511,6 +5599,7 @@ HTTP W/ Auth True True True + False True @@ -4522,6 +5611,8 @@ HTTP W/ Auth + True + True 3 @@ -4532,6 +5623,7 @@ HTTP W/ Auth True + False <b>Status</b> True @@ -4551,6 +5643,8 @@ HTTP W/ Auth + True + True 2 @@ -4570,7 +5664,7 @@ HTTP W/ Auth - + True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK @@ -4579,17 +5673,19 @@ HTTP W/ Auth True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK queue none True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 0 10 @@ -4599,16 +5695,19 @@ HTTP W/ Auth False + True 0 True + False GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK False + True 1 @@ -4617,7 +5716,6 @@ HTTP W/ Auth True True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - vertical True @@ -4647,26 +5745,31 @@ HTTP W/ Auth True + False queue none True + False 0 none True + False 12 True + False 5 2 5 True + False 0 @@ -4680,6 +5783,7 @@ HTTP W/ Auth True + False 0 @@ -4693,6 +5797,7 @@ HTTP W/ Auth True + False 0 @@ -4704,6 +5809,7 @@ HTTP W/ Auth True + False 0 0 Details: @@ -4718,6 +5824,7 @@ HTTP W/ Auth True + False 0 Version: @@ -4731,6 +5838,7 @@ HTTP W/ Auth True + False 0 Author: @@ -4742,6 +5850,7 @@ HTTP W/ Auth True + False 0 Homepage: @@ -4755,6 +5864,7 @@ HTTP W/ Auth True + False 0 Author Email: @@ -4768,6 +5878,7 @@ HTTP W/ Auth True + False 0 @@ -4781,6 +5892,7 @@ HTTP W/ Auth True + False 0 @@ -4798,6 +5910,7 @@ HTTP W/ Auth True + False <b>Info</b> True @@ -4817,26 +5930,32 @@ HTTP W/ Auth + True + True 2 True + False center True True True + False True + False 5 True + False gtk-add @@ -4848,6 +5967,7 @@ HTTP W/ Auth True + False _Install Plugin True True @@ -4872,14 +5992,17 @@ HTTP W/ Auth True True True + False True + False 5 True + False gtk-refresh @@ -4891,6 +6014,7 @@ HTTP W/ Auth True + False _Rescan Plugins True True @@ -4920,19 +6044,23 @@ HTTP W/ Auth True + False True True True + False True + False 5 True + False gtk-find @@ -4944,6 +6072,7 @@ HTTP W/ Auth True + False _Find More Plugins True True @@ -4976,7 +6105,7 @@ HTTP W/ Auth - 9 + 2 @@ -5002,69 +6131,11 @@ HTTP W/ Auth + True + True 1 - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - end - - - gtk-cancel - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - False - False - 0 - - - - - gtk-apply - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - False - False - 1 - - - - - gtk-ok - True - True - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - True - - - - False - False - 2 - - - - - False - end - 0 - - diff --git a/deluge/ui/gtkui/glade/torrent_menu.glade b/deluge/ui/gtkui/glade/torrent_menu.glade index eef4f5adc..c7dfbe096 100644 --- a/deluge/ui/gtkui/glade/torrent_menu.glade +++ b/deluge/ui/gtkui/glade/torrent_menu.glade @@ -274,6 +274,13 @@ True + + + False + _Change Ownership + True + + True diff --git a/deluge/ui/gtkui/glade/tray_menu.glade b/deluge/ui/gtkui/glade/tray_menu.glade index eaca5a191..ac983601f 100644 --- a/deluge/ui/gtkui/glade/tray_menu.glade +++ b/deluge/ui/gtkui/glade/tray_menu.glade @@ -90,7 +90,7 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-missing-image + gtk-go-down 1 @@ -106,7 +106,7 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-missing-image + gtk-go-up 1 @@ -123,6 +123,7 @@ GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK Quit & Shutdown Daemon True + False @@ -143,17 +144,10 @@ True GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - _Quit + gtk-quit + True True - - - True - GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - gtk-quit - 1 - - diff --git a/deluge/ui/gtkui/gtkui.py b/deluge/ui/gtkui/gtkui.py index 1609a2869..70fa2a5f9 100644 --- a/deluge/ui/gtkui/gtkui.py +++ b/deluge/ui/gtkui/gtkui.py @@ -33,11 +33,13 @@ # # +import gobject +gobject.set_prgname("deluge") + # Install the twisted reactor from twisted.internet import gtk2reactor reactor = gtk2reactor.install() -import gobject import gettext import locale import pkg_resources @@ -114,8 +116,9 @@ DEFAULT_PREFS = { "interactive_add": True, "focus_add_dialog": True, "enable_system_tray": True, - "close_to_tray": True, + "close_to_tray": False, "start_in_tray": False, + "enable_appindicator": False, "lock_tray": False, "tray_password": "", "check_new_releases": True, @@ -135,8 +138,6 @@ DEFAULT_PREFS = { "autoconnect_host_id": None, "autostart_localhost": False, "autoadd_queued": False, - "autoadd_enable": False, - "autoadd_location": "", "choose_directory_dialog_path": deluge.common.get_default_download_dir(), "show_new_releases": True, "signal_port": 40000, @@ -158,7 +159,12 @@ DEFAULT_PREFS = { "sidebar_show_trackers": True, "sidebar_position": 170, "show_rate_in_title": False, - "createtorrent.trackers": [] + "createtorrent.trackers": [], + "show_piecesbar": False, + "pieces_color_missing": [65535, 0, 0], + "pieces_color_waiting": [4874, 56494, 0], + "pieces_color_downloading": [65535, 55255, 0], + "pieces_color_completed": [4883, 26985, 56540], } class GtkUI(object): @@ -206,7 +212,7 @@ class GtkUI(object): # Initialize gdk threading gtk.gdk.threads_init() - gobject.threads_init() + # We make sure that the UI components start once we get a core URI client.set_disconnect_callback(self.__on_disconnect) @@ -237,7 +243,8 @@ class GtkUI(object): rpc_stats.start(10) reactor.callWhenRunning(self._on_reactor_start) - # Start the gtk main loop + + # Initialize gdk threading gtk.gdk.threads_enter() reactor.run() self.shutdown() @@ -327,37 +334,88 @@ Please see the details below for more information."), details=traceback.format_e def __start_non_classic(self): # Autoconnect to a host if self.config["autoconnect"]: - for host in self.connectionmanager.config["hosts"]: - if host[0] == self.config["autoconnect_host_id"]: + + def update_connection_manager(): + if not self.connectionmanager.running: + return + self.connectionmanager.glade.get_widget( + "button_refresh" + ).emit("clicked") + + def close_connection_manager(): + if not self.connectionmanager.running: + return + self.connectionmanager.glade.get_widget( + "button_close" + ).emit("clicked") + + for host_config in self.connectionmanager.config["hosts"]: + hostid, host, port, user, passwd = host_config + if hostid == self.config["autoconnect_host_id"]: try_connect = True # Check to see if we need to start the localhost daemon - if self.config["autostart_localhost"] and host[1] in ("localhost", "127.0.0.1"): - log.debug("Autostarting localhost:%s", host[2]) - try_connect = client.start_daemon(host[2], deluge.configmanager.get_config_dir()) + if self.config["autostart_localhost"] and host in ("localhost", "127.0.0.1"): + log.debug("Autostarting localhost:%s", host) + try_connect = client.start_daemon( + port, deluge.configmanager.get_config_dir() + ) log.debug("Localhost started: %s", try_connect) if not try_connect: dialogs.ErrorDialog( _("Error Starting Daemon"), - _("There was an error starting the daemon process. Try running it from a console to see if there is an error.")).run() + _("There was an error starting the daemon " + "process. Try running it from a console " + "to see if there is an error.") + ).run() + + # Daemon Started, let's update it's info + reactor.callLater(0.5, update_connection_manager) def on_connect(connector): component.start() - def on_connect_fail(result, try_counter): - log.error("Connection to host failed..") - # We failed connecting to the daemon, but lets try again - if try_counter: - log.info("Retrying connection.. Retries left: %s", try_counter) - try_counter -= 1 - import time - time.sleep(0.5) - do_connect(try_counter) - return result + reactor.callLater(0.2, update_connection_manager) + reactor.callLater(0.5, close_connection_manager) - def do_connect(try_counter): - client.connect(*host[1:]).addCallback(on_connect).addErrback(on_connect_fail, try_counter) + def on_connect_fail(reason, try_counter, + host, port, user, passwd): + if not try_counter: + return + + if reason.check(deluge.error.AuthenticationRequired, + deluge.error.BadLoginError): + log.debug("PasswordRequired exception") + dialog = dialogs.AuthenticationDialog( + reason.value.message, reason.value.username + ) + def dialog_finished(response_id, host, port): + if response_id == gtk.RESPONSE_OK: + reactor.callLater( + 0.5, do_connect, try_counter-1, + host, port, dialog.get_username(), + dialog.get_password()) + dialog.run().addCallback(dialog_finished, + host, port) + return + + log.error("Connection to host failed..") + log.info("Retrying connection.. Retries left: " + "%s", try_counter) + reactor.callLater(0.5, update_connection_manager) + reactor.callLater(0.5, do_connect, try_counter-1, + host, port, user, passwd) + + def do_connect(try_counter, host, port, user, passwd): + log.debug("Trying to connect to %s@%s:%s", + user, host, port) + d = client.connect(host, port, user, passwd) + d.addCallback(on_connect) + d.addErrback(on_connect_fail, try_counter, + host, port, user, passwd) if try_connect: - do_connect(6) + reactor.callLater( + 0.5, do_connect, 6, host, port, user, passwd + ) break if self.config["show_connection_manager_on_start"]: diff --git a/deluge/ui/gtkui/listview.py b/deluge/ui/gtkui/listview.py index 9a2aed9a6..f708c503e 100644 --- a/deluge/ui/gtkui/listview.py +++ b/deluge/ui/gtkui/listview.py @@ -103,6 +103,23 @@ def cell_data_date(column, cell, model, row, data): """Display value as date, eg 05/05/08""" cell.set_property('text', deluge.common.fdate(model.get_value(row, data))) +def cell_data_date_or_never(column, cell, model, row, data): + """Display value as date, eg 05/05/08 or Never""" + value = model.get_value(row, data) + if value > 0.0: + cell.set_property('text', deluge.common.fdate(value)) + else: + cell.set_property('text', _("Never")) + +def cell_data_speed_limit(column, cell, model, row, data): + """Display value as a speed, eg. 2 KiB/s""" + speed = model.get_value(row, data) + speed_str = "" + if speed > 0: + speed_str = deluge.common.fspeed(speed * 1024) + + cell.set_property('text', speed_str) + class ListViewColumnState: """Used for saving/loading column state""" def __init__(self, name, position, width, visible, sort, sort_order): @@ -427,7 +444,7 @@ class ListView: def add_column(self, header, render, col_types, hidden, position, status_field, sortid, text=0, value=0, pixbuf=0, function=None, - column_type=None, sort_func=None): + column_type=None, sort_func=None, tooltip=None, default=True): """Adds a column to the ListView""" # Add the column types to liststore_columns column_indices = [] @@ -504,20 +521,33 @@ class ListView: column.connect('button-press-event', self.on_treeview_header_right_clicked) + if tooltip: + column.get_widget().set_tooltip_markup(tooltip) + # Check for loaded state and apply + column_in_state = False if self.state != None: for column_state in self.state: if header == column_state.name: # We found a loaded state + column_in_state = True if column_state.width > 0: column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED) column.set_fixed_width(column_state.width) if column_state.sort is not None and column_state.sort > -1: - self.model_filter.set_sort_column_id(column_state.sort, column_state.sort_order) + self.model_filter.set_sort_column_id( + column_state.sort, column_state.sort_order + ) column.set_visible(column_state.visible) position = column_state.position - + break + + # Set this column to not visible if its not in the state and + # its not supposed to be shown by default + if not column_in_state and not default and not hidden: + column.set_visible(False) + if position is not None: self.treeview.insert_column(column, position) else: @@ -531,79 +561,113 @@ class ListView: return True - def add_text_column(self, header, col_type=str, hidden=False, - position=None, - status_field=None, - sortid=0, - column_type="text", - sort_func=None): + def add_text_column(self, header, col_type=str, hidden=False, position=None, + status_field=None, sortid=0, column_type="text", + sort_func=None, tooltip=None, default=True): """Add a text column to the listview. Only the header name is required. """ render = gtk.CellRendererText() self.add_column(header, render, col_type, hidden, position, - status_field, sortid, column_type=column_type, sort_func=sort_func) + status_field, sortid, column_type=column_type, + sort_func=sort_func, tooltip=tooltip, default=default) return True def add_bool_column(self, header, col_type=bool, hidden=False, - position=None, - status_field=None, - sortid=0, - column_type="bool"): + position=None, status_field=None, sortid=0, + column_type="bool", tooltip=None, default=True): """Add a bool column to the listview""" render = gtk.CellRendererToggle() self.add_column(header, render, col_type, hidden, position, - status_field, sortid, column_type=column_type) + status_field, sortid, column_type=column_type, + tooltip=tooltip, default=default) def add_func_column(self, header, function, col_types, sortid=0, - hidden=False, position=None, status_field=None, - column_type="func", sort_func=None): + hidden=False, position=None, status_field=None, + column_type="func", sort_func=None, tooltip=None, default=True): """Add a function column to the listview. Need a header name, the function and the column types.""" render = gtk.CellRendererText() self.add_column(header, render, col_types, hidden, position, - status_field, sortid, column_type=column_type, - function=function, sort_func=sort_func) + status_field, sortid, column_type=column_type, + function=function, sort_func=sort_func, tooltip=tooltip, default=default) return True - def add_progress_column(self, header, col_types=[float, str], - sortid=0, - hidden=False, - position=None, - status_field=None, - function=None, - column_type="progress"): + def add_progress_column(self, header, col_types=[float, str], sortid=0, + hidden=False, position=None, status_field=None, + function=None, column_type="progress", + tooltip=None, default=True): """Add a progress column to the listview.""" render = gtk.CellRendererProgress() self.add_column(header, render, col_types, hidden, position, - status_field, sortid, function=function, - column_type=column_type, - value=0, text=1) + status_field, sortid, function=function, + column_type=column_type, value=0, text=1, + tooltip=tooltip, default=default) return True - def add_texticon_column(self, header, col_types=[str, str], - sortid=1, - hidden=False, - position=None, - status_field=None, - column_type="texticon", - function=None): + def add_texticon_column(self, header, col_types=[str, str], sortid=1, + hidden=False, position=None, status_field=None, + column_type="texticon", function=None, + tooltip=None, default=True): """Adds a texticon column to the listview.""" render1 = gtk.CellRendererPixbuf() render2 = gtk.CellRendererText() - self.add_column(header, (render1, render2), col_types, hidden, - position, status_field, sortid, - column_type=column_type, function=function, - pixbuf=0, text=1) + self.add_column(header, (render1, render2), col_types, hidden, position, + status_field, sortid, column_type=column_type, + function=function, pixbuf=0, text=1, tooltip=tooltip, default=default) return True def on_keypress_search_by_name(self, model, columnn, key, iter): TORRENT_NAME_COL = 5 return not model[iter][TORRENT_NAME_COL].lower().startswith(key.lower()) + + def restore_columns_order_from_state(self): + if self.state is None: + # No state file exists, so, no reordering can be done + return + columns = self.treeview.get_columns() + def find_column(header): + for column in columns: + if column.get_title() == header: + return column + + restored_columns = [] + for col_state in self.state: + if col_state.name in restored_columns: + # Duplicate column in state!?!?!? + continue + elif not col_state.visible: + # Column is not visible, no need to reposition + continue + + try: + column_at_position = columns[col_state.position] + except IndexError: + # While updating the multiuser branch, which adds a new column + # an IndexError was raised, just continue processing, once + # deluge is restarted, it all should be good + continue + if col_state.name == column_at_position.get_title(): + # It's in the right position + continue + column = find_column(col_state.name) + if not column: + log.debug("Could not find column matching \"%s\" on state." % + col_state.name) + # The cases where I've found that the column could not be found + # is when not using the english locale, ie, the default one, or + # when changing locales between runs. + # On the next load, all should be fine + continue + self.treeview.move_column_after(column, column_at_position) + # Get columns again to keep reordering since positions have changed + columns = self.treeview.get_columns() + restored_columns.append(col_state.name) + self.create_new_liststore() diff --git a/deluge/ui/gtkui/mainwindow.py b/deluge/ui/gtkui/mainwindow.py index bc582c439..572284e25 100644 --- a/deluge/ui/gtkui/mainwindow.py +++ b/deluge/ui/gtkui/mainwindow.py @@ -38,7 +38,6 @@ import pygtk pygtk.require('2.0') import gtk import gtk.glade -import gobject import logging import pkg_resources from urlparse import urlparse @@ -162,8 +161,20 @@ class MainWindow(component.Component): :type shutdown: boolean """ if shutdown: - client.daemon.shutdown() - reactor.stop() + def on_daemon_shutdown(result): + reactor.stop() + client.daemon.shutdown().addCallback(on_daemon_shutdown) + return + if client.is_classicmode(): + reactor.stop() + return + if not client.connected(): + reactor.stop() + return + def on_client_disconnected(result): + reactor.stop() + client.disconnect().addCallback(on_client_disconnected) + def load_window_state(self): x = self.config["window_x_pos"] @@ -247,11 +258,11 @@ class MainWindow(component.Component): else: self.window.set_title("Deluge") - def on_newversionavailable_event(self, event): + def on_newversionavailable_event(self, new_version): if self.config["show_new_releases"]: from deluge.ui.gtkui.new_release_dialog import NewReleaseDialog - reactor.callLater(5.0, NewReleaseDialog().show, event.new_release) + reactor.callLater(5.0, NewReleaseDialog().show, new_version) - def on_torrentfinished_event(self, event): + def on_torrentfinished_event(self, torrent_id): from deluge.ui.gtkui.notification import Notification - Notification().notify(event.torrent_id) + Notification().notify(torrent_id) diff --git a/deluge/ui/gtkui/menubar.py b/deluge/ui/gtkui/menubar.py index ce80e5463..7a0f4d904 100644 --- a/deluge/ui/gtkui/menubar.py +++ b/deluge/ui/gtkui/menubar.py @@ -2,6 +2,7 @@ # menubar.py # # Copyright (C) 2007, 2008 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -46,6 +47,7 @@ import deluge.component as component from deluge.ui.client import client import deluge.common import common +import dialogs from deluge.configmanager import ConfigManager log = logging.getLogger(__name__) @@ -199,10 +201,19 @@ class MenuBar(component.Component): if not self.config["classic_mode"]: self.window.main_glade.get_widget("separatormenuitem").show() self.window.main_glade.get_widget("menuitem_quitdaemon").show() + # Show the Torrent menu because we're connected to a host self.menu_torrent.show() + if client.get_auth_level() == deluge.common.AUTH_LEVEL_ADMIN: + # Get known accounts to allow changing ownership + client.core.get_known_accounts().addCallback( + self._on_known_accounts).addErrback(self._on_known_accounts_fail + ) + def stop(self): + log.debug("MenuBar stopping") + for widget in self.change_sensitivity: self.window.main_glade.get_widget(widget).set_sensitive(False) @@ -230,11 +241,11 @@ class MenuBar(component.Component): return sep ### Callbacks ### - def on_torrentstatechanged_event(self, event): - if event.state == "Paused": + def on_torrentstatechanged_event(self, torrent_id, state): + if state == "Paused": self.update_menu() - def on_torrentresumed_event(self, event): + def on_torrentresumed_event(self, torrent_id): self.update_menu() def on_sessionpaused_event(self): @@ -311,17 +322,21 @@ class MenuBar(component.Component): def _on_torrent_status(status): deluge.common.open_file(status["save_path"]) for torrent_id in component.get("TorrentView").get_selected_torrents(): - component.get("SessionProxy").get_torrent_status(torrent_id, ["save_path"]).addCallback(_on_torrent_status) + component.get("SessionProxy").get_torrent_status( + torrent_id, ["save_path"]).addCallback(_on_torrent_status) def on_menuitem_move_activate(self, data=None): log.debug("on_menuitem_move_activate") if client.is_localhost(): from deluge.configmanager import ConfigManager config = ConfigManager("gtkui.conf") - chooser = gtk.FileChooserDialog(_("Choose a directory to move files to"\ - ) , component.get("MainWindow").window, \ - gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, buttons=(gtk.STOCK_CANCEL, \ - gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) + chooser = gtk.FileChooserDialog( + _("Choose a directory to move files to"), + component.get("MainWindow").window, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OK, gtk.RESPONSE_OK) + ) chooser.set_local_only(True) if not deluge.common.windows_check(): chooser.set_icon(common.get_deluge_icon()) @@ -334,25 +349,39 @@ class MenuBar(component.Component): component.get("TorrentView").get_selected_torrents(), result) chooser.destroy() else: - component.get("SessionProxy").get_torrent_status(component.get("TorrentView").get_selected_torrent(), ["save_path"]).addCallback(self.show_move_storage_dialog) + component.get("SessionProxy").get_torrent_status( + component.get("TorrentView").get_selected_torrent(), + ["save_path"]).addCallback(self.show_move_storage_dialog) def show_move_storage_dialog(self, status): log.debug("show_move_storage_dialog") - glade = gtk.glade.XML( - pkg_resources.resource_filename("deluge.ui.gtkui", - "glade/move_storage_dialog.glade")) - dialog = glade.get_widget("move_storage_dialog") - dialog.set_transient_for(self.window.window) - entry = glade.get_widget("entry_destination") - entry.set_text(status["save_path"]) - def _on_response_event(widget, response_id): + glade = gtk.glade.XML(pkg_resources.resource_filename( + "deluge.ui.gtkui", "glade/move_storage_dialog.glade" + )) + # Keep it referenced: + # https://bugzilla.gnome.org/show_bug.cgi?id=546802 + self.move_storage_dialog = glade.get_widget("move_storage_dialog") + self.move_storage_dialog.set_transient_for(self.window.window) + self.move_storage_dialog_entry = glade.get_widget("entry_destination") + self.move_storage_dialog_entry.set_text(status["save_path"]) + def on_dialog_response_event(widget, response_id): + + def on_core_result(result): + # Delete references + del self.move_storage_dialog + del self.move_storage_dialog_entry + if response_id == gtk.RESPONSE_OK: - log.debug("Moving torrents to %s", entry.get_text()) - path = entry.get_text() - client.core.move_storage(component.get("TorrentView").get_selected_torrents(), path) - dialog.hide() - dialog.connect("response", _on_response_event) - dialog.show() + log.debug("Moving torrents to %s", + self.move_storage_dialog_entry.get_text()) + path = self.move_storage_dialog_entry.get_text() + client.core.move_storage( + component.get("TorrentView").get_selected_torrents(), path + ).addCallback(on_core_result) + self.move_storage_dialog.hide() + + self.move_storage_dialog.connect("response", on_dialog_response_event) + self.move_storage_dialog.show() def on_menuitem_queue_top_activate(self, value): log.debug("on_menuitem_queue_top_activate") @@ -423,10 +452,21 @@ class MenuBar(component.Component): } # widget: (header, type_str, image_stockid, image_filename, default) other_dialog_info = { - "menuitem_down_speed": (_("Set Maximum Download Speed"), "KiB/s", None, "downloading.svg", -1.0), - "menuitem_up_speed": (_("Set Maximum Upload Speed"), "KiB/s", None, "seeding.svg", -1.0), - "menuitem_max_connections": (_("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, -1), - "menuitem_upload_slots": (_("Set Maximum Upload Slots"), "", gtk.STOCK_SORT_ASCENDING, None, -1) + "menuitem_down_speed": ( + _("Set Maximum Download Speed"), + "KiB/s", None, "downloading.svg", -1.0 + ), + "menuitem_up_speed": ( + _("Set Maximum Upload Speed"), + "KiB/s", None, "seeding.svg", -1.0 + ), + "menuitem_max_connections": ( + _("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, -1 + ), + "menuitem_upload_slots": ( + _("Set Maximum Upload Slots"), + "", gtk.STOCK_SORT_ASCENDING, None, -1 + ) } # Show the other dialog @@ -465,3 +505,75 @@ class MenuBar(component.Component): for item in items: getattr(self.window.main_glade.get_widget(item), attr)() + + def _on_known_accounts(self, known_accounts): + known_accounts_to_log = [] + for account in known_accounts: + account_to_log = {} + for key, value in account.copy().iteritems(): + if key == 'password': + value = '*' * len(value) + account_to_log[key] = value + known_accounts_to_log.append(account_to_log) + log.debug("_on_known_accounts: %s", known_accounts_to_log) + if len(known_accounts) <= 1: + return + + self.torrentmenu_glade.get_widget("menuitem_change_owner").set_visible(True) + + self.change_owner_submenu = gtk.Menu() + self.change_owner_submenu_items = {} + maingroup = gtk.RadioMenuItem(None, None) + + self.change_owner_submenu_items[None] = gtk.RadioMenuItem(maingroup) + + for account in known_accounts: + username = account["username"] + item = gtk.RadioMenuItem(maingroup, username) + self.change_owner_submenu_items[username] = item + self.change_owner_submenu.append(item) + item.connect("toggled", self._on_change_owner_toggled, username) + + self.change_owner_submenu.show_all() + self.change_owner_submenu_items[None].set_active(True) + self.change_owner_submenu_items[None].hide() + self.torrentmenu_glade.get_widget("menuitem_change_owner").connect( + "activate", self._on_change_owner_submenu_active + ) + self.torrentmenu_glade.get_widget("menuitem_change_owner").set_submenu(self.change_owner_submenu) + + def _on_known_accounts_fail(self, reason): + self.torrentmenu_glade.get_widget("menuitem_change_owner").set_visible(False) + + def _on_change_owner_submenu_active(self, widget): + log.debug("_on_change_owner_submenu_active") + selected = component.get("TorrentView").get_selected_torrents() + if len(selected) > 1: + self.change_owner_submenu_items[None].set_active(True) + return + + torrent_owner = component.get("TorrentView").get_torrent_status(selected[0])["owner"] + for username, item in self.change_owner_submenu_items.iteritems(): + item.set_active(username == torrent_owner) + + def _on_change_owner_toggled(self, widget, username): + log.debug("_on_change_owner_toggled") + update_torrents = [] + selected = component.get("TorrentView").get_selected_torrents() + for torrent_id in selected: + torrent_status = component.get("TorrentView").get_torrent_status(torrent_id) + if torrent_status["owner"] != username: + update_torrents.append(torrent_id) + + if update_torrents: + log.debug("Setting torrent owner \"%s\" on %s", username, update_torrents) + + def failed_change_owner(failure): + dialogs.ErrorDialog( + _("Ownership Change Error"), + _("There was an error while trying changing ownership."), + self.window.window, details=failure.value.logable() + ).run() + client.core.set_torrents_owner( + update_torrents, username).addErrback(failed_change_owner) + diff --git a/deluge/ui/gtkui/options_tab.py b/deluge/ui/gtkui/options_tab.py index 6ea769171..4360ec76a 100644 --- a/deluge/ui/gtkui/options_tab.py +++ b/deluge/ui/gtkui/options_tab.py @@ -53,6 +53,7 @@ class OptionsTab(Tab): self.spin_max_upload_slots = glade.get_widget("spin_max_upload_slots") self.chk_private = glade.get_widget("chk_private") self.chk_prioritize_first_last = glade.get_widget("chk_prioritize_first_last") + self.chk_sequential_download = glade.get_widget("chk_sequential_download") self.chk_auto_managed = glade.get_widget("chk_auto_managed") self.chk_stop_at_ratio = glade.get_widget("chk_stop_at_ratio") self.chk_remove_at_ratio = glade.get_widget("chk_remove_at_ratio") @@ -60,6 +61,8 @@ class OptionsTab(Tab): self.chk_move_completed = glade.get_widget("chk_move_completed") self.filechooser_move_completed = glade.get_widget("filechooser_move_completed") self.entry_move_completed = glade.get_widget("entry_move_completed") + self.chk_shared = glade.get_widget("chk_shared") + self.button_apply = glade.get_widget("button_apply") self.prev_torrent_id = None self.prev_status = None @@ -68,7 +71,10 @@ class OptionsTab(Tab): "on_button_apply_clicked": self._on_button_apply_clicked, "on_button_edit_trackers_clicked": self._on_button_edit_trackers_clicked, "on_chk_move_completed_toggled": self._on_chk_move_completed_toggled, - "on_chk_stop_at_ratio_toggled": self._on_chk_stop_at_ratio_toggled + "on_chk_stop_at_ratio_toggled": self._on_chk_stop_at_ratio_toggled, + "on_chk_toggled": self._on_chk_toggled, + "on_spin_value_changed": self._on_spin_value_changed, + "on_move_completed_file_set": self._on_move_completed_file_set }) def start(self): @@ -78,6 +84,9 @@ class OptionsTab(Tab): else: self.filechooser_move_completed.hide() self.entry_move_completed.show() + self.entry_move_completed.connect( + "changed", self._on_entry_move_completed_changed + ) def stop(self): pass @@ -98,8 +107,8 @@ class OptionsTab(Tab): if torrent_id != self.prev_torrent_id: self.prev_status = None - component.get("SessionProxy").get_torrent_status(torrent_id, - ["max_download_speed", + component.get("SessionProxy").get_torrent_status(torrent_id, [ + "max_download_speed", "max_upload_speed", "max_connections", "max_upload_slots", @@ -109,8 +118,12 @@ class OptionsTab(Tab): "stop_at_ratio", "stop_ratio", "remove_at_ratio", + "compact", + "sequential_download", "move_on_completed", - "move_on_completed_path"]).addCallback(self._on_get_torrent_status) + "move_on_completed_path", + "shared" + ]).addCallback(self._on_get_torrent_status) self.prev_torrent_id = torrent_id def clear(self): @@ -153,44 +166,98 @@ class OptionsTab(Tab): self.filechooser_move_completed.set_current_folder(status["move_on_completed_path"]) else: self.entry_move_completed.set_text(status["move_on_completed_path"]) + if status["shared"] != self.prev_status["shared"]: + self.chk_shared.set_active(status["shared"]) + + if status["compact"]: + self.chk_prioritize_first_last.set_sensitive(False) + self.chk_prioritize_first_last.hide() + self.chk_sequential_download.set_sensitive(False) + self.chk_sequential_download.hide() + else: + if status["prioritize_first_last"] != self.prev_status["prioritize_first_last"]: + self.chk_prioritize_first_last.set_active(status["prioritize_first_last"]) + if status["sequential_download"] != self.prev_status["sequential_download"]: + self.chk_sequential_download.set_active(status["sequential_download"]) + + + if self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(False) self.prev_status = status def _on_button_apply_clicked(self, button): if self.spin_max_download.get_value() != self.prev_status["max_download_speed"]: - client.core.set_torrent_max_download_speed(self.prev_torrent_id, self.spin_max_download.get_value()) + client.core.set_torrent_max_download_speed( + self.prev_torrent_id, self.spin_max_download.get_value() + ) if self.spin_max_upload.get_value() != self.prev_status["max_upload_speed"]: - client.core.set_torrent_max_upload_speed(self.prev_torrent_id, self.spin_max_upload.get_value()) + client.core.set_torrent_max_upload_speed( + self.prev_torrent_id, self.spin_max_upload.get_value() + ) if self.spin_max_connections.get_value_as_int() != self.prev_status["max_connections"]: - client.core.set_torrent_max_connections(self.prev_torrent_id, self.spin_max_connections.get_value_as_int()) + client.core.set_torrent_max_connections( + self.prev_torrent_id, self.spin_max_connections.get_value_as_int() + ) if self.spin_max_upload_slots.get_value_as_int() != self.prev_status["max_upload_slots"]: - client.core.set_torrent_max_upload_slots(self.prev_torrent_id, self.spin_max_upload_slots.get_value_as_int()) - if self.chk_prioritize_first_last.get_active() != self.prev_status["prioritize_first_last"]: - client.core.set_torrent_prioritize_first_last(self.prev_torrent_id, self.chk_prioritize_first_last.get_active()) + client.core.set_torrent_max_upload_slots( + self.prev_torrent_id, self.spin_max_upload_slots.get_value_as_int() + ) + if self.chk_prioritize_first_last.get_active() != \ + self.prev_status["prioritize_first_last"] and \ + not self.prev_status["compact"]: + client.core.set_torrent_prioritize_first_last( + self.prev_torrent_id, self.chk_prioritize_first_last.get_active() + ) + if self.chk_sequential_download.get_active() != \ + self.prev_status["sequential_download"] and \ + not self.prev_status["compact"]: + client.core.set_torrent_sequential_download( + self.prev_torrent_id, self.chk_prioritize_first_last.get_active() + ) if self.chk_auto_managed.get_active() != self.prev_status["is_auto_managed"]: - client.core.set_torrent_auto_managed(self.prev_torrent_id, self.chk_auto_managed.get_active()) + client.core.set_torrent_auto_managed( + self.prev_torrent_id, self.chk_auto_managed.get_active() + ) if self.chk_stop_at_ratio.get_active() != self.prev_status["stop_at_ratio"]: - client.core.set_torrent_stop_at_ratio(self.prev_torrent_id, self.chk_stop_at_ratio.get_active()) + client.core.set_torrent_stop_at_ratio( + self.prev_torrent_id, self.chk_stop_at_ratio.get_active() + ) if self.spin_stop_ratio.get_value() != self.prev_status["stop_ratio"]: - client.core.set_torrent_stop_ratio(self.prev_torrent_id, self.spin_stop_ratio.get_value()) + client.core.set_torrent_stop_ratio( + self.prev_torrent_id, self.spin_stop_ratio.get_value() + ) if self.chk_remove_at_ratio.get_active() != self.prev_status["remove_at_ratio"]: - client.core.set_torrent_remove_at_ratio(self.prev_torrent_id, self.chk_remove_at_ratio.get_active()) + client.core.set_torrent_remove_at_ratio( + self.prev_torrent_id, self.chk_remove_at_ratio.get_active() + ) if self.chk_move_completed.get_active() != self.prev_status["move_on_completed"]: - client.core.set_torrent_move_completed(self.prev_torrent_id, self.chk_move_completed.get_active()) + client.core.set_torrent_move_completed( + self.prev_torrent_id, self.chk_move_completed.get_active() + ) if self.chk_move_completed.get_active(): if client.is_localhost(): path = self.filechooser_move_completed.get_current_folder() else: path = self.entry_move_completed.get_text() client.core.set_torrent_move_completed_path(self.prev_torrent_id, path) - + if self.chk_shared.get_active() != self.prev_status["shared"]: + client.core.set_torrents_shared( + self.prev_torrent_id, self.chk_shared.get_active() + ) + self.button_apply.set_sensitive(False) def _on_button_edit_trackers_clicked(self, button): from edittrackersdialog import EditTrackersDialog dialog = EditTrackersDialog( self.prev_torrent_id, component.get("MainWindow").window) - dialog.run() + + def on_response(result): + if result: + self.button_apply.set_sensitive(True) + dialog.run().addCallback(on_response) + def _on_chk_move_completed_toggled(self, widget): value = self.chk_move_completed.get_active() @@ -201,8 +268,30 @@ class OptionsTab(Tab): widget.set_sensitive(value) + if not self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(True) + def _on_chk_stop_at_ratio_toggled(self, widget): value = widget.get_active() self.spin_stop_ratio.set_sensitive(value) self.chk_remove_at_ratio.set_sensitive(value) + + if not self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(True) + + def _on_chk_toggled(self, widget): + if not self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(True) + + def _on_spin_value_changed(self, widget): + if not self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(True) + + def _on_move_completed_file_set(self, widget): + if not self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(True) + + def _on_entry_move_completed_changed(self, widget): + if not self.button_apply.is_sensitive(): + self.button_apply.set_sensitive(True) diff --git a/deluge/ui/gtkui/peers_tab.py b/deluge/ui/gtkui/peers_tab.py index 9ac5a6de9..cebf1feed 100644 --- a/deluge/ui/gtkui/peers_tab.py +++ b/deluge/ui/gtkui/peers_tab.py @@ -40,7 +40,6 @@ import logging import os.path import cPickle import pkg_resources -import gobject from itertools import izip from deluge.ui.client import client @@ -164,6 +163,8 @@ class PeersTab(Tab): column.set_resizable(True) column.set_expand(False) column.set_min_width(50) + # Bugfix: Last column needs max_width set to stop scrollbar appearing + column.set_max_width(150) column.set_reorderable(True) self.listview.append_column(column) @@ -272,7 +273,7 @@ class PeersTab(Tab): self.cached_flag_pixbufs[country] = gtk.gdk.pixbuf_new_from_file( pkg_resources.resource_filename( "deluge", - os.path.join("data", "pixmaps", "flags", country.lower() + ".png"))) + os.path.join("ui", "data", "pixmaps", "flags", country.lower() + ".png"))) except Exception, e: log.debug("Unable to load flag: %s", e) return None diff --git a/deluge/ui/gtkui/piecesbar.py b/deluge/ui/gtkui/piecesbar.py new file mode 100644 index 000000000..55e7f7cb7 --- /dev/null +++ b/deluge/ui/gtkui/piecesbar.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +# +# piecesbar.py +# +# Copyright (C) 2011 Pedro Algarvio +# +# Deluge is free software. +# +# You may 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. +# +# deluge 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 deluge. If not, write to: +# The Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor +# Boston, MA 02110-1301, USA. +# +# In addition, as a special exception, the copyright holders give +# permission to link the code of portions of this program with the OpenSSL +# library. +# You must obey the GNU General Public License in all respects for all of +# the code used other than OpenSSL. If you modify file(s) with this +# exception, you may extend this exception to your version of the file(s), +# but you are not obligated to do so. If you do not wish to do so, delete +# this exception statement from your version. If you delete this exception +# statement from all source files in the program, then also delete it here. +# +# + +import gtk +import cairo +import pango +import pangocairo +import logging +from math import pi +from deluge.configmanager import ConfigManager + +log = logging.getLogger(__name__) + + +COLOR_STATES = { + 0: "missing", + 1: "waiting", + 2: "downloading", + 3: "completed" +} + +class PiecesBar(gtk.DrawingArea): + # Draw in response to an expose-event + __gsignals__ = {"expose-event": "override"} + + def __init__(self): + gtk.DrawingArea.__init__(self) + # Get progress bar styles, in order to keep font consistency + pb = gtk.ProgressBar() + pb_style = pb.get_style() + self.__text_font = pb_style.font_desc + self.__text_font.set_weight(pango.WEIGHT_BOLD) + # Done with the ProgressBar styles, don't keep refs of it + del pb, pb_style + + self.set_size_request(-1, 25) + self.gtkui_config = ConfigManager("gtkui.conf") + self.__width = self.__old_width = 0 + self.__height = self.__old_height = 0 + self.__pieces = self.__old_pieces = () + self.__num_pieces = self.__old_num_pieces = None + self.__text = self.__old_text = "" + self.__fraction = self.__old_fraction = 0.0 + self.__state = self.__old_state = None + self.__progress_overlay = self.__text_overlay = self.__pieces_overlay = None + self.__cr = None + + self.connect('size-allocate', self.do_size_allocate_event) + self.set_colormap(self.get_screen().get_rgba_colormap()) + self.show() + + def do_size_allocate_event(self, widget, size): + self.__old_width = self.__width + self.__width = size.width + self.__old_height = self.__height + self.__height = size.height + + # Handle the expose-event by drawing + def do_expose_event(self, event): + # Create cairo context + self.__cr = self.window.cairo_create() + self.__cr.set_line_width(max(self.__cr.device_to_user_distance(0.5, 0.5))) + + # Restrict Cairo to the exposed area; avoid extra work + self.__roundcorners_clipping() + + if not self.__pieces and self.__num_pieces is not None: + # Special case. Completed torrents do not send any pieces in their + # status. + self.__draw_pieces_completed() + elif self.__pieces: + self.__draw_pieces() + + self.__draw_progress_overlay() + self.__write_text() + self.__roundcorners_border() + + # Drawn once, update width, eight + if self.__resized(): + self.__old_width = self.__width + self.__old_height = self.__height + + def __roundcorners_clipping(self): + self.__create_roundcorners_subpath( + self.__cr, 0, 0, self.__width, self.__height + ) + self.__cr.clip() + + def __roundcorners_border(self): + self.__create_roundcorners_subpath( + self.__cr, 0.5, 0.5, self.__width-1, self.__height-1 + ) + self.__cr.set_source_rgba(0.0, 0.0, 0.0, 0.9) + self.__cr.stroke() + + def __create_roundcorners_subpath(self, ctx, x, y, width, height): + aspect = 1.0 + corner_radius = height/10.0 + radius = corner_radius/aspect + degrees = pi/180.0 + ctx.new_sub_path() + ctx.arc(x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees) + ctx.arc(x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees) + ctx.arc(x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees) + ctx.arc(x + radius, y + radius, radius, 180 * degrees, 270 * degrees) + ctx.close_path() + return ctx + + def __draw_pieces(self): + if (self.__resized() or self.__pieces != self.__old_pieces or + self.__pieces_overlay == None): + # Need to recreate the cache drawing + self.__pieces_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.__width, self.__height + ) + ctx = cairo.Context(self.__pieces_overlay) + start_pos = 0 + num_pieces = self.__num_pieces and self.__num_pieces or len(self.__pieces) + piece_width = self.__width*1.0/num_pieces + + for state in self.__pieces: + color = self.gtkui_config["pieces_color_%s" % COLOR_STATES[state]] + ctx.set_source_rgb( + color[0]/65535.0, + color[1]/65535.0, + color[2]/65535.0, + ) + ctx.rectangle(start_pos, 0, piece_width, self.__height) + ctx.fill() + start_pos += piece_width + + self.__cr.set_source_surface(self.__pieces_overlay) + self.__cr.paint() + + def __draw_pieces_completed(self): + if (self.__resized() or self.__pieces != self.__old_pieces or + self.__pieces_overlay is None): + # Need to recreate the cache drawing + self.__pieces_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.__width, self.__height + ) + ctx = cairo.Context(self.__pieces_overlay) + piece_width = self.__width*1.0/self.__num_pieces + start = 0 + for _ in range(self.__num_pieces): + # Like this to keep same aspect ratio + color = self.gtkui_config["pieces_color_%s" % COLOR_STATES[3]] + ctx.set_source_rgb( + color[0]/65535.0, + color[1]/65535.0, + color[2]/65535.0, + ) + ctx.rectangle(start, 0, piece_width, self.__height) + ctx.fill() + start += piece_width + + self.__cr.set_source_surface(self.__pieces_overlay) + self.__cr.paint() + + def __draw_progress_overlay(self): + if not self.__state: + # Nothing useful to draw, return now! + return + if (self.__resized() or self.__fraction != self.__old_fraction) or \ + self.__progress_overlay is None: + # Need to recreate the cache drawing + self.__progress_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.__width, self.__height + ) + ctx = cairo.Context(self.__progress_overlay) + ctx.set_source_rgba(0.1, 0.1, 0.1, 0.3) # Transparent + ctx.rectangle(0.0, 0.0, self.__width*self.__fraction, self.__height) + ctx.fill() + self.__cr.set_source_surface(self.__progress_overlay) + self.__cr.paint() + + def __write_text(self): + if not self.__state: + # Nothing useful to draw, return now! + return + if (self.__resized() or self.__text != self.__old_text or + self.__fraction != self.__old_fraction or + self.__state != self.__old_state or + self.__text_overlay is None): + # Need to recreate the cache drawing + self.__text_overlay = cairo.ImageSurface( + cairo.FORMAT_ARGB32, self.__width, self.__height + ) + ctx = cairo.Context(self.__text_overlay) + pg = pangocairo.CairoContext(ctx) + pl = pg.create_layout() + pl.set_font_description(self.__text_font) + pl.set_width(-1) # No text wrapping + + text = "" + if self.__text: + text += self.__text + else: + if self.__state: + text += self.__state + " " + if self.__fraction == 1.0: + format = "%d%%" + else: + format = "%.2f%%" + text += format % (self.__fraction*100) + log.trace("PiecesBar text %r", text) + pl.set_text(text) + plsize = pl.get_size() + text_width = plsize[0]/pango.SCALE + text_height = plsize[1]/pango.SCALE + area_width_without_text = self.__width - text_width + area_height_without_text = self.__height - text_height + ctx.move_to(area_width_without_text/2, area_height_without_text/2) + ctx.set_source_rgb(1.0, 1.0, 1.0) + pg.update_layout(pl) + pg.show_layout(pl) + self.__cr.set_source_surface(self.__text_overlay) + self.__cr.paint() + + def __resized(self): + return (self.__old_width != self.__width or + self.__old_height != self.__height) + + def set_fraction(self, fraction): + self.__old_fraction = self.__fraction + self.__fraction = fraction + + def get_fraction(self): + return self.__fraction + + def get_text(self): + return self.__text + + def set_text(self, text): + self.__old_text = self.__text + self.__text = text + + def set_pieces(self, pieces, num_pieces): + self.__old_pieces = self.__pieces + self.__pieces = pieces + self.__num_pieces = num_pieces + + def get_pieces(self): + return self.__pieces + + def set_state(self, state): + self.__old_state = self.__state + self.__state = state + + def get_state(self): + return self.__state + + def update_from_status(self, status): + log.trace("Updating PiecesBar from status") + self.set_fraction(status["progress"]/100) + torrent_state = status["state"] + self.set_state(torrent_state) + if torrent_state == "Checking": + self.update() + # Skip the pieces assignment + return + + self.set_pieces(status['pieces'], status['num_pieces']) + self.update() + + def clear(self): + self.__pieces = self.__old_pieces = () + self.__num_pieces = self.__old_num_pieces = None + self.__text = self.__old_text = "" + self.__fraction = self.__old_fraction = 0.0 + self.__state = self.__old_state = None + self.__progress_overlay = self.__text_overlay = self.__pieces_overlay = None + self.__cr = None + self.update() + + def update(self): + self.queue_draw() diff --git a/deluge/ui/gtkui/pluginmanager.py b/deluge/ui/gtkui/pluginmanager.py index 8c4ffdc88..b5169977d 100644 --- a/deluge/ui/gtkui/pluginmanager.py +++ b/deluge/ui/gtkui/pluginmanager.py @@ -91,11 +91,11 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase, for plugin in enabled_plugins: self.enable_plugin(plugin) - def _on_plugin_enabled_event(self, event): - self.enable_plugin(event.plugin_name) + def _on_plugin_enabled_event(self, name): + self.enable_plugin(name) - def _on_plugin_disabled_event(self, event): - self.disable_plugin(event.plugin_name) + def _on_plugin_disabled_event(self, name): + self.disable_plugin(name) ## Hook functions def run_on_show_prefs(self): diff --git a/deluge/ui/gtkui/preferences.py b/deluge/ui/gtkui/preferences.py index 28aa8762d..ee265a068 100644 --- a/deluge/ui/gtkui/preferences.py +++ b/deluge/ui/gtkui/preferences.py @@ -2,6 +2,7 @@ # preferences.py # # Copyright (C) 2007, 2008 Andrew Resch +# Copyright (C) 2011 Pedro Algarvio # # Deluge is free software. # @@ -44,13 +45,23 @@ import pkg_resources import deluge.component as component from deluge.ui.client import client import deluge.common -import deluge.error import common +import dialogs from deluge.configmanager import ConfigManager import deluge.configmanager log = logging.getLogger(__name__) +ACCOUNTS_USERNAME, ACCOUNTS_LEVEL, ACCOUNTS_PASSWORD = range(3) +COLOR_MISSING, COLOR_WAITING, COLOR_DOWNLOADING, COLOR_COMPLETED = range(4) + +COLOR_STATES = { + "missing": COLOR_MISSING, + "waiting": COLOR_WAITING, + "downloading": COLOR_DOWNLOADING, + "completed": COLOR_COMPLETED +} + class Preferences(component.Component): def __init__(self): component.Component.__init__(self, "Preferences") @@ -71,7 +82,7 @@ class Preferences(component.Component): self.liststore = gtk.ListStore(int, str) self.treeview.set_model(self.liststore) render = gtk.CellRendererText() - column = gtk.TreeViewColumn("Categories", render, text=1) + column = gtk.TreeViewColumn(_("Categories"), render, text=1) self.treeview.append_column(column) # Add the default categories i = 0 @@ -81,6 +92,35 @@ class Preferences(component.Component): self.liststore.append([i, category]) i += 1 + # Setup accounts tab lisview + self.accounts_levels_mapping = None + self.accounts_authlevel = self.glade.get_widget("accounts_authlevel") + self.accounts_liststore = gtk.ListStore(str, str, str, int) + self.accounts_liststore.set_sort_column_id(ACCOUNTS_USERNAME, + gtk.SORT_ASCENDING) + self.accounts_listview = self.glade.get_widget("accounts_listview") + self.accounts_listview.append_column( + gtk.TreeViewColumn( + _("Username"), gtk.CellRendererText(), text=ACCOUNTS_USERNAME + ) + ) + self.accounts_listview.append_column( + gtk.TreeViewColumn( + _("Level"), gtk.CellRendererText(), text=ACCOUNTS_LEVEL + ) + ) + password_column = gtk.TreeViewColumn( + 'password', gtk.CellRendererText(), text=ACCOUNTS_PASSWORD + ) + self.accounts_listview.append_column(password_column) + password_column.set_visible(False) + self.accounts_listview.set_model(self.accounts_liststore) + + self.accounts_listview.get_selection().connect( + "changed", self._on_accounts_selection_changed + ) + self.accounts_frame = self.glade.get_widget("AccountsFrame") + # Setup plugin tab listview self.plugin_liststore = gtk.ListStore(str, bool) self.plugin_liststore.set_sort_column_id(0, gtk.SORT_ASCENDING) @@ -96,11 +136,13 @@ class Preferences(component.Component): # Connect to the 'changed' event of TreeViewSelection to get selection # changes. - self.treeview.get_selection().connect("changed", - self.on_selection_changed) + self.treeview.get_selection().connect( + "changed", self.on_selection_changed + ) - self.plugin_listview.get_selection().connect("changed", - self.on_plugin_selection_changed) + self.plugin_listview.get_selection().connect( + "changed", self.on_plugin_selection_changed + ) self.glade.signal_autoconnect({ "on_pref_dialog_delete_event": self.on_pref_dialog_delete_event, @@ -114,9 +156,28 @@ class Preferences(component.Component): "on_button_find_plugins_clicked": self._on_button_find_plugins_clicked, "on_button_cache_refresh_clicked": self._on_button_cache_refresh_clicked, "on_combo_proxy_type_changed": self._on_combo_proxy_type_changed, - "on_button_associate_magnet_clicked": self._on_button_associate_magnet_clicked + "on_button_associate_magnet_clicked": self._on_button_associate_magnet_clicked, + "on_accounts_add_clicked": self._on_accounts_add_clicked, + "on_accounts_delete_clicked": self._on_accounts_delete_clicked, + "on_accounts_edit_clicked": self._on_accounts_edit_clicked, + "on_alocation_toggled": self._on_alocation_toggled, + "on_piecesbar_toggle_toggled": self._on_piecesbar_toggle_toggled, + "on_completed_color_set": self._on_completed_color_set, + "on_revert_color_completed_clicked": self._on_revert_color_completed_clicked, + "on_downloading_color_set": self._on_downloading_color_set, + "on_revert_color_downloading_clicked": self._on_revert_color_downloading_clicked, + "on_waiting_color_set": self._on_waiting_color_set, + "on_revert_color_waiting_clicked": self._on_revert_color_waiting_clicked, + "on_missing_color_set": self._on_missing_color_set, + "on_revert_color_missing_clicked": self._on_revert_color_missing_clicked }) + from deluge.ui.gtkui.gtkui import DEFAULT_PREFS + self.COLOR_DEFAULTS = {} + for key in ("missing", "waiting", "downloading", "completed"): + self.COLOR_DEFAULTS[key] = DEFAULT_PREFS["pieces_color_%s" % key][:] + del DEFAULT_PREFS + # These get updated by requests done to the core self.all_plugins = [] self.enabled_plugins = [] @@ -191,9 +252,12 @@ class Preferences(component.Component): component.get("PluginManager").run_on_show_prefs() + # Update the preferences dialog to reflect current config settings self.core_config = {} if client.connected(): + self._get_accounts_tab_data() + def _on_get_config(config): self.core_config = config client.core.get_available_plugins().addCallback(_on_get_available_plugins) @@ -234,10 +298,6 @@ class Preferences(component.Component): ("active", self.core_config["del_copy_torrent_file"]), "torrent_files_button": \ ("filename", self.core_config["torrentfiles_location"]), - "chk_autoadd": \ - ("active", self.core_config["autoadd_enable"]), - "folder_autoadd": \ - ("filename", self.core_config["autoadd_location"]), "radio_compact_allocation": \ ("active", self.core_config["compact_allocation"]), "radio_full_allocation": \ @@ -245,6 +305,9 @@ class Preferences(component.Component): "chk_prioritize_first_last_pieces": \ ("active", self.core_config["prioritize_first_last_pieces"]), + "chk_sequential_download": \ + ("active", + self.core_config["sequential_download"]), "chk_add_paused": ("active", self.core_config["add_paused"]), "spin_port_min": ("value", self.core_config["listen_ports"][0]), "spin_port_max": ("value", self.core_config["listen_ports"][1]), @@ -312,33 +375,44 @@ class Preferences(component.Component): } # Add proxy stuff for t in ("peer", "web_seed", "tracker", "dht"): - core_widgets["spin_proxy_port_%s" % t] = ("value", self.core_config["proxies"][t]["port"]) - core_widgets["combo_proxy_type_%s" % t] = ("active", self.core_config["proxies"][t]["type"]) - core_widgets["txt_proxy_server_%s" % t] = ("text", self.core_config["proxies"][t]["hostname"]) - core_widgets["txt_proxy_username_%s" % t] = ("text", self.core_config["proxies"][t]["username"]) - core_widgets["txt_proxy_password_%s" % t] = ("text", self.core_config["proxies"][t]["password"]) + core_widgets["spin_proxy_port_%s" % t] = ( + "value", self.core_config["proxies"][t]["port"] + ) + core_widgets["combo_proxy_type_%s" % t] = ( + "active", self.core_config["proxies"][t]["type"] + ) + core_widgets["txt_proxy_server_%s" % t] = ( + "text", self.core_config["proxies"][t]["hostname"] + ) + core_widgets["txt_proxy_username_%s" % t] = ( + "text", self.core_config["proxies"][t]["username"] + ) + core_widgets["txt_proxy_password_%s" % t] = ( + "text", self.core_config["proxies"][t]["password"] + ) # Change a few widgets if we're connected to a remote host if not client.is_localhost(): self.glade.get_widget("entry_download_path").show() self.glade.get_widget("download_path_button").hide() core_widgets.pop("download_path_button") - core_widgets["entry_download_path"] = ("text", self.core_config["download_location"]) + core_widgets["entry_download_path"] = ( + "text", self.core_config["download_location"] + ) self.glade.get_widget("entry_move_completed_path").show() self.glade.get_widget("move_completed_path_button").hide() core_widgets.pop("move_completed_path_button") - core_widgets["entry_move_completed_path"] = ("text", self.core_config["move_completed_path"]) + core_widgets["entry_move_completed_path"] = ( + "text", self.core_config["move_completed_path"] + ) self.glade.get_widget("entry_torrents_path").show() self.glade.get_widget("torrent_files_button").hide() core_widgets.pop("torrent_files_button") - core_widgets["entry_torrents_path"] = ("text", self.core_config["torrentfiles_location"]) - - self.glade.get_widget("entry_autoadd").show() - self.glade.get_widget("folder_autoadd").hide() - core_widgets.pop("folder_autoadd") - core_widgets["entry_autoadd"] = ("text", self.core_config["autoadd_location"]) + core_widgets["entry_torrents_path"] = ( + "text", self.core_config["torrentfiles_location"] + ) else: self.glade.get_widget("entry_download_path").hide() self.glade.get_widget("download_path_button").show() @@ -346,8 +420,6 @@ class Preferences(component.Component): self.glade.get_widget("move_completed_path_button").show() self.glade.get_widget("entry_torrents_path").hide() self.glade.get_widget("torrent_files_button").show() - self.glade.get_widget("entry_autoadd").hide() - self.glade.get_widget("folder_autoadd").show() # Update the widgets accordingly for key in core_widgets.keys(): @@ -386,11 +458,10 @@ class Preferences(component.Component): "chk_copy_torrent_file", "chk_del_copy_torrent_file", "torrent_files_button", - "chk_autoadd", - "folder_autoadd", "radio_compact_allocation", "radio_full_allocation", "chk_prioritize_first_last_pieces", + "chk_sequential_download", "chk_add_paused", "spin_port_min", "spin_port_max", @@ -471,12 +542,21 @@ class Preferences(component.Component): self.gtkui_config["close_to_tray"]) self.glade.get_widget("chk_start_in_tray").set_active( self.gtkui_config["start_in_tray"]) + self.glade.get_widget("chk_enable_appindicator").set_active( + self.gtkui_config["enable_appindicator"]) self.glade.get_widget("chk_lock_tray").set_active( self.gtkui_config["lock_tray"]) self.glade.get_widget("chk_classic_mode").set_active( self.gtkui_config["classic_mode"]) self.glade.get_widget("chk_show_rate_in_title").set_active( self.gtkui_config["show_rate_in_title"]) + self.glade.get_widget("piecesbar_toggle").set_active( + self.gtkui_config["show_piecesbar"] + ) + self.__set_color("completed", from_config=True) + self.__set_color("downloading", from_config=True) + self.__set_color("waiting", from_config=True) + self.__set_color("missing", from_config=True) ## Other tab ## self.glade.get_widget("chk_show_new_releases").set_active( @@ -509,13 +589,16 @@ class Preferences(component.Component): """ Sets all altered config values in the core. - :param hide: bool, if True, will not re-show the dialog and will hide it instead + :param hide: bool, if True, will not re-show the dialog and will hide + it instead """ try: from hashlib import sha1 as sha_hash except ImportError: from sha import new as sha_hash + classic_mode_was_set = self.gtkui_config["classic_mode"] + # Get the values from the dialog new_core_config = {} new_gtkui_config = {} @@ -525,6 +608,13 @@ class Preferences(component.Component): self.glade.get_widget("chk_show_dialog").get_active() new_gtkui_config["focus_add_dialog"] = \ self.glade.get_widget("chk_focus_dialog").get_active() + + for state in ("missing", "waiting", "downloading", "completed"): + color = self.glade.get_widget("%s_color" % state).get_color() + new_gtkui_config["pieces_color_%s" % state] = [ + color.red, color.green, color.blue + ] + new_core_config["copy_torrent_file"] = \ self.glade.get_widget("chk_copy_torrent_file").get_active() new_core_config["del_copy_torrent_file"] = \ @@ -546,20 +636,16 @@ class Preferences(component.Component): new_core_config["torrentfiles_location"] = \ self.glade.get_widget("entry_torrents_path").get_text() - new_core_config["autoadd_enable"] = \ - self.glade.get_widget("chk_autoadd").get_active() - if client.is_localhost(): - new_core_config["autoadd_location"] = \ - self.glade.get_widget("folder_autoadd").get_filename() - else: - new_core_config["autoadd_location"] = \ - self.glade.get_widget("entry_autoadd").get_text() - new_core_config["compact_allocation"] = \ self.glade.get_widget("radio_compact_allocation").get_active() new_core_config["prioritize_first_last_pieces"] = \ self.glade.get_widget( "chk_prioritize_first_last_pieces").get_active() + new_core_config["sequential_download"] = \ + self.glade.get_widget("chk_sequential_download").get_active() + new_core_config["sequential_download"] = \ + self.glade.get_widget("radio_compact_allocation").get_active() and \ + False or self.glade.get_widget("chk_sequential_download").get_active() new_core_config["add_paused"] = \ self.glade.get_widget("chk_add_paused").get_active() @@ -637,14 +723,18 @@ class Preferences(component.Component): self.glade.get_widget("chk_min_on_close").get_active() new_gtkui_config["start_in_tray"] = \ self.glade.get_widget("chk_start_in_tray").get_active() + new_gtkui_config["enable_appindicator"] = \ + self.glade.get_widget("chk_enable_appindicator").get_active() new_gtkui_config["lock_tray"] = \ self.glade.get_widget("chk_lock_tray").get_active() passhex = sha_hash(\ self.glade.get_widget("txt_tray_password").get_text()).hexdigest() if passhex != "c07eb5a8c0dc7bb81c217b67f11c3b7a5e95ffd7": new_gtkui_config["tray_password"] = passhex - new_gtkui_config["classic_mode"] = \ - self.glade.get_widget("chk_classic_mode").get_active() + + new_gtkui_in_classic_mode = self.glade.get_widget("chk_classic_mode").get_active() + new_gtkui_config["classic_mode"] = new_gtkui_in_classic_mode + new_gtkui_config["show_rate_in_title"] = \ self.glade.get_widget("chk_show_rate_in_title").get_active() @@ -740,6 +830,27 @@ class Preferences(component.Component): # Re-show the dialog to make sure everything has been updated self.show() + if classic_mode_was_set == True and new_gtkui_in_classic_mode == False: + def on_response(response): + if response == gtk.RESPONSE_NO: + # Set each changed config value in the core + self.gtkui_config["classic_mode"] = True + self.glade.get_widget("chk_classic_mode").set_active(True) + else: + client.disconnect() + component.stop() + dialog = dialogs.YesNoDialog( + _("Attention"), + _("Your current session will be stopped. Continue?") + ) + dialog.run().addCallback(on_response) + elif classic_mode_was_set == False and new_gtkui_in_classic_mode == True: + dialog = dialogs.InformationDialog( + _("Attention"), + _("You must now restart the deluge UI") + ) + dialog.run() + def hide(self): self.glade.get_widget("port_img").hide() self.pref_dialog.hide() @@ -782,6 +893,7 @@ class Preferences(component.Component): "spin_outgoing_port_max": False}, "chk_use_tray": {"chk_min_on_close": True, "chk_start_in_tray": True, + "chk_enable_appindicator": True, "chk_lock_tray": True}, "chk_lock_tray": {"txt_tray_password": True, "password_label": True}, @@ -790,7 +902,6 @@ class Preferences(component.Component): "chk_move_completed" : {"move_completed_path_button" : True}, "chk_copy_torrent_file" : {"torrent_files_button" : True, "chk_del_copy_torrent_file" : True}, - "chk_autoadd" : {"folder_autoadd" : True}, "chk_seed_ratio" : {"spin_share_ratio": True, "chk_remove_ratio" : True} } @@ -827,12 +938,17 @@ class Preferences(component.Component): # Show the correct notebook page based on what row is selected. (model, row) = treeselection.get_selected() try: + if model.get_value(row, 1) == _("Daemon"): + # Let's see update the accounts related stuff + if client.connected(): + self._get_accounts_tab_data() self.notebook.set_current_page(model.get_value(row, 0)) except TypeError: pass def on_test_port_clicked(self, data): log.debug("on_test_port_clicked") + def on_get_test(status): if status: self.glade.get_widget("port_img").set_from_stock(gtk.STOCK_YES, 4) @@ -841,6 +957,8 @@ class Preferences(component.Component): self.glade.get_widget("port_img").set_from_stock(gtk.STOCK_DIALOG_WARNING, 4) self.glade.get_widget("port_img").show() client.core.test_listen_port().addCallback(on_get_test) + # XXX: Consider using gtk.Spinner() instead of the loading gif + # It requires gtk.ver > 2.12 self.glade.get_widget("port_img").set_from_file( deluge.common.get_pixmap('loading.gif') ) @@ -956,3 +1074,231 @@ class Preferences(component.Component): def _on_button_associate_magnet_clicked(self, widget): common.associate_magnet_links(True) + + + def _get_accounts_tab_data(self): + def on_ok(accounts): + self.accounts_frame.show() + self._on_get_known_accounts(accounts) + + def on_fail(failure): + if failure.value.exception_type == 'NotAuthorizedError': + self.accounts_frame.hide() + else: + dialogs.ErrorDialog( + _("Server Side Error"), + _("An error ocurred on the server"), + self.pref_dialog, details=failure.value.logable() + ).run() + client.core.get_known_accounts().addCallback(on_ok).addErrback(on_fail) + + def _on_get_known_accounts(self, known_accounts): + known_accounts_to_log = [] + for account in known_accounts: + account_to_log = {} + for key, value in account.copy().iteritems(): + if key == 'password': + value = '*' * len(value) + account_to_log[key] = value + known_accounts_to_log.append(account_to_log) + log.debug("_on_known_accounts: %s", known_accounts_to_log) + + self.accounts_liststore.clear() + + for account in known_accounts: + iter = self.accounts_liststore.append() + self.accounts_liststore.set_value( + iter, ACCOUNTS_USERNAME, account['username'] + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_LEVEL, account['authlevel'] + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_PASSWORD, account['password'] + ) + + def _on_accounts_selection_changed(self, treeselection): + log.debug("_on_accounts_selection_changed") + (model, itr) = treeselection.get_selected() + if not itr: + return + username = model[itr][0] + if username: + self.glade.get_widget("accounts_edit").set_sensitive(True) + self.glade.get_widget("accounts_delete").set_sensitive(True) + else: + self.glade.get_widget("accounts_edit").set_sensitive(False) + self.glade.get_widget("accounts_delete").set_sensitive(False) + + def _on_accounts_add_clicked(self, widget): + dialog = dialogs.AccountDialog( + levels_mapping=client.auth_levels_mapping, + parent=self.pref_dialog + ) + + def dialog_finished(response_id): + username = dialog.get_username() + password = dialog.get_password() + authlevel = dialog.get_authlevel() + + def add_ok(rv): + iter = self.accounts_liststore.append() + self.accounts_liststore.set_value( + iter, ACCOUNTS_USERNAME, username + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_LEVEL, authlevel + ) + self.accounts_liststore.set_value( + iter, ACCOUNTS_PASSWORD, password + ) + + def add_fail(failure): + if failure.value.exception_type == 'AuthManagerError': + dialogs.ErrorDialog( + _("Error Adding Account"), + failure.value.exception_msg + ).run() + else: + dialogs.ErrorDialog( + _("Error Adding Account"), + _("An error ocurred while adding account"), + self.pref_dialog, details=failure.value.logable() + ).run() + + if response_id == gtk.RESPONSE_OK: + client.core.create_account( + username, password, authlevel + ).addCallback(add_ok).addErrback(add_fail) + + dialog.run().addCallback(dialog_finished) + + def _on_accounts_edit_clicked(self, widget): + (model, itr) = self.accounts_listview.get_selection().get_selected() + if not itr: + return + + dialog = dialogs.AccountDialog( + model[itr][ACCOUNTS_USERNAME], + model[itr][ACCOUNTS_PASSWORD], + model[itr][ACCOUNTS_LEVEL], + levels_mapping=client.auth_levels_mapping, + parent=self.pref_dialog + ) + + def dialog_finished(response_id): + + def update_ok(rc): + model.set_value(itr, ACCOUNTS_PASSWORD, dialog.get_username()) + model.set_value(itr, ACCOUNTS_LEVEL, dialog.get_authlevel()) + + def update_fail(failure): + dialogs.ErrorDialog( + _("Error Updating Account"), + _("An error ocurred while updating account"), + self.pref_dialog, details=failure.value.logable() + ).run() + + if response_id == gtk.RESPONSE_OK: + client.core.update_account( + dialog.get_username(), + dialog.get_password(), + dialog.get_authlevel() + ).addCallback(update_ok).addErrback(update_fail) + + dialog.run().addCallback(dialog_finished) + + def _on_accounts_delete_clicked(self, widget): + (model, itr) = self.accounts_listview.get_selection().get_selected() + if not itr: + return + + username = model[itr][0] + header = _("Remove Account") + text = _("Are you sure you wan't do remove the account with the " + "username \"%(username)s\"?" % dict(username=username)) + dialog = dialogs.YesNoDialog(header, text, parent=self.pref_dialog) + + def dialog_finished(response_id): + def remove_ok(rc): + model.remove(itr) + + def remove_fail(failure): + if failure.value.exception_type == 'AuthManagerError': + dialogs.ErrorDialog( + _("Error Removing Account"), + failure.value.exception_msg + ).run() + else: + dialogs.ErrorDialog( + _("Error Removing Account"), + _("An error ocurred while removing account"), + self.pref_dialog, details=failure.value.logable() + ).run() + if response_id == gtk.RESPONSE_YES: + client.core.remove_account( + username + ).addCallback(remove_ok).addErrback(remove_fail) + dialog.run().addCallback(dialog_finished) + + def _on_alocation_toggled(self, widget): + full_allocation_active = self.glade.get_widget("radio_full_allocation").get_active() + self.glade.get_widget("chk_prioritize_first_last_pieces").set_sensitive(full_allocation_active) + self.glade.get_widget("chk_sequential_download").set_sensitive(full_allocation_active) + + def _on_piecesbar_toggle_toggled(self, widget): + self.gtkui_config['show_piecesbar'] = widget.get_active() + colors_widget = self.glade.get_widget("piecebar_colors_expander") + colors_widget.set_visible(widget.get_active()) + + def _on_completed_color_set(self, widget): + self.__set_color("completed") + + def _on_revert_color_completed_clicked(self, widget): + self.__revert_color("completed") + + def _on_downloading_color_set(self, widget): + self.__set_color("downloading") + + def _on_revert_color_downloading_clicked(self, widget): + self.__revert_color("downloading") + + def _on_waiting_color_set(self, widget): + self.__set_color("waiting") + + def _on_revert_color_waiting_clicked(self, widget): + self.__revert_color("waiting") + + def _on_missing_color_set(self, widget): + self.__set_color("missing") + + def _on_revert_color_missing_clicked(self, widget): + self.__revert_color("missing") + + def __set_color(self, state, from_config=False): + if from_config: + color = gtk.gdk.Color(*self.gtkui_config["pieces_color_%s" % state]) + log.debug("Setting %r color state from config to %s", state, + (color.red, color.green, color.blue)) + self.glade.get_widget("%s_color" % state).set_color(color) + else: + color = self.glade.get_widget("%s_color" % state).get_color() + log.debug("Setting %r color state to %s", state, + (color.red, color.green, color.blue)) + self.gtkui_config["pieces_color_%s" % state] = [ + color.red, color.green, color.blue + ] + self.gtkui_config.save() + self.gtkui_config.apply_set_functions("pieces_colors") + + self.glade.get_widget("revert_color_%s" % state).set_sensitive( + [color.red, color.green, color.blue] != self.COLOR_DEFAULTS[state] + ) + + def __revert_color(self, state, from_config=False): + log.debug("Reverting %r color state", state) + self.glade.get_widget("%s_color" % state).set_color( + gtk.gdk.Color(*self.COLOR_DEFAULTS[state]) + ) + self.glade.get_widget("revert_color_%s" % state).set_sensitive(False) + self.gtkui_config.apply_set_functions("pieces_colors") diff --git a/deluge/ui/gtkui/status_tab.py b/deluge/ui/gtkui/status_tab.py index 255c303cc..01c062750 100644 --- a/deluge/ui/gtkui/status_tab.py +++ b/deluge/ui/gtkui/status_tab.py @@ -42,7 +42,9 @@ import logging from deluge.ui.client import client import deluge.component as component import deluge.common +from deluge.configmanager import ConfigManager from deluge.ui.gtkui.torrentdetails import Tab +from deluge.ui.gtkui.piecesbar import PiecesBar log = logging.getLogger(__name__) @@ -70,13 +72,19 @@ class StatusTab(Tab): def __init__(self): Tab.__init__(self) # Get the labels we need to update. - # widgetname, modifier function, status keys - glade = component.get("MainWindow").main_glade + # widget name, modifier function, status keys + self.glade = glade = component.get("MainWindow").main_glade + self.progressbar = component.get("MainWindow").main_glade.get_widget("progressbar") self._name = "Status" self._child_widget = glade.get_widget("status_tab") self._tab_label = glade.get_widget("status_tab_label") - + self.config = ConfigManager("gtkui.conf") + self.config.register_set_function( + "show_piecesbar", + self.on_show_pieces_bar_config_changed, + apply_now=True + ) self.label_widgets = [ (glade.get_widget("summary_pieces"), fpeer_size_second, ("num_pieces", "piece_length")), (glade.get_widget("summary_availability"), fratio, ("distributed_copies",)), @@ -118,6 +126,9 @@ class StatusTab(Tab): "tracker_status", "max_connections", "max_upload_slots", "max_upload_speed", "max_download_speed", "active_time", "seeding_time", "seed_rank", "is_auto_managed", "time_added"] + if self.config['show_piecesbar']: + status_keys.extend(["pieces", "state"]) + component.get("SessionProxy").get_torrent_status( selected, status_keys).addCallback(self._on_get_torrent_status) @@ -127,6 +138,11 @@ class StatusTab(Tab): if status is None: return + if status["is_auto_managed"]: + status["is_auto_managed"]=_("On") + else: + status["is_auto_managed"]=_("Off") + # Update all the label widgets for widget in self.label_widgets: if widget[1] != None: @@ -146,13 +162,37 @@ class StatusTab(Tab): widget[0].set_text(txt) # Do the progress bar because it's a special case (not a label) - w = component.get("MainWindow").main_glade.get_widget("progressbar") - fraction = status["progress"] / 100 - if w.get_fraction() != fraction: - w.set_fraction(fraction) + if self.config['show_piecesbar']: + self.piecesbar.update_from_status(status) + else: + fraction = status["progress"] / 100 + if self.progressbar.get_fraction() != fraction: + self.progressbar.set_fraction(fraction) + + def on_show_pieces_bar_config_changed(self, key, show): + self.show_pieces_bar(show) + + def show_pieces_bar(self, show): + if hasattr(self, 'piecesbar'): + if show: + self.piecesbar.show() + self.progressbar.hide() + else: + self.piecesbar.hide() + self.progressbar.show() + else: + if show: + self.piecesbar = PiecesBar() + self.glade.get_widget("status_progress_vbox").pack_start( + self.piecesbar, False, False, 5 + ) + self.progressbar.hide() def clear(self): for widget in self.label_widgets: widget[0].set_text("") - component.get("MainWindow").main_glade.get_widget("progressbar").set_fraction(0.0) + if self.config['show_piecesbar']: + self.piecesbar.clear() + else: + self.progressbar.set_fraction(0.0) diff --git a/deluge/ui/gtkui/statusbar.py b/deluge/ui/gtkui/statusbar.py index 52f8c7fb5..60202a213 100644 --- a/deluge/ui/gtkui/statusbar.py +++ b/deluge/ui/gtkui/statusbar.py @@ -288,14 +288,14 @@ class StatusBar(component.Component): client.core.get_session_status(keys).addCallback(self._on_get_session_status) client.core.get_free_space().addCallback(self._on_get_free_space) - def on_configvaluechanged_event(self, event): + def on_configvaluechanged_event(self, key, value): """ This is called when we receive a ConfigValueChangedEvent from the core. """ - if event.key in self.config_value_changed_dict.keys(): - self.config_value_changed_dict[event.key](event.value) + if key in self.config_value_changed_dict.keys(): + self.config_value_changed_dict[key](value) def _on_max_connections_global(self, max_connections): self.max_connections = max_connections diff --git a/deluge/ui/gtkui/systemtray.py b/deluge/ui/gtkui/systemtray.py index 5a193358e..b465c5423 100644 --- a/deluge/ui/gtkui/systemtray.py +++ b/deluge/ui/gtkui/systemtray.py @@ -33,6 +33,10 @@ # # +try: + import appindicator +except ImportError: + appindicator = None import gtk import logging @@ -65,7 +69,11 @@ class SystemTray(component.Component): "separatormenuitem4" ] self.config.register_set_function("enable_system_tray", - self.on_enable_system_tray_set) + self.on_enable_system_tray_set) + # bit of a hack to prevent function from doing something on startup + self.__enabled_set_once = False + self.config.register_set_function("enable_appindicator", + self.on_enable_appindicator_set) self.max_download_speed = -1.0 self.download_rate = 0.0 @@ -79,25 +87,10 @@ class SystemTray(component.Component): def enable(self): """Enables the system tray icon.""" - log.debug("Enabling the system tray icon..") self.tray_glade = gtk.glade.XML( pkg_resources.resource_filename("deluge.ui.gtkui", "glade/tray_menu.glade")) - if deluge.common.windows_check() or deluge.common.osx_check(): - self.tray = gtk.status_icon_new_from_pixbuf( - common.get_logo(32)) - else: - try: - self.tray = gtk.status_icon_new_from_icon_name("deluge") - except: - log.warning("Update PyGTK to 2.10 or greater for SystemTray..") - return - - self.tray.connect("activate", self.on_tray_clicked) - self.tray.connect("popup-menu", self.on_tray_popup) - - self.tray_glade.signal_autoconnect({ "on_menuitem_show_deluge_activate": \ self.on_menuitem_show_deluge_activate, @@ -114,20 +107,53 @@ class SystemTray(component.Component): self.tray_menu = self.tray_glade.get_widget("tray_menu") - self.tray_glade.get_widget("download-limit-image").set_from_file( - deluge.common.get_pixmap("downloading16.png")) - self.tray_glade.get_widget("upload-limit-image").set_from_file( - deluge.common.get_pixmap("seeding16.png")) + if appindicator and self.config["enable_appindicator"]: + log.debug("Enabling the Application Indicator..") + self.indicator = appindicator.Indicator ( + "deluge", "deluge", appindicator.CATEGORY_APPLICATION_STATUS) + # Pass the menu to the Application Indicator + self.indicator.set_menu(self.tray_menu) + + # Make sure the status of the Show Window MenuItem is correct + self._sig_win_hide = self.window.window.connect("hide", self._on_window_hide) + self._sig_win_show = self.window.window.connect("show", self._on_window_show) + if self.window.visible(): + self.tray_glade.get_widget("menuitem_show_deluge").set_active(True) + else: + self.tray_glade.get_widget("menuitem_show_deluge").set_active(False) + + # Show the Application Indicator + self.indicator.set_status(appindicator.STATUS_ACTIVE) + + else: + log.debug("Enabling the system tray icon..") + if deluge.common.windows_check() or deluge.common.osx_check(): + self.tray = gtk.status_icon_new_from_pixbuf( + common.get_logo(32)) + else: + try: + self.tray = gtk.status_icon_new_from_icon_name("deluge") + except: + log.warning("Update PyGTK to 2.10 or greater for SystemTray..") + return + + self.tray.connect("activate", self.on_tray_clicked) + self.tray.connect("popup-menu", self.on_tray_popup) + + # For some reason these icons do not display in appindicator + self.tray_glade.get_widget("download-limit-image").set_from_file( + deluge.common.get_pixmap("downloading16.png")) + self.tray_glade.get_widget("upload-limit-image").set_from_file( + deluge.common.get_pixmap("seeding16.png")) client.register_event_handler("ConfigValueChangedEvent", self.config_value_changed) - if not client.connected(): - # Hide menu widgets because we're not connected to a host. - for widget in self.hide_widget_list: - self.tray_glade.get_widget(widget).hide() - if client.connected(): # We're connected so we need to get some values from the core self.__start() + else: + # Hide menu widgets because we're not connected to a host. + for widget in self.hide_widget_list: + self.tray_glade.get_widget(widget).hide() def __start(self): if self.config["enable_system_tray"]: @@ -138,6 +164,16 @@ class SystemTray(component.Component): self.tray_glade.get_widget("menuitem_quitdaemon").hide() self.tray_glade.get_widget("separatormenuitem4").hide() + # These do not work with appindicator currently and can crash Deluge. + # Related to Launchpad bug #608219 + if appindicator and self.config["enable_appindicator"]: + self.hide_widget_list.remove("menuitem_download_limit") + self.hide_widget_list.remove("menuitem_upload_limit") + self.hide_widget_list.remove("separatormenuitem3") + self.tray_glade.get_widget("menuitem_download_limit").hide() + self.tray_glade.get_widget("menuitem_upload_limit").hide() + self.tray_glade.get_widget("separatormenuitem3").hide() + # Show widgets in the hide list because we've connected to a host for widget in self.hide_widget_list: self.tray_glade.get_widget(widget).show() @@ -168,19 +204,22 @@ class SystemTray(component.Component): def shutdown(self): if self.config["enable_system_tray"]: - self.tray.set_visible(False) + if appindicator and self.config["enable_appindicator"]: + self.indicator.set_status(appindicator.STATUS_PASSIVE) + else: + self.tray.set_visible(False) def send_status_request(self): client.core.get_session_status([ "payload_upload_rate", "payload_download_rate"]).addCallback(self._on_get_session_status) - def config_value_changed(self, event): + def config_value_changed(self, key, value): """This is called when we received a config_value_changed signal from the core.""" - if event.key in self.config_value_changed_dict.keys(): - self.config_value_changed_dict[event.key](event.value) + if key in self.config_value_changed_dict.keys(): + self.config_value_changed_dict[key](value) def _on_max_download_speed(self, max_download_speed): if self.max_download_speed != max_download_speed: @@ -200,6 +239,10 @@ class SystemTray(component.Component): if not self.config["enable_system_tray"]: return + # Tool tip text not available for appindicator + if appindicator and self.config["enable_appindicator"]: + return + # Set the tool tip text max_download_speed = self.max_download_speed max_upload_speed = self.max_upload_speed @@ -245,12 +288,29 @@ class SystemTray(component.Component): submenu_bwdownset.show_all() submenu_bwupset.show_all() - def disable(self): - """Disables the system tray icon.""" - log.debug("Disabling the system tray icon..") + # Re-set the menu to partly work around Launchpad bug #608219 + if appindicator and self.config["enable_appindicator"]: + self.indicator.set_menu(self.tray_menu) + + def disable(self,invert_app_ind_conf=False): + """Disables the system tray icon or appindicator.""" try: - self.tray.set_visible(False) - del self.tray + if invert_app_ind_conf: + app_ind_conf = not self.config["enable_appindicator"] + else: + app_ind_conf = self.config["enable_appindicator"] + if appindicator and app_ind_conf: + if hasattr(self, "_sig_win_hide"): + self.window.window.disconnect(self._sig_win_hide) + self.window.window.disconnect(self._sig_win_show) + log.debug("Disabling the application indicator..") + + self.indicator.set_status(appindicator.STATUS_PASSIVE) + del self.indicator + else: + log.debug("Disabling the system tray icon..") + self.tray.set_visible(False) + del self.tray del self.tray_glade del self.tray_menu except Exception, e: @@ -271,6 +331,13 @@ class SystemTray(component.Component): else: self.disable() + def on_enable_appindicator_set(self, key, value): + """Called whenever the 'enable_appindicator' config key is modified""" + if self.__enabled_set_once: + self.disable(True) + self.enable() + self.__enabled_set_once = True + def on_tray_clicked(self, icon): """Called when the tray icon is left clicked.""" self.blink(False) @@ -338,6 +405,16 @@ class SystemTray(component.Component): self.setbwlimit(widget, _("Set Maximum Download Speed"), "max_download_speed", "tray_download_speed_list", self.max_download_speed, "downloading.svg") + def _on_window_hide(self, widget, data=None): + """_on_window_hide - update the menuitem's status""" + log.debug("_on_window_hide") + self.tray_glade.get_widget("menuitem_show_deluge").set_active(False) + + def _on_window_show(self, widget, data=None): + """_on_window_show - update the menuitem's status""" + log.debug("_on_window_show") + self.tray_glade.get_widget("menuitem_show_deluge").set_active(True) + def tray_setbwup(self, widget, data=None): self.setbwlimit(widget, _("Set Maximum Upload Speed"), "max_upload_speed", "tray_upload_speed_list", self.max_upload_speed, "seeding.svg") diff --git a/deluge/ui/gtkui/toolbar.py b/deluge/ui/gtkui/toolbar.py index 5099fd54d..858b7e86e 100644 --- a/deluge/ui/gtkui/toolbar.py +++ b/deluge/ui/gtkui/toolbar.py @@ -74,7 +74,9 @@ class ToolBar(component.Component): "toolbutton_pause", "toolbutton_resume", "toolbutton_queue_up", - "toolbutton_queue_down" + "toolbutton_queue_down", + "toolbutton_filter", + "find_menuitem" ] self.config.register_set_function("classic_mode", self._on_classic_mode, True) diff --git a/deluge/ui/gtkui/torrentview.py b/deluge/ui/gtkui/torrentview.py index 52590afae..4f8217350 100644 --- a/deluge/ui/gtkui/torrentview.py +++ b/deluge/ui/gtkui/torrentview.py @@ -43,8 +43,11 @@ import gtk.glade import gettext import gobject import logging +import warnings from urlparse import urlparse +from twisted.internet import reactor + import deluge.common import deluge.component as component from deluge.ui.client import client @@ -77,15 +80,19 @@ ICON_STATE = { "Seeding": icon_seeding, "Paused": icon_inactive, "Error": icon_alert, - "Queued": icon_queued + "Queued": icon_queued, + "Checking Resume Data": icon_checking } def cell_data_statusicon(column, cell, model, row, data): """Display text with an icon""" try: icon = ICON_STATE[model.get_value(row, data)] - if cell.get_property("pixbuf") != icon: - cell.set_property("pixbuf", icon) + #Supress Warning: g_object_set_qdata: assertion `G_IS_OBJECT (object)' failed + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + if cell.get_property("pixbuf") != icon: + cell.set_property("pixbuf", icon) except KeyError: pass @@ -104,8 +111,11 @@ def cell_data_trackericon(column, cell, model, row, data): else: icon = create_blank_icon() - if cell.get_property("pixbuf") != icon: - cell.set_property("pixbuf", icon) + #Supress Warning: g_object_set_qdata: assertion `G_IS_OBJECT (object)' failed + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + if cell.get_property("pixbuf") != icon: + cell.set_property("pixbuf", icon) host = model[row][data] if host: @@ -131,7 +141,7 @@ def cell_data_queue(column, cell, model, row, data): if value < 0: cell.set_property("text", "") else: - cell.set_property("text", value + 1) + cell.set_property("text", str(value + 1)) def queue_peer_seed_sort_function(v1, v2): if v1 == v2: @@ -174,6 +184,135 @@ def seed_peer_column_sort(model, iter1, iter2, data): return queue_peer_seed_sort_function(v2, v4) return queue_peer_seed_sort_function(v1, v3) +class SearchBox(object): + def __init__(self, torrentview): + self.torrentview = torrentview + self.window = torrentview.window + + self.visible = False + self.search_pending = self.prefiltered = None + + self.search_box = self.window.main_glade.get_widget("search_box") + self.search_torrents_entry = self.window.main_glade.get_widget("search_torrents_entry") + self.close_search_button = self.window.main_glade.get_widget("close_search_button") + self.match_search_button = self.window.main_glade.get_widget("search_torrents_match") + self.window.main_glade.signal_autoconnect(self) + + def show(self): + self.visible = True + self.search_box.show_all() + self.search_torrents_entry.grab_focus() + + def hide(self): + self.visible = False + self.clear_search() + self.search_box.hide_all() + self.search_pending = self.prefiltered = None + + def clear_search(self): + if self.search_pending and self.search_pending.active(): + self.search_pending.cancel() + + if self.prefiltered: + filter_column = self.torrentview.columns["filter"].column_indices[0] + torrent_id_column = self.torrentview.columns["torrent_id"].column_indices[0] + for row in self.torrentview.liststore: + torrent_id = row[torrent_id_column] + + if torrent_id in self.prefiltered: + # Reset to previous filter state + self.prefiltered.pop(self.prefiltered.index(torrent_id)) + row[filter_column] = not row[filter_column] + + self.prefiltered = None + + self.search_torrents_entry.set_text("") + if self.torrentview.filter and 'name' in self.torrentview.filter: + self.torrentview.filter.pop('name', None) + self.search_pending = reactor.callLater(0.5, self.torrentview.update) + + def set_search_filter(self): + if self.search_pending and self.search_pending.active(): + self.search_pending.cancel() + + if self.torrentview.filter and 'name' in self.torrentview.filter: + self.torrentview.filter.pop('name', None) + + elif self.torrentview.filter is None: + self.torrentview.filter = {} + + search_string = self.search_torrents_entry.get_text() + if not search_string: + self.clear_search() + else: + if self.match_search_button.get_active(): + search_string += '::match' + self.torrentview.filter['name'] = search_string + self.prefilter_torrentview() + + def prefilter_torrentview(self): + filter_column = self.torrentview.columns["filter"].column_indices[0] + torrent_id_column = self.torrentview.columns["torrent_id"].column_indices[0] + torrent_name_column = self.torrentview.columns[_("Name")].column_indices[1] + + match_case = self.match_search_button.get_active() + if match_case: + search_string = self.search_torrents_entry.get_text() + else: + search_string = self.search_torrents_entry.get_text().lower() + + if self.prefiltered is None: + self.prefiltered = [] + + for row in self.torrentview.liststore: + torrent_id = row[torrent_id_column] + + if torrent_id in self.prefiltered: + # Reset to previous filter state + self.prefiltered.pop(self.prefiltered.index(torrent_id)) + row[filter_column] = not row[filter_column] + + + if not row[filter_column]: + # Row is not visible(filtered out, but not by our filter), skip it + continue + + if match_case: + torrent_name = row[torrent_name_column] + else: + torrent_name = row[torrent_name_column].lower() + + if search_string in torrent_name and not row[filter_column]: + row[filter_column] = True + self.prefiltered.append(torrent_id) + elif search_string not in torrent_name and row[filter_column]: + row[filter_column] = False + self.prefiltered.append(torrent_id) + + def on_close_search_button_clicked(self, widget): + self.hide() + + def on_search_filter_toggle(self, widget): + if self.visible: + self.hide() + else: + self.show() + + def on_search_torrents_match_toggled(self, widget): + if self.search_torrents_entry.get_text(): + self.set_search_filter() + self.search_pending = reactor.callLater(0.7, self.torrentview.update) + + def on_search_torrents_entry_icon_press(self, entry, icon, event): + if icon != gtk.ENTRY_ICON_SECONDARY: + return + self.clear_search() + + def on_search_torrents_entry_changed(self, widget): + self.set_search_filter() + self.search_pending = reactor.callLater(0.7, self.torrentview.update) + + class TorrentView(listview.ListView, component.Component): """TorrentView handles the listing of torrents.""" def __init__(self): @@ -197,7 +336,8 @@ class TorrentView(listview.ListView, component.Component): # Register the columns menu with the listview so it gets updated # accordingly. self.register_checklist_menu( - self.window.main_glade.get_widget("menu_columns")) + self.window.main_glade.get_widget("menu_columns") + ) # Add the columns to the listview self.add_text_column("torrent_id", hidden=True) @@ -211,34 +351,53 @@ class TorrentView(listview.ListView, component.Component): self.add_func_column(_("Size"), listview.cell_data_size, [gobject.TYPE_UINT64], status_field=["total_wanted"]) + self.add_func_column(_("Downloaded"), listview.cell_data_size, + [gobject.TYPE_UINT64], + status_field=["all_time_download"], default=False) + self.add_func_column(_("Uploaded"), listview.cell_data_size, + [gobject.TYPE_UINT64], + status_field=["total_uploaded"], default=False) self.add_progress_column(_("Progress"), status_field=["progress", "state"], col_types=[float, str], function=cell_data_progress) self.add_func_column(_("Seeders"), listview.cell_data_peer, [int, int], status_field=["num_seeds", "total_seeds"], - sort_func=seed_peer_column_sort) + sort_func=seed_peer_column_sort, default=False) self.add_func_column(_("Peers"), listview.cell_data_peer, [int, int], status_field=["num_peers", "total_peers"], - sort_func=seed_peer_column_sort) + sort_func=seed_peer_column_sort, default=False) + self.add_func_column(_("Seeders") + "/" + _("Peers"), listview.cell_data_ratio, [float], + status_field=["seeds_peers_ratio"], default=False) self.add_func_column(_("Down Speed"), listview.cell_data_speed, [float], status_field=["download_payload_rate"]) self.add_func_column(_("Up Speed"), listview.cell_data_speed, [float], status_field=["upload_payload_rate"]) + self.add_func_column(_("Down Limit"), listview.cell_data_speed_limit, [float], + status_field=["max_download_speed"], default=False) + self.add_func_column(_("Up Limit"), listview.cell_data_speed_limit, [float], + status_field=["max_upload_speed"], default=False) self.add_func_column(_("ETA"), listview.cell_data_time, [int], status_field=["eta"], sort_func=eta_column_sort) self.add_func_column(_("Ratio"), listview.cell_data_ratio, [float], - status_field=["ratio"]) + status_field=["ratio"], default=False) self.add_func_column(_("Avail"), listview.cell_data_ratio, [float], - status_field=["distributed_copies"]) + status_field=["distributed_copies"], default=False) self.add_func_column(_("Added"), listview.cell_data_date, [float], - status_field=["time_added"]) + status_field=["time_added"], default=False) + self.add_func_column(_("Last Seen Complete"), + listview.cell_data_date_or_never, [float], + status_field=["last_seen_complete"], default=False) self.add_texticon_column(_("Tracker"), status_field=["tracker_host", "tracker_host"], - function=cell_data_trackericon) - self.add_text_column(_("Save Path"), status_field=["save_path"]) - self.add_text_column(_("Owner"), status_field=["owner"]) - self.add_bool_column(_("Public"), status_field=["public"]) + function=cell_data_trackericon, default=False) + self.add_text_column(_("Save Path"), status_field=["save_path"], default=False) + self.add_text_column(_("Owner"), status_field=["owner"], default=False) + self.add_bool_column(_("Public"), status_field=["public"], default=False) + self.restore_columns_order_from_state() + self.add_bool_column(_("Shared"), status_field=["shared"], + tooltip=_("Torrent is shared between other Deluge " + "users or not."), default=False) # Set filter to None for now self.filter = None @@ -246,18 +405,18 @@ class TorrentView(listview.ListView, component.Component): ### Connect Signals ### # Connect to the 'button-press-event' to know when to bring up the # torrent menu popup. - self.treeview.connect("button-press-event", - self.on_button_press_event) + self.treeview.connect("button-press-event", self.on_button_press_event) # Connect to the 'key-press-event' to know when the bring up the # torrent menu popup via keypress. self.treeview.connect("key-release-event", self.on_key_press_event) # Connect to the 'changed' event of TreeViewSelection to get selection # changes. self.treeview.get_selection().connect("changed", - self.on_selection_changed) + self.on_selection_changed) self.treeview.connect("drag-drop", self.on_drag_drop) self.treeview.connect("key-press-event", self.on_key_press_event) + self.treeview.connect("columns-changed", self.on_columns_changed_event) client.register_event_handler("TorrentStateChangedEvent", self.on_torrentstatechanged_event) client.register_event_handler("TorrentAddedEvent", self.on_torrentadded_event) @@ -266,6 +425,8 @@ class TorrentView(listview.ListView, component.Component): client.register_event_handler("SessionResumedEvent", self.on_sessionresumed_event) client.register_event_handler("TorrentQueueChangedEvent", self.on_torrentqueuechanged_event) + self.search_box = SearchBox(self) + def start(self): """Start the torrentview""" # We need to get the core session state to know which torrents are in @@ -291,6 +452,8 @@ class TorrentView(listview.ListView, component.Component): # We need to clear the liststore self.liststore.clear() self.prev_status = {} + self.filter = None + self.search_box.hide() def shutdown(self): """Called when GtkUi is exiting""" @@ -307,7 +470,10 @@ class TorrentView(listview.ListView, component.Component): """Sets filters for the torrentview.. see: core.get_torrents_status """ + search_filter = self.filter and self.filter.get('name', None) or None self.filter = dict(filter_dict) #copied version of filter_dict. + if search_filter and 'name' not in filter_dict: + self.filter['name'] = search_filter self.update() def set_columns_to_update(self, columns=None): @@ -351,6 +517,9 @@ class TorrentView(listview.ListView, component.Component): def update(self): if self.got_state: + if self.search_box.search_pending is not None and self.search_box.search_pending.active(): + # An update request is scheduled, let's wait for that one + return # Send a status request gobject.idle_add(self.send_status_request) @@ -384,7 +553,10 @@ class TorrentView(listview.ListView, component.Component): if row[column_index[i]] != row_value: row[column_index[i]] = row_value except Exception, e: - log.debug("%s", e) + log.debug("Error while updating row for column " + "index %d, status field %r, value %r:" + " %s", column_index[0], status_field, + row_value, e) component.get("MenuBar").update_menu() @@ -394,6 +566,8 @@ class TorrentView(listview.ListView, component.Component): """Callback function for get_torrents_status(). 'status' should be a dictionary of {torrent_id: {key, value}}.""" self.status = status + if self.search_box.prefiltered is not None: + self.search_box.prefiltered = None if self.status == self.prev_status and self.prev_status: # We do not bother updating since the status hasn't changed self.prev_status = self.status @@ -410,10 +584,7 @@ class TorrentView(listview.ListView, component.Component): # Insert a new row to the liststore row = self.liststore.append() # Store the torrent id - self.liststore.set_value( - row, - self.columns["torrent_id"].column_indices[0], - torrent_id) + self.liststore.set_value(row, self.columns["torrent_id"].column_indices[0], torrent_id) if update: self.update() @@ -471,7 +642,7 @@ class TorrentView(listview.ListView, component.Component): return [] return torrent_ids - except ValueError, TypeError: + except (ValueError, TypeError): return [] def get_torrent_status(self, torrent_id): @@ -515,40 +686,45 @@ class TorrentView(listview.ListView, component.Component): def on_drag_drop(self, widget, drag_context, x, y, timestamp): widget.stop_emission("drag-drop") - def on_torrentadded_event(self, event): - self.add_row(event.torrent_id) - self.mark_dirty(event.torrent_id) + def on_columns_changed_event(self, treeview): + log.debug("Treeview Columns Changed") + self.save_state() - def on_torrentremoved_event(self, event): - self.remove_row(event.torrent_id) + def on_torrentadded_event(self, torrent_id, from_state): + self.add_row(torrent_id) + self.mark_dirty(torrent_id) - def on_torrentstatechanged_event(self, event): + def on_torrentremoved_event(self, torrent_id): + self.remove_row(torrent_id) + + def on_torrentstatechanged_event(self, torrent_id, state): # Update the torrents state for row in self.liststore: - if not event.torrent_id == row[self.columns["torrent_id"].column_indices[0]]: + if not torrent_id == row[self.columns["torrent_id"].column_indices[0]]: continue - row[self.get_column_index(_("Progress"))[1]] = event.state + row[self.get_column_index(_("Progress"))[1]] = state - self.mark_dirty(event.torrent_id) + self.mark_dirty(torrent_id) - def on_sessionpaused_event(self, event): + def on_sessionpaused_event(self): self.mark_dirty() self.update() - def on_sessionresumed_event(self, event): + def on_sessionresumed_event(self): self.mark_dirty() self.update() - def on_torrentqueuechanged_event(self, event): + def on_torrentqueuechanged_event(self): self.mark_dirty() self.update() # Handle keyboard shortcuts def on_key_press_event(self, widget, event): keyname = gtk.gdk.keyval_name(event.keyval) - func = getattr(self, 'keypress_' + keyname, None) - if func: - return func(event) + if keyname is not None: + func = getattr(self, 'keypress_' + keyname, None) + if func: + return func(event) def keypress_Delete(self, event): log.debug("keypress_Delete") diff --git a/deluge/ui/sessionproxy.py b/deluge/ui/sessionproxy.py index 4fc53d85b..af485a458 100644 --- a/deluge/ui/sessionproxy.py +++ b/deluge/ui/sessionproxy.py @@ -75,11 +75,11 @@ class SessionProxy(component.Component): def on_torrent_status(status): # Save the time we got the torrent status t = time.time() - for key, value in status.items(): + for key, value in status.iteritems(): self.torrents[key] = [t, value] self.cache_times[key] = {} - for k, v in value.items(): - self.cache_times[key][k] = t + for ikey in value.iterkeys(): + self.cache_times[key][ikey] = t return client.core.get_torrents_status({}, [], True).addCallback(on_torrent_status) @@ -105,7 +105,10 @@ class SessionProxy(component.Component): sd = {} for torrent_id in torrent_ids: if keys: - sd[torrent_id] = dict([(x, y) for x, y in self.torrents[torrent_id][1].iteritems() if x in keys]) + sd[torrent_id] = dict([ + (x, y) for x, y in self.torrents[torrent_id][1].iteritems() + if x in keys + ]) else: sd[torrent_id] = dict(self.torrents[torrent_id][1]) @@ -135,7 +138,9 @@ class SessionProxy(component.Component): keys_to_get.append(key) if not keys_to_get: - return succeed(self.create_status_dict([torrent_id], keys)[torrent_id]) + return succeed( + self.create_status_dict([torrent_id], keys)[torrent_id] + ) else: d = client.core.get_torrent_status(torrent_id, keys_to_get, True) def on_status(result, torrent_id): @@ -236,21 +241,21 @@ class SessionProxy(component.Component): d = client.core.get_torrents_status(filter_dict, keys, True) return d.addCallback(on_status, None, keys) - def on_torrent_state_changed(self, event): - if event.torrent_id in self.torrents: - self.torrents[event.torrent_id][1]["state"] = event.state - self.cache_times[event.torrent_id]["state"] = time.time() + def on_torrent_state_changed(self, torrent_id, state): + if torrent_id in self.torrents: + self.torrents[torrent_id][1]["state"] = state + self.cache_times[torrent_id]["state"] = time.time() - def on_torrent_added(self, event): - self.torrents[event.torrent_id] = [time.time() - self.cache_time - 1, {}] - self.cache_times[event.torrent_id] = {} + def on_torrent_added(self, torrent_id, from_state): + self.torrents[torrent_id] = [time.time() - self.cache_time - 1, {}] + self.cache_times[torrent_id] = {} def on_status(status): - self.torrents[event.torrent_id][1].update(status) + self.torrents[torrent_id][1].update(status) t = time.time() for key in status: - self.cache_times[event.torrent_id][key] = t - client.core.get_torrent_status(event.torrent_id, []).addCallback(on_status) + self.cache_times[torrent_id][key] = t + client.core.get_torrent_status(torrent_id, []).addCallback(on_status) - def on_torrent_removed(self, event): - del self.torrents[event.torrent_id] - del self.cache_times[event.torrent_id] + def on_torrent_removed(self, torrent_id): + del self.torrents[torrent_id] + del self.cache_times[torrent_id] diff --git a/deluge/ui/web/auth.py b/deluge/ui/web/auth.py index 101bc628a..5cd345e2c 100644 --- a/deluge/ui/web/auth.py +++ b/deluge/ui/web/auth.py @@ -71,11 +71,11 @@ def get_session_id(session_id): """ if not session_id: return None - + try: checksum = int(session_id[-4:]) session_id = session_id[:-4] - + if checksum == make_checksum(session_id): return session_id return None @@ -93,32 +93,32 @@ class Auth(JSONComponent): """ The component that implements authentification into the JSON interface. """ - + def __init__(self): super(Auth, self).__init__("Auth") self.worker = LoopingCall(self._clean_sessions) self.worker.start(5) - + def _clean_sessions(self): config = component.get("DelugeWeb").config session_ids = config["sessions"].keys() - + now = time.gmtime() for session_id in session_ids: session = config["sessions"][session_id] - + if "expires" not in session: del config["sessions"][session_id] continue - + if time.gmtime(session["expires"]) < now: del config["sessions"][session_id] continue - + def _create_session(self, request, login='admin'): """ Creates a new session. - + :keyword login: the username of the user logging in, currently \ only for future use currently. :type login: string @@ -131,13 +131,13 @@ class Auth(JSONComponent): session_id = m.hexdigest() config = component.get("DelugeWeb").config - + expires, expires_str = make_expires(config["session_timeout"]) checksum = str(make_checksum(session_id)) - + request.addCookie('_session_id', session_id + checksum, - path="/json", expires=expires_str) - + path=request.base+"json", expires=expires_str) + log.debug("Creating session for %s", login) config = component.get("DelugeWeb").config @@ -150,7 +150,7 @@ class Auth(JSONComponent): "expires": expires } return True - + def check_password(self, password): config = component.get("DelugeWeb").config if "pwd_md5" in config.config: @@ -164,14 +164,14 @@ class Auth(JSONComponent): # the old passwords from the config file. self._change_password(password) del config.config["pwd_md5"] - + # Remove the older password if there is now. if "old_pwd_md5" in config.config: del config.config["old_pwd_salt"] del config.config["old_pwd_md5"] - + return True - + elif "old_pwd_md5" in config.config: # We are using the 1.1 webui auth method log.debug("Received a password via the 1.1 auth method") @@ -180,13 +180,13 @@ class Auth(JSONComponent): m.update(decodestring(config["old_pwd_salt"])) m.update(password) if m.digest() == decodestring(config["old_pwd_md5"]): - + # We want to move the password over to sha1 and remove # the old passwords from the config file. self._change_password(password) del config.config["old_pwd_salt"] del config.config["old_pwd_md5"] - + return True elif "pwd_sha1" in config.config: @@ -203,25 +203,25 @@ class Auth(JSONComponent): # access. log.debug("Failed to detect the login method") return False - + def check_request(self, request, method=None, level=None): """ Check to ensure that a request is authorised to call the specified method of authentication level. - + :param request: The HTTP request in question :type request: twisted.web.http.Request :keyword method: Check the specified method :type method: function :keyword level: Check the specified auth level :type level: integer - + :raises: Exception """ config = component.get("DelugeWeb").config session_id = get_session_id(request.getCookie("_session_id")) - + if session_id not in config["sessions"]: auth_level = AUTH_LEVEL_NONE session_id = None @@ -233,32 +233,32 @@ class Auth(JSONComponent): _session_id = request.getCookie("_session_id") request.addCookie('_session_id', _session_id, - path="/json", expires=expires_str) - + path=request.base+"json", expires=expires_str) + if method: if not hasattr(method, "_json_export"): raise Exception("Not an exported method") - + method_level = getattr(method, "_json_auth_level") if method_level is None: raise Exception("Method has no auth level") level = method_level - + if level is None: raise Exception("No level specified to check against") - + request.auth_level = auth_level request.session_id = session_id - + if auth_level < level: raise AuthError("Not authenticated") - + def _change_password(self, new_password): """ Change the password. This is to allow the UI to change/reset a password. - + :param new_password: the password to change to :type new_password: string """ @@ -270,12 +270,12 @@ class Auth(JSONComponent): config["pwd_salt"] = salt config["pwd_sha1"] = s.hexdigest() return True - + @export def change_password(self, old_password, new_password): """ Change the password. - + :param old_password: the current password :type old_password: string :param new_password: the password to change to @@ -284,22 +284,22 @@ class Auth(JSONComponent): if not self.check_password(old_password): return False return self._change_password(new_password) - + @export(AUTH_LEVEL_NONE) def check_session(self, session_id=None): """ Check a session to see if it's still valid. - + :returns: True if the session is valid, False if not. :rtype: booleon """ return __request__.session_id is not None - + @export def delete_session(self): """ Removes a session. - + :param session_id: the id for the session to remove :type session_id: string """ @@ -307,18 +307,18 @@ class Auth(JSONComponent): config = component.get("DelugeWeb").config del config["sessions"][__request__.session_id] return True - + @export(AUTH_LEVEL_NONE) def login(self, password): """ Test a password to see if it's valid. - + :param password: the password to test :type password: string :returns: a session id or False :rtype: string or False """ - + if self.check_password(password): return self._create_session(__request__) else: diff --git a/deluge/ui/web/common.py b/deluge/ui/web/common.py index b21bfd8aa..e3795fa1b 100644 --- a/deluge/ui/web/common.py +++ b/deluge/ui/web/common.py @@ -35,7 +35,6 @@ import zlib import gettext -from mako.template import Template as MakoTemplate from deluge import common _ = lambda x: gettext.gettext(x).decode("utf-8") @@ -59,18 +58,31 @@ def compress(contents, request): contents += compress.flush() return contents -class Template(MakoTemplate): - """ - A template that adds some built-ins to the rendering - """ - - builtins = { - "_": _, - "escape": escape, - "version": common.get_version() - } - - def render(self, *args, **data): - data.update(self.builtins) - rendered = MakoTemplate.render_unicode(self, *args, **data) - return rendered.encode('utf-8', 'replace') +try: + # This is beeing done like this in order to allow tests to use the above + # `compress` without requiring Mako to be instaled + from mako.template import Template as MakoTemplate + class Template(MakoTemplate): + """ + A template that adds some built-ins to the rendering + """ + + builtins = { + "_": _, + "escape": escape, + "version": common.get_version() + } + + def render(self, *args, **data): + data.update(self.builtins) + rendered = MakoTemplate.render_unicode(self, *args, **data) + return rendered.encode('utf-8', 'replace') +except ImportError: + import warnings + warnings.warn("The Mako library is required to run deluge.ui.web", + RuntimeWarning) + class Template(object): + def __new__(cls, *args, **kwargs): + raise RuntimeError( + "The Mako library is required to run deluge.ui.web" + ) diff --git a/deluge/ui/web/js/deluge-all/AddConnectionWindow.js b/deluge/ui/web/js/deluge-all/AddConnectionWindow.js index 417363948..67e15b6d5 100644 --- a/deluge/ui/web/js/deluge-all/AddConnectionWindow.js +++ b/deluge/ui/web/js/deluge-all/AddConnectionWindow.js @@ -37,86 +37,86 @@ Ext.ns('Deluge'); */ Deluge.AddConnectionWindow = Ext.extend(Ext.Window, { - title: _('Add Connection'), - iconCls: 'x-deluge-add-window-icon', + title: _('Add Connection'), + iconCls: 'x-deluge-add-window-icon', - layout: 'fit', - width: 300, - height: 195, + layout: 'fit', + width: 300, + height: 195, - bodyStyle: 'padding: 10px 5px;', - closeAction: 'hide', + bodyStyle: 'padding: 10px 5px;', + closeAction: 'hide', - initComponent: function() { - Deluge.AddConnectionWindow.superclass.initComponent.call(this); + initComponent: function() { + Deluge.AddConnectionWindow.superclass.initComponent.call(this); - this.addEvents('hostadded'); - - this.addButton(_('Close'), this.hide, this); - this.addButton(_('Add'), this.onAddClick, this); - - this.on('hide', this.onHide, this); - - this.form = this.add({ - xtype: 'form', - defaultType: 'textfield', - baseCls: 'x-plain', - labelWidth: 60, - items: [{ - fieldLabel: _('Host'), - name: 'host', - anchor: '75%', - value: '' - }, { - xtype: 'spinnerfield', - fieldLabel: _('Port'), - name: 'port', - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 65535 - }, - value: '58846', - anchor: '40%' - }, { - fieldLabel: _('Username'), - name: 'username', - anchor: '75%', - value: '' - }, { - fieldLabel: _('Password'), - anchor: '75%', - name: 'password', - inputType: 'password', - value: '' - }] - }); - }, + this.addEvents('hostadded'); + + this.addButton(_('Close'), this.hide, this); + this.addButton(_('Add'), this.onAddClick, this); + + this.on('hide', this.onHide, this); + + this.form = this.add({ + xtype: 'form', + defaultType: 'textfield', + baseCls: 'x-plain', + labelWidth: 60, + items: [{ + fieldLabel: _('Host'), + name: 'host', + anchor: '75%', + value: '' + }, { + xtype: 'spinnerfield', + fieldLabel: _('Port'), + name: 'port', + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 65535 + }, + value: '58846', + anchor: '40%' + }, { + fieldLabel: _('Username'), + name: 'username', + anchor: '75%', + value: '' + }, { + fieldLabel: _('Password'), + anchor: '75%', + name: 'password', + inputType: 'password', + value: '' + }] + }); + }, - onAddClick: function() { - var values = this.form.getForm().getValues(); - deluge.client.web.add_host(values.host, values.port, values.username, values.password, { - success: function(result) { - if (!result[0]) { - Ext.MessageBox.show({ - title: _('Error'), - msg: "Unable to add host: " + result[1], - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.ERROR, - iconCls: 'x-deluge-icon-error' - }); - } else { - this.fireEvent('hostadded'); - } - this.hide(); - }, - scope: this - }); - }, + onAddClick: function() { + var values = this.form.getForm().getValues(); + deluge.client.web.add_host(values.host, values.port, values.username, values.password, { + success: function(result) { + if (!result[0]) { + Ext.MessageBox.show({ + title: _('Error'), + msg: "Unable to add host: " + result[1], + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + } else { + this.fireEvent('hostadded'); + } + this.hide(); + }, + scope: this + }); + }, - onHide: function() { - this.form.getForm().reset(); - } + onHide: function() { + this.form.getForm().reset(); + } }); diff --git a/deluge/ui/web/js/deluge-all/AddTrackerWindow.js b/deluge/ui/web/js/deluge-all/AddTrackerWindow.js index 4d3081d19..a921ec2cc 100644 --- a/deluge/ui/web/js/deluge-all/AddTrackerWindow.js +++ b/deluge/ui/web/js/deluge-all/AddTrackerWindow.js @@ -36,57 +36,57 @@ Ext.ns('Deluge'); * @extends Ext.Window */ Deluge.AddTrackerWindow = Ext.extend(Ext.Window, { - - title: _('Add Tracker'), - layout: 'fit', - width: 375, - height: 150, - plain: true, - closable: true, - resizable: false, + + title: _('Add Tracker'), + layout: 'fit', + width: 375, + height: 150, + plain: true, + closable: true, + resizable: false, - bodyStyle: 'padding: 5px', - buttonAlign: 'right', - closeAction: 'hide', - iconCls: 'x-deluge-edit-trackers', + bodyStyle: 'padding: 5px', + buttonAlign: 'right', + closeAction: 'hide', + iconCls: 'x-deluge-edit-trackers', - initComponent: function() { - Deluge.AddTrackerWindow.superclass.initComponent.call(this); - - this.addButton(_('Cancel'), this.onCancelClick, this); - this.addButton(_('Add'), this.onAddClick, this); - this.addEvents('add'); - - this.form = this.add({ - xtype: 'form', - defaultType: 'textarea', - baseCls: 'x-plain', - labelWidth: 55, - items: [{ - fieldLabel: _('Trackers'), - name: 'trackers', - anchor: '100%' - }] - }) - }, + initComponent: function() { + Deluge.AddTrackerWindow.superclass.initComponent.call(this); + + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Add'), this.onAddClick, this); + this.addEvents('add'); + + this.form = this.add({ + xtype: 'form', + defaultType: 'textarea', + baseCls: 'x-plain', + labelWidth: 55, + items: [{ + fieldLabel: _('Trackers'), + name: 'trackers', + anchor: '100%' + }] + }) + }, - onAddClick: function() { - var trackers = this.form.getForm().findField('trackers').getValue(); - trackers = trackers.split('\n'); - - var cleaned = []; - Ext.each(trackers, function(tracker) { - if (Ext.form.VTypes.url(tracker)) { - cleaned.push(tracker); - } - }, this); - this.fireEvent('add', cleaned); - this.hide(); - this.form.getForm().findField('trackers').setValue(''); - }, + onAddClick: function() { + var trackers = this.form.getForm().findField('trackers').getValue(); + trackers = trackers.split('\n'); + + var cleaned = []; + Ext.each(trackers, function(tracker) { + if (Ext.form.VTypes.url(tracker)) { + cleaned.push(tracker); + } + }, this); + this.fireEvent('add', cleaned); + this.hide(); + this.form.getForm().findField('trackers').setValue(''); + }, - onCancelClick: function() { - this.form.getForm().findField('trackers').setValue(''); - this.hide(); - } + onCancelClick: function() { + this.form.getForm().findField('trackers').setValue(''); + this.hide(); + } }); diff --git a/deluge/ui/web/js/deluge-all/Client.js b/deluge/ui/web/js/deluge-all/Client.js index 4483f34c8..658cf7127 100644 --- a/deluge/ui/web/js/deluge-all/Client.js +++ b/deluge/ui/web/js/deluge-all/Client.js @@ -39,157 +39,157 @@ Ext.namespace('Ext.ux.util'); */ Ext.ux.util.RpcClient = Ext.extend(Ext.util.Observable, { - _components: [], - - _methods: [], - - _requests: {}, - - _url: null, - - _optionKeys: ['scope', 'success', 'failure'], - - constructor: function(config) { - Ext.ux.util.RpcClient.superclass.constructor.call(this, config); - this._url = config.url || null; - this._id = 0; - - this.addEvents( - // raw events - /** - * @event connected - * Fires when the client has retrieved the list of methods from the server. - * @param {Ext.ux.util.RpcClient} this - */ - 'connected', - - 'error' - ); - this.reloadMethods(); - }, - - reloadMethods: function() { - Ext.each(this._components, function(component) { - delete this[component]; - }, this); - this._execute('system.listMethods', { - success: this._setMethods, - scope: this - }); - }, + _components: [], + + _methods: [], + + _requests: {}, + + _url: null, + + _optionKeys: ['scope', 'success', 'failure'], + + constructor: function(config) { + Ext.ux.util.RpcClient.superclass.constructor.call(this, config); + this._url = config.url || null; + this._id = 0; + + this.addEvents( + // raw events + /** + * @event connected + * Fires when the client has retrieved the list of methods from the server. + * @param {Ext.ux.util.RpcClient} this + */ + 'connected', + + 'error' + ); + this.reloadMethods(); + }, + + reloadMethods: function() { + Ext.each(this._components, function(component) { + delete this[component]; + }, this); + this._execute('system.listMethods', { + success: this._setMethods, + scope: this + }); + }, - _execute: function(method, options) { - options = options || {}; - options.params = options.params || []; - options.id = this._id; - - var request = Ext.encode({ - method: method, - params: options.params, - id: options.id - }); - this._id++; - - return Ext.Ajax.request({ - url: this._url, - method: 'POST', - success: this._onSuccess, - failure: this._onFailure, - scope: this, - jsonData: request, - options: options - }); - }, - - _onFailure: function(response, requestOptions) { - var options = requestOptions.options; - errorObj = { - id: options.id, - result: null, - error: { - msg: 'HTTP: ' + response.status + ' ' + response.statusText, - code: 255 - } - } - - this.fireEvent('error', errorObj, response, requestOptions) - - if (Ext.type(options.failure) != 'function') return; - if (options.scope) { - options.failure.call(options.scope, errorObj, response, requestOptions); - } else { - options.failure(errorObj, response, requestOptions); - } - }, - - _onSuccess: function(response, requestOptions) { - var responseObj = Ext.decode(response.responseText); - var options = requestOptions.options; - if (responseObj.error) { - this.fireEvent('error', responseObj, response, requestOptions); - - if (Ext.type(options.failure) != 'function') return; - if (options.scope) { - options.failure.call(options.scope, responseObj, response, requestOptions); - } else { - options.failure(responseObj, response, requestOptions); - } - } else { - if (Ext.type(options.success) != 'function') return; - if (options.scope) { - options.success.call(options.scope, responseObj.result, responseObj, response, requestOptions); - } else { - options.success(responseObj.result, responseObj, response, requestOptions); - } - } - }, - - _parseArgs: function(args) { - var params = []; - Ext.each(args, function(arg) { - params.push(arg); - }); - - var options = params[params.length - 1]; - if (Ext.type(options) == 'object') { - var keys = Ext.keys(options), isOption = false; - - Ext.each(this._optionKeys, function(key) { - if (keys.indexOf(key) > -1) isOption = true; - }); - - if (isOption) { - params.remove(options) - } else { - options = {} - } - } else { - options = {} - } - options.params = params; - return options; - }, + _execute: function(method, options) { + options = options || {}; + options.params = options.params || []; + options.id = this._id; + + var request = Ext.encode({ + method: method, + params: options.params, + id: options.id + }); + this._id++; + + return Ext.Ajax.request({ + url: this._url, + method: 'POST', + success: this._onSuccess, + failure: this._onFailure, + scope: this, + jsonData: request, + options: options + }); + }, + + _onFailure: function(response, requestOptions) { + var options = requestOptions.options; + errorObj = { + id: options.id, + result: null, + error: { + msg: 'HTTP: ' + response.status + ' ' + response.statusText, + code: 255 + } + } + + this.fireEvent('error', errorObj, response, requestOptions) + + if (Ext.type(options.failure) != 'function') return; + if (options.scope) { + options.failure.call(options.scope, errorObj, response, requestOptions); + } else { + options.failure(errorObj, response, requestOptions); + } + }, + + _onSuccess: function(response, requestOptions) { + var responseObj = Ext.decode(response.responseText); + var options = requestOptions.options; + if (responseObj.error) { + this.fireEvent('error', responseObj, response, requestOptions); + + if (Ext.type(options.failure) != 'function') return; + if (options.scope) { + options.failure.call(options.scope, responseObj, response, requestOptions); + } else { + options.failure(responseObj, response, requestOptions); + } + } else { + if (Ext.type(options.success) != 'function') return; + if (options.scope) { + options.success.call(options.scope, responseObj.result, responseObj, response, requestOptions); + } else { + options.success(responseObj.result, responseObj, response, requestOptions); + } + } + }, + + _parseArgs: function(args) { + var params = []; + Ext.each(args, function(arg) { + params.push(arg); + }); + + var options = params[params.length - 1]; + if (Ext.type(options) == 'object') { + var keys = Ext.keys(options), isOption = false; + + Ext.each(this._optionKeys, function(key) { + if (keys.indexOf(key) > -1) isOption = true; + }); + + if (isOption) { + params.remove(options) + } else { + options = {} + } + } else { + options = {} + } + options.params = params; + return options; + }, - _setMethods: function(methods) { - var components = {}, self = this; - - Ext.each(methods, function(method) { - var parts = method.split('.'); - var component = components[parts[0]] || {}; - - var fn = function() { - var options = self._parseArgs(arguments); - return self._execute(method, options); - } - component[parts[1]] = fn; - components[parts[0]] = component; - }); - - for (var name in components) { - self[name] = components[name]; - } - - this._components = Ext.keys(components); - this.fireEvent('connected', this); - } + _setMethods: function(methods) { + var components = {}, self = this; + + Ext.each(methods, function(method) { + var parts = method.split('.'); + var component = components[parts[0]] || {}; + + var fn = function() { + var options = self._parseArgs(arguments); + return self._execute(method, options); + } + component[parts[1]] = fn; + components[parts[0]] = component; + }); + + for (var name in components) { + self[name] = components[name]; + } + + this._components = Ext.keys(components); + this.fireEvent('connected', this); + } }); diff --git a/deluge/ui/web/js/deluge-all/ConnectionManager.js b/deluge/ui/web/js/deluge-all/ConnectionManager.js index 54b3b25ed..f3d44e70d 100644 --- a/deluge/ui/web/js/deluge-all/ConnectionManager.js +++ b/deluge/ui/web/js/deluge-all/ConnectionManager.js @@ -1,6 +1,6 @@ /*! * Deluge.ConnectionManager.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -32,343 +32,345 @@ Deluge.ConnectionManager = Ext.extend(Ext.Window, { - layout: 'fit', - width: 300, - height: 220, - bodyStyle: 'padding: 10px 5px;', - buttonAlign: 'right', - closeAction: 'hide', - closable: true, - plain: true, - title: _('Connection Manager'), - iconCls: 'x-deluge-connect-window-icon', + layout: 'fit', + width: 300, + height: 220, + bodyStyle: 'padding: 10px 5px;', + buttonAlign: 'right', + closeAction: 'hide', + closable: true, + plain: true, + title: _('Connection Manager'), + iconCls: 'x-deluge-connect-window-icon', - initComponent: function() { - Deluge.ConnectionManager.superclass.initComponent.call(this); - this.on('hide', this.onHide, this); - this.on('show', this.onShow, this); + initComponent: function() { + Deluge.ConnectionManager.superclass.initComponent.call(this); + this.on('hide', this.onHide, this); + this.on('show', this.onShow, this); - deluge.events.on('login', this.onLogin, this); - deluge.events.on('logout', this.onLogout, this); + deluge.events.on('login', this.onLogin, this); + deluge.events.on('logout', this.onLogout, this); - this.addButton(_('Close'), this.onClose, this); - this.addButton(_('Connect'), this.onConnect, this); + this.addButton(_('Close'), this.onClose, this); + this.addButton(_('Connect'), this.onConnect, this); - this.list = new Ext.list.ListView({ - store: new Ext.data.ArrayStore({ - fields: [ - {name: 'status', mapping: 3}, - {name: 'host', mapping: 1}, - {name: 'port', mapping: 2}, - {name: 'version', mapping: 4} - ], - id: 0 - }), - columns: [{ - header: _('Status'), - width: .24, - sortable: true, - dataIndex: 'status' - }, { - id:'host', - header: _('Host'), - width: .51, - sortable: true, - tpl: '{host}:{port}', - dataIndex: 'host' - }, { - header: _('Version'), - width: .25, - sortable: true, - tpl: '{version}', - dataIndex: 'version' - }], - singleSelect: true, - listeners: { - 'selectionchange': {fn: this.onSelectionChanged, scope: this} - } - }); - - this.panel = this.add({ - autoScroll: true, - items: [this.list], - bbar: new Ext.Toolbar({ - buttons: [ - { - id: 'cm-add', - cls: 'x-btn-text-icon', - text: _('Add'), - iconCls: 'icon-add', - handler: this.onAddClick, - scope: this - }, { - id: 'cm-remove', - cls: 'x-btn-text-icon', - text: _('Remove'), - iconCls: 'icon-remove', - handler: this.onRemoveClick, - disabled: true, - scope: this - }, '->', { - id: 'cm-stop', - cls: 'x-btn-text-icon', - text: _('Stop Daemon'), - iconCls: 'icon-error', - handler: this.onStopClick, - disabled: true, - scope: this - } - ] - }) - }); - this.update = this.update.createDelegate(this); - }, + this.list = new Ext.list.ListView({ + store: new Ext.data.ArrayStore({ + fields: [ + {name: 'status', mapping: 3}, + {name: 'host', mapping: 1}, + {name: 'port', mapping: 2}, + {name: 'version', mapping: 4} + ], + id: 0 + }), + columns: [{ + header: _('Status'), + width: .24, + sortable: true, + dataIndex: 'status' + }, { + id:'host', + header: _('Host'), + width: .51, + sortable: true, + tpl: '{host}:{port}', + dataIndex: 'host' + }, { + header: _('Version'), + width: .25, + sortable: true, + tpl: '{version}', + dataIndex: 'version' + }], + singleSelect: true, + listeners: { + 'selectionchange': {fn: this.onSelectionChanged, scope: this} + } + }); - /** - * Check to see if the the web interface is currently connected - * to a Deluge Daemon and show the Connection Manager if not. - */ - checkConnected: function() { - deluge.client.web.connected({ - success: function(connected) { - if (connected) { - deluge.events.fire('connect'); - } else { - this.show(); - } - }, - scope: this - }); - }, + this.panel = this.add({ + autoScroll: true, + items: [this.list], + bbar: new Ext.Toolbar({ + buttons: [ + { + id: 'cm-add', + cls: 'x-btn-text-icon', + text: _('Add'), + iconCls: 'icon-add', + handler: this.onAddClick, + scope: this + }, { + id: 'cm-remove', + cls: 'x-btn-text-icon', + text: _('Remove'), + iconCls: 'icon-remove', + handler: this.onRemoveClick, + disabled: true, + scope: this + }, '->', { + id: 'cm-stop', + cls: 'x-btn-text-icon', + text: _('Stop Daemon'), + iconCls: 'icon-error', + handler: this.onStopClick, + disabled: true, + scope: this + } + ] + }) + }); + this.update = this.update.createDelegate(this); + }, - disconnect: function(show) { - deluge.events.fire('disconnect'); - if (show) { - if (this.isVisible()) return; - this.show(); - } - }, + /** + * Check to see if the the web interface is currently connected + * to a Deluge Daemon and show the Connection Manager if not. + */ + checkConnected: function() { + deluge.client.web.connected({ + success: function(connected) { + if (connected) { + deluge.events.fire('connect'); + } else { + this.show(); + } + }, + scope: this + }); + }, - loadHosts: function() { - deluge.client.web.get_hosts({ - success: this.onGetHosts, - scope: this - }); - }, + disconnect: function(show) { + deluge.events.fire('disconnect'); + if (show) { + if (this.isVisible()) return; + this.show(); + } + }, - update: function() { - this.list.getStore().each(function(r) { - deluge.client.web.get_host_status(r.id, { - success: this.onGetHostStatus, - scope: this - }); - }, this); - }, + loadHosts: function() { + deluge.client.web.get_hosts({ + success: this.onGetHosts, + scope: this + }); + }, - /** - * Updates the buttons in the Connection Manager UI according to the - * passed in records host state. - * @param {Ext.data.Record} record The hosts record to update the UI for - */ - updateButtons: function(record) { - var button = this.buttons[1], status = record.get('status'); - - // Update the Connect/Disconnect button - if (status == _('Connected')) { - button.enable(); - button.setText(_('Disconnect')); - } else if (status == _('Offline')) { - button.disable(); - } else { - button.enable(); - button.setText(_('Connect')); - } - - // Update the Stop/Start Daemon button - if (status == _('Offline')) { - if (record.get('host') == '127.0.0.1' || record.get('host') == 'localhost') { - this.stopHostButton.enable(); - this.stopHostButton.setText(_('Start Daemon')); - } else { - this.stopHostButton.disable(); - } - } else { - this.stopHostButton.enable(); - this.stopHostButton.setText(_('Stop Daemon')); - } - }, + update: function() { + this.list.getStore().each(function(r) { + deluge.client.web.get_host_status(r.id, { + success: this.onGetHostStatus, + scope: this + }); + }, this); + }, - // private - onAddClick: function(button, e) { - if (!this.addWindow) { - this.addWindow = new Deluge.AddConnectionWindow(); - this.addWindow.on('hostadded', this.onHostAdded, this); - } - this.addWindow.show(); - }, + /** + * Updates the buttons in the Connection Manager UI according to the + * passed in records host state. + * @param {Ext.data.Record} record The hosts record to update the UI for + */ + updateButtons: function(record) { + var button = this.buttons[1], status = record.get('status'); - // private - onHostAdded: function() { - this.loadHosts(); - }, + // Update the Connect/Disconnect button + if (status == _('Connected')) { + button.enable(); + button.setText(_('Disconnect')); + } else if (status == _('Offline')) { + button.disable(); + } else { + button.enable(); + button.setText(_('Connect')); + } - // private - onClose: function(e) { - this.hide(); - }, + // Update the Stop/Start Daemon button + if (status == _('Offline')) { + if (record.get('host') == '127.0.0.1' || record.get('host') == 'localhost') { + this.stopHostButton.enable(); + this.stopHostButton.setText(_('Start Daemon')); + } else { + this.stopHostButton.disable(); + } + } else { + this.stopHostButton.enable(); + this.stopHostButton.setText(_('Stop Daemon')); + } + }, - // private - onConnect: function(e) { - var selected = this.list.getSelectedRecords()[0]; - if (!selected) return; + // private + onAddClick: function(button, e) { + if (!this.addWindow) { + this.addWindow = new Deluge.AddConnectionWindow(); + this.addWindow.on('hostadded', this.onHostAdded, this); + } + this.addWindow.show(); + }, - if (selected.get('status') == _('Connected')) { - deluge.client.web.disconnect({ - success: function(result) { - this.update(this); - deluge.events.fire('disconnect'); - }, - scope: this - }); - } else { - var id = selected.id; - deluge.client.web.connect(id, { - success: function(methods) { - deluge.client.reloadMethods(); - deluge.client.on('connected', function(e) { - deluge.events.fire('connect'); - }, this, {single: true}); - } - }); - this.hide(); - } - }, + // private + onHostAdded: function() { + this.loadHosts(); + }, - // private - onGetHosts: function(hosts) { - this.list.getStore().loadData(hosts); - Ext.each(hosts, function(host) { - deluge.client.web.get_host_status(host[0], { - success: this.onGetHostStatus, - scope: this - }); - }, this); - }, + // private + onClose: function(e) { + this.hide(); + }, - // private - onGetHostStatus: function(host) { - var record = this.list.getStore().getById(host[0]); - record.set('status', host[3]) - record.set('version', host[4]) - record.commit(); - if (this.list.getSelectedRecords()[0] == record) this.updateButtons(record); - }, + // private + onConnect: function(e) { + var selected = this.list.getSelectedRecords()[0]; + if (!selected) return; - // private - onHide: function() { - if (this.running) window.clearInterval(this.running); - }, + if (selected.get('status') == _('Connected')) { + deluge.client.web.disconnect({ + success: function(result) { + this.update(this); + deluge.events.fire('disconnect'); + }, + scope: this + }); + } else { + var id = selected.id; + deluge.client.web.connect(id, { + success: function(methods) { + deluge.client.reloadMethods(); + deluge.client.on('connected', function(e) { + deluge.events.fire('connect'); + }, this, {single: true}); + } + }); + this.hide(); + } + }, - // private - onLogin: function() { - if (deluge.config.first_login) { - Ext.MessageBox.confirm('Change password', - 'As this is your first login, we recommend that you ' + - 'change your password. Would you like to ' + - 'do this now?', function(res) { - this.checkConnected(); - if (res == 'yes') { - deluge.preferences.show(); - deluge.preferences.selectPage('Interface'); - } - deluge.client.web.set_config({first_login: false}); - }, this); - } else { - this.checkConnected(); - } - }, + // private + onGetHosts: function(hosts) { + this.list.getStore().loadData(hosts); + Ext.each(hosts, function(host) { + deluge.client.web.get_host_status(host[0], { + success: this.onGetHostStatus, + scope: this + }); + }, this); + }, - // private - onLogout: function() { - this.disconnect(); - if (!this.hidden && this.rendered) { - this.hide(); - } - }, + // private + onGetHostStatus: function(host) { + var record = this.list.getStore().getById(host[0]); + record.set('status', host[3]) + record.set('version', host[4]) + record.commit(); + if (this.list.getSelectedRecords()[0] == record) this.updateButtons(record); + }, - // private - onRemoveClick: function(button) { - var connection = this.list.getSelectedRecords()[0]; - if (!connection) return; + // private + onHide: function() { + if (this.running) window.clearInterval(this.running); + }, - deluge.client.web.remove_host(connection.id, { - success: function(result) { - if (!result) { - Ext.MessageBox.show({ - title: _('Error'), - msg: result[1], - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.ERROR, - iconCls: 'x-deluge-icon-error' - }); - } else { - this.list.getStore().remove(connection); - } - }, - scope: this - }); - }, + // private + onLogin: function() { + if (deluge.config.first_login) { + Ext.MessageBox.confirm('Change password', + 'As this is your first login, we recommend that you ' + + 'change your password. Would you like to ' + + 'do this now?', function(res) { + this.checkConnected(); + if (res == 'yes') { + deluge.preferences.show(); + deluge.preferences.selectPage('Interface'); + } + deluge.client.web.set_config({first_login: false}); + }, this); + } else { + this.checkConnected(); + } + }, - // private - onSelectionChanged: function(list, selections) { - if (selections[0]) { - this.removeHostButton.enable(); - this.stopHostButton.enable(); - this.stopHostButton.setText(_('Stop Daemon')); - this.updateButtons(this.list.getRecord(selections[0])); - } else { - this.removeHostButton.disable(); - this.stopHostButton.disable(); - } - }, + // private + onLogout: function() { + this.disconnect(); + if (!this.hidden && this.rendered) { + this.hide(); + } + }, - // private - onShow: function() { - if (!this.addHostButton) { - var bbar = this.panel.getBottomToolbar(); - this.addHostButton = bbar.items.get('cm-add'); - this.removeHostButton = bbar.items.get('cm-remove'); - this.stopHostButton = bbar.items.get('cm-stop'); - } - this.loadHosts(); - this.running = window.setInterval(this.update, 2000, this); - }, - - // private - onStopClick: function(button, e) { - var connection = this.list.getSelectedRecords()[0]; - if (!connection) return; + // private + onRemoveClick: function(button) { + var connection = this.list.getSelectedRecords()[0]; + if (!connection) return; - if (connection.get('status') == 'Offline') { - // This means we need to start the daemon - deluge.client.web.start_daemon(connection.get('port')); - } else { - // This means we need to stop the daemon - deluge.client.web.stop_daemon(connection.id, { - success: function(result) { - if (!result[0]) { - Ext.MessageBox.show({ - title: _('Error'), - msg: result[1], - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.ERROR, - iconCls: 'x-deluge-icon-error' - }); - } - } - }); - } - } + deluge.client.web.remove_host(connection.id, { + success: function(result) { + if (!result) { + Ext.MessageBox.show({ + title: _('Error'), + msg: result[1], + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + } else { + this.list.getStore().remove(connection); + } + }, + scope: this + }); + }, + + // private + onSelectionChanged: function(list, selections) { + if (selections[0]) { + this.removeHostButton.enable(); + this.stopHostButton.enable(); + this.stopHostButton.setText(_('Stop Daemon')); + this.updateButtons(this.list.getRecord(selections[0])); + } else { + this.removeHostButton.disable(); + this.stopHostButton.disable(); + } + }, + + // FIXME: Find out why this is being fired twice + // private + onShow: function() { + if (!this.addHostButton) { + var bbar = this.panel.getBottomToolbar(); + this.addHostButton = bbar.items.get('cm-add'); + this.removeHostButton = bbar.items.get('cm-remove'); + this.stopHostButton = bbar.items.get('cm-stop'); + } + this.loadHosts(); + if (this.running) return; + this.running = window.setInterval(this.update, 2000, this); + }, + + // private + onStopClick: function(button, e) { + var connection = this.list.getSelectedRecords()[0]; + if (!connection) return; + + if (connection.get('status') == 'Offline') { + // This means we need to start the daemon + deluge.client.web.start_daemon(connection.get('port')); + } else { + // This means we need to stop the daemon + deluge.client.web.stop_daemon(connection.id, { + success: function(result) { + if (!result[0]) { + Ext.MessageBox.show({ + title: _('Error'), + msg: result[1], + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + } + } + }); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/Deluge.js b/deluge/ui/web/js/deluge-all/Deluge.js index 382b611dd..2e9c0fded 100644 --- a/deluge/ui/web/js/deluge-all/Deluge.js +++ b/deluge/ui/web/js/deluge-all/Deluge.js @@ -36,50 +36,50 @@ Ext.state.Manager.setProvider(new Ext.state.CookieProvider()); // Add some additional functions to ext and setup some of the // configurable parameters Ext.apply(Ext, { - escapeHTML: function(text) { - text = String(text).replace('<', '<').replace('>', '>'); - return text.replace('&', '&'); - }, + escapeHTML: function(text) { + text = String(text).replace('<', '<').replace('>', '>'); + return text.replace('&', '&'); + }, - isObjectEmpty: function(obj) { - for(var i in obj) { return false; } - return true; - }, + isObjectEmpty: function(obj) { + for(var i in obj) { return false; } + return true; + }, - areObjectsEqual: function(obj1, obj2) { - var equal = true; - if (!obj1 || !obj2) return false; - for (var i in obj1) { - if (obj1[i] != obj2[i]) { - equal = false; - } - } - return equal; - }, - - keys: function(obj) { - var keys = []; - for (var i in obj) if (obj.hasOwnProperty(i)) - { - keys.push(i); - } - return keys; - }, + areObjectsEqual: function(obj1, obj2) { + var equal = true; + if (!obj1 || !obj2) return false; + for (var i in obj1) { + if (obj1[i] != obj2[i]) { + equal = false; + } + } + return equal; + }, + + keys: function(obj) { + var keys = []; + for (var i in obj) if (obj.hasOwnProperty(i)) + { + keys.push(i); + } + return keys; + }, - values: function(obj) { - var values = []; - for (var i in obj) { - if (obj.hasOwnProperty(i)) { - values.push(obj[i]); - } - } - return values; - }, - - splat: function(obj) { - var type = Ext.type(obj); - return (type) ? ((type != 'array') ? [obj] : obj) : []; - } + values: function(obj) { + var values = []; + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + values.push(obj[i]); + } + } + return values; + }, + + splat: function(obj) { + var type = Ext.type(obj); + return (type) ? ((type != 'array') ? [obj] : obj) : []; + } }); Ext.getKeys = Ext.keys; Ext.BLANK_IMAGE_URL = deluge.config.base + 'images/s.gif'; @@ -88,65 +88,65 @@ Ext.USE_NATIVE_JSON = true; // Create the Deluge namespace Ext.apply(Deluge, { - // private - pluginStore: {}, - - // private - progressTpl: '
' + - '
' + - '
' + - '
' + - '
{0}
' + - '
' + - '
' + - '
' + - '
{0}
' + - '
' + - '
' + - '
', + // private + pluginStore: {}, + + // private + progressTpl: '
' + + '
' + + '
' + + '
' + + '
{0}
' + + '
' + + '
' + + '
' + + '
{0}
' + + '
' + + '
' + + '
', - - /** - * A method to create a progress bar that can be used by renderers - * to display a bar within a grid or tree. - * @param {Number} progress The bars progress - * @param {Number} width The width of the bar - * @param {String} text The text to display on the bar - * @param {Number} modified Amount to subtract from the width allowing for fixes - */ - progressBar: function(progress, width, text, modifier) { - modifier = Ext.value(modifier, 10); - var progressWidth = ((width / 100.0) * progress).toFixed(0); - var barWidth = progressWidth - 1; - var textWidth = ((progressWidth - modifier) > 0 ? progressWidth - modifier : 0); - return String.format(Deluge.progressTpl, text, width, barWidth, textWidth); - }, + + /** + * A method to create a progress bar that can be used by renderers + * to display a bar within a grid or tree. + * @param {Number} progress The bars progress + * @param {Number} width The width of the bar + * @param {String} text The text to display on the bar + * @param {Number} modified Amount to subtract from the width allowing for fixes + */ + progressBar: function(progress, width, text, modifier) { + modifier = Ext.value(modifier, 10); + var progressWidth = ((width / 100.0) * progress).toFixed(0); + var barWidth = progressWidth - 1; + var textWidth = ((progressWidth - modifier) > 0 ? progressWidth - modifier : 0); + return String.format(Deluge.progressTpl, text, width, barWidth, textWidth); + }, - /** - * Constructs a new instance of the specified plugin. - * @param {String} name The plugin name to create - */ - createPlugin: function(name) { - return new Deluge.pluginStore[name](); - }, + /** + * Constructs a new instance of the specified plugin. + * @param {String} name The plugin name to create + */ + createPlugin: function(name) { + return new Deluge.pluginStore[name](); + }, - /** - * Check to see if a plugin has been registered. - * @param {String} name The plugin name to check - */ - hasPlugin: function(name) { - return (Deluge.pluginStore[name]) ? true : false; - }, + /** + * Check to see if a plugin has been registered. + * @param {String} name The plugin name to check + */ + hasPlugin: function(name) { + return (Deluge.pluginStore[name]) ? true : false; + }, - /** - * Register a plugin with the Deluge interface. - * @param {String} name The plugin name to register - * @param {Plugin} plugin The plugin to register - */ - registerPlugin: function(name, plugin) { - Deluge.pluginStore[name] = plugin; - } - + /** + * Register a plugin with the Deluge interface. + * @param {String} name The plugin name to register + * @param {Plugin} plugin The plugin to register + */ + registerPlugin: function(name, plugin) { + Deluge.pluginStore[name] = plugin; + } + }); // Setup a space for plugins to insert themselves @@ -158,12 +158,12 @@ deluge.plugins = {}; // _('High Priority') // _('Highest Priority') FILE_PRIORITY = { - 9: 'Mixed', + 9: 'Mixed', 0: 'Do Not Download', 1: 'Normal Priority', 2: 'High Priority', 5: 'Highest Priority', - 'Mixed': 9, + 'Mixed': 9, 'Do Not Download': 0, 'Normal Priority': 1, 'High Priority': 2, @@ -171,9 +171,9 @@ FILE_PRIORITY = { } FILE_PRIORITY_CSS = { - 9: 'x-mixed-download', - 0: 'x-no-download', - 1: 'x-normal-download', - 2: 'x-high-download', - 5: 'x-highest-download' + 9: 'x-mixed-download', + 0: 'x-no-download', + 1: 'x-normal-download', + 2: 'x-high-download', + 5: 'x-highest-download' } diff --git a/deluge/ui/web/js/deluge-all/EditTrackerWindow.js b/deluge/ui/web/js/deluge-all/EditTrackerWindow.js index 59a0a0fc1..32bcef175 100644 --- a/deluge/ui/web/js/deluge-all/EditTrackerWindow.js +++ b/deluge/ui/web/js/deluge-all/EditTrackerWindow.js @@ -37,58 +37,58 @@ Ext.ns('Deluge'); */ Deluge.EditTrackerWindow = Ext.extend(Ext.Window, { - title: _('Edit Tracker'), - layout: 'fit', - width: 375, - height: 110, - plain: true, - closable: true, - resizable: false, + title: _('Edit Tracker'), + layout: 'fit', + width: 375, + height: 110, + plain: true, + closable: true, + resizable: false, - bodyStyle: 'padding: 5px', - buttonAlign: 'right', - closeAction: 'hide', - iconCls: 'x-deluge-edit-trackers', - - initComponent: function() { - Deluge.EditTrackerWindow.superclass.initComponent.call(this); - - this.addButton(_('Cancel'), this.onCancelClick, this); - this.addButton(_('Save'), this.onSaveClick, this); - this.on('hide', this.onHide, this); - - this.form = this.add({ - xtype: 'form', - defaultType: 'textfield', - baseCls: 'x-plain', - labelWidth: 55, - items: [{ - fieldLabel: _('Tracker'), - name: 'tracker', - anchor: '100%' - }] - }); - }, - - show: function(record) { - Deluge.EditTrackerWindow.superclass.show.call(this); - - this.record = record; - this.form.getForm().findField('tracker').setValue(record.data['url']); - }, - - onCancelClick: function() { - this.hide(); - }, - - onHide: function() { - this.form.getForm().findField('tracker').setValue(''); - }, - - onSaveClick: function() { - var url = this.form.getForm().findField('tracker').getValue(); - this.record.set('url', url); - this.record.commit(); - this.hide(); - } + bodyStyle: 'padding: 5px', + buttonAlign: 'right', + closeAction: 'hide', + iconCls: 'x-deluge-edit-trackers', + + initComponent: function() { + Deluge.EditTrackerWindow.superclass.initComponent.call(this); + + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Save'), this.onSaveClick, this); + this.on('hide', this.onHide, this); + + this.form = this.add({ + xtype: 'form', + defaultType: 'textfield', + baseCls: 'x-plain', + labelWidth: 55, + items: [{ + fieldLabel: _('Tracker'), + name: 'tracker', + anchor: '100%' + }] + }); + }, + + show: function(record) { + Deluge.EditTrackerWindow.superclass.show.call(this); + + this.record = record; + this.form.getForm().findField('tracker').setValue(record.data['url']); + }, + + onCancelClick: function() { + this.hide(); + }, + + onHide: function() { + this.form.getForm().findField('tracker').setValue(''); + }, + + onSaveClick: function() { + var url = this.form.getForm().findField('tracker').getValue(); + this.record.set('url', url); + this.record.commit(); + this.hide(); + } }); diff --git a/deluge/ui/web/js/deluge-all/EditTrackersWindow.js b/deluge/ui/web/js/deluge-all/EditTrackersWindow.js index 589ab1f5d..ed3672079 100644 --- a/deluge/ui/web/js/deluge-all/EditTrackersWindow.js +++ b/deluge/ui/web/js/deluge-all/EditTrackersWindow.js @@ -1,6 +1,6 @@ /*! * Deluge.EditTrackers.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -37,195 +37,203 @@ Ext.ns('Deluge'); */ Deluge.EditTrackersWindow = Ext.extend(Ext.Window, { - title: _('Edit Trackers'), - layout: 'fit', - width: 350, - height: 220, - plain: true, - closable: true, - resizable: false, + title: _('Edit Trackers'), + layout: 'fit', + width: 350, + height: 220, + plain: true, + closable: true, + resizable: true, - bodyStyle: 'padding: 5px', - buttonAlign: 'right', - closeAction: 'hide', - iconCls: 'x-deluge-edit-trackers', - - initComponent: function() { - Deluge.EditTrackersWindow.superclass.initComponent.call(this); - - this.addButton(_('Cancel'), this.onCancelClick, this); - this.addButton(_('Ok'), this.onOkClick, this); - this.addEvents('save'); - - this.on('show', this.onShow, this); - this.on('save', this.onSave, this); - - this.addWindow = new Deluge.AddTrackerWindow(); - this.addWindow.on('add', this.onAddTrackers, this); - this.editWindow = new Deluge.EditTrackerWindow(); + bodyStyle: 'padding: 5px', + buttonAlign: 'right', + closeAction: 'hide', + iconCls: 'x-deluge-edit-trackers', - this.list = new Ext.list.ListView({ - store: new Ext.data.JsonStore({ - root: 'trackers', - fields: [ - 'tier', - 'url' - ] - }), - columns: [{ - header: _('Tier'), - width: .1, - dataIndex: 'tier' - }, { - header: _('Tracker'), - width: .9, - dataIndex: 'url' - }], - columnSort: { - sortClasses: ['', ''] - }, - stripeRows: true, - singleSelect: true, - autoScroll: true, - listeners: { - 'dblclick': {fn: this.onListNodeDblClicked, scope: this}, - 'selectionchange': {fn: this.onSelect, scope: this} - } - }); - - this.panel = this.add({ - margins: '0 0 0 0', - items: [this.list], - bbar: new Ext.Toolbar({ - items: [ - { - text: _('Up'), - iconCls: 'icon-up', - handler: this.onUpClick, - scope: this - }, { - text: _('Down'), - iconCls: 'icon-down', - handler: this.onDownClick, - scope: this - }, '->', { - text: _('Add'), - iconCls: 'icon-add', - handler: this.onAddClick, - scope: this - }, { - text: _('Edit'), - iconCls: 'icon-edit-trackers', - handler: this.onEditClick, - scope: this - }, { - text: _('Remove'), - iconCls: 'icon-remove', - handler: this.onRemoveClick, - scope: this - } - ] - }) - }); - }, - - onAddClick: function() { - this.addWindow.show(); - }, - - onAddTrackers: function(trackers) { - var store = this.list.getStore(); - Ext.each(trackers, function(tracker) { - var duplicate = false, heightestTier = -1; - store.each(function(record) { - if (record.get('tier') > heightestTier) { - heightestTier = record.get('tier'); - } - if (tracker == record.get('tracker')) { - duplicate = true; - return false; - } - }, this); - if (duplicate) return; - store.add(new store.recordType({'tier': heightestTier + 1, 'url': tracker})); - }, this); - }, - - onCancelClick: function() { - this.hide(); - }, - - onEditClick: function() { - this.editWindow.show(this.list.getSelectedRecords()[0]); - }, - - onHide: function() { - this.list.getStore().removeAll(); - }, + initComponent: function() { + Deluge.EditTrackersWindow.superclass.initComponent.call(this); - onListNodeDblClicked: function(list, index, node, e) { - this.editWindow.show(this.list.getRecord(node)); - }, - - onOkClick: function() { - var trackers = []; - this.list.getStore().each(function(record) { - trackers.push({ - 'tier': record.get('tier'), - 'url': record.get('url') - }) - }, this); - - deluge.client.core.set_torrent_trackers(this.torrentId, trackers, { - failure: this.onSaveFail, - scope: this - }); + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Ok'), this.onOkClick, this); + this.addEvents('save'); - this.hide(); - }, - - onRemoveClick: function() { - // Remove from the grid - this.list.getStore().remove(this.list.getSelectedRecords()[0]); - }, - - onRequestComplete: function(status) { - this.list.getStore().loadData(status); - this.list.getStore().sort('tier', 'ASC'); - }, - - onSaveFail: function() { - - }, - - onSelect: function(list) { - if (list.getSelectionCount()) { - this.panel.getBottomToolbar().items.get(4).enable(); - } - }, - - onShow: function() { - this.panel.getBottomToolbar().items.get(4).disable(); - var r = deluge.torrents.getSelected(); - this.torrentId = r.id; - deluge.client.core.get_torrent_status(r.id, ['trackers'], { - success: this.onRequestComplete, - scope: this - }); - }, + this.on('show', this.onShow, this); + this.on('save', this.onSave, this); - onDownClick: function() { - var r = this.list.getSelectedRecords()[0]; - r.set('tier', r.get('tier') + 1); - r.commit(); - r.store.sort('tier', 'ASC'); - }, + this.addWindow = new Deluge.AddTrackerWindow(); + this.addWindow.on('add', this.onAddTrackers, this); + this.editWindow = new Deluge.EditTrackerWindow(); - onUpClick: function() { - var r = this.list.getSelectedRecords()[0]; - if (r.get('tier') == 0) return; - r.set('tier', r.get('tier') - 1); - r.commit(); - r.store.sort('tier', 'ASC'); - } + this.list = new Ext.list.ListView({ + store: new Ext.data.JsonStore({ + root: 'trackers', + fields: [ + 'tier', + 'url' + ] + }), + columns: [{ + header: _('Tier'), + width: .1, + dataIndex: 'tier' + }, { + header: _('Tracker'), + width: .9, + dataIndex: 'url' + }], + columnSort: { + sortClasses: ['', ''] + }, + stripeRows: true, + singleSelect: true, + listeners: { + 'dblclick': {fn: this.onListNodeDblClicked, scope: this}, + 'selectionchange': {fn: this.onSelect, scope: this} + } + }); + + this.panel = this.add({ + margins: '0 0 0 0', + items: [this.list], + autoScroll: true, + bbar: new Ext.Toolbar({ + items: [ + { + text: _('Up'), + iconCls: 'icon-up', + handler: this.onUpClick, + scope: this + }, { + text: _('Down'), + iconCls: 'icon-down', + handler: this.onDownClick, + scope: this + }, '->', { + text: _('Add'), + iconCls: 'icon-add', + handler: this.onAddClick, + scope: this + }, { + text: _('Edit'), + iconCls: 'icon-edit-trackers', + handler: this.onEditClick, + scope: this + }, { + text: _('Remove'), + iconCls: 'icon-remove', + handler: this.onRemoveClick, + scope: this + } + ] + }) + }); + }, + + onAddClick: function() { + this.addWindow.show(); + }, + + onAddTrackers: function(trackers) { + var store = this.list.getStore(); + Ext.each(trackers, function(tracker) { + var duplicate = false, heightestTier = -1; + store.each(function(record) { + if (record.get('tier') > heightestTier) { + heightestTier = record.get('tier'); + } + if (tracker == record.get('tracker')) { + duplicate = true; + return false; + } + }, this); + if (duplicate) return; + store.add(new store.recordType({'tier': heightestTier + 1, 'url': tracker})); + }, this); + }, + + onCancelClick: function() { + this.hide(); + }, + + onEditClick: function() { + this.editWindow.show(this.list.getSelectedRecords()[0]); + }, + + onHide: function() { + this.list.getStore().removeAll(); + }, + + onListNodeDblClicked: function(list, index, node, e) { + this.editWindow.show(this.list.getRecord(node)); + }, + + onOkClick: function() { + var trackers = []; + this.list.getStore().each(function(record) { + trackers.push({ + 'tier': record.get('tier'), + 'url': record.get('url') + }) + }, this); + + deluge.client.core.set_torrent_trackers(this.torrentId, trackers, { + failure: this.onSaveFail, + scope: this + }); + + this.hide(); + }, + + onRemoveClick: function() { + // Remove from the grid + this.list.getStore().remove(this.list.getSelectedRecords()[0]); + }, + + onRequestComplete: function(status) { + this.list.getStore().loadData(status); + this.list.getStore().sort('tier', 'ASC'); + }, + + onSaveFail: function() { + + }, + + onSelect: function(list) { + if (list.getSelectionCount()) { + this.panel.getBottomToolbar().items.get(4).enable(); + } + }, + + onShow: function() { + this.panel.getBottomToolbar().items.get(4).disable(); + var r = deluge.torrents.getSelected(); + this.torrentId = r.id; + deluge.client.core.get_torrent_status(r.id, ['trackers'], { + success: this.onRequestComplete, + scope: this + }); + }, + + onDownClick: function() { + var r = this.list.getSelectedRecords()[0]; + if (!r) return; + + r.set('tier', r.get('tier') + 1); + r.store.sort('tier', 'ASC'); + r.store.commitChanges(); + + this.list.select(r.store.indexOf(r)); + }, + + onUpClick: function() { + var r = this.list.getSelectedRecords()[0]; + if (!r) return; + + if (r.get('tier') == 0) return; + r.set('tier', r.get('tier') - 1); + r.store.sort('tier', 'ASC'); + r.store.commitChanges(); + + this.list.select(r.store.indexOf(r)); + } }); diff --git a/deluge/ui/web/js/deluge-all/EventsManager.js b/deluge/ui/web/js/deluge-all/EventsManager.js index 0981d4393..9a799d53d 100644 --- a/deluge/ui/web/js/deluge-all/EventsManager.js +++ b/deluge/ui/web/js/deluge-all/EventsManager.js @@ -37,79 +37,79 @@ * Class for holding global events that occur within the UI. */ Deluge.EventsManager = Ext.extend(Ext.util.Observable, { - constructor: function() { - this.toRegister = []; - this.on('login', this.onLogin, this); - Deluge.EventsManager.superclass.constructor.call(this); - }, - - /** - * Append an event handler to this object. - */ - addListener: function(eventName, fn, scope, o) { - this.addEvents(eventName); - if (/[A-Z]/.test(eventName.substring(0, 1))) { - if (!deluge.client) { - this.toRegister.push(eventName); - } else { - deluge.client.web.register_event_listener(eventName); - } - } - Deluge.EventsManager.superclass.addListener.call(this, eventName, fn, scope, o); - }, + constructor: function() { + this.toRegister = []; + this.on('login', this.onLogin, this); + Deluge.EventsManager.superclass.constructor.call(this); + }, + + /** + * Append an event handler to this object. + */ + addListener: function(eventName, fn, scope, o) { + this.addEvents(eventName); + if (/[A-Z]/.test(eventName.substring(0, 1))) { + if (!deluge.client) { + this.toRegister.push(eventName); + } else { + deluge.client.web.register_event_listener(eventName); + } + } + Deluge.EventsManager.superclass.addListener.call(this, eventName, fn, scope, o); + }, - getEvents: function() { - deluge.client.web.get_events({ - success: this.onGetEventsSuccess, - failure: this.onGetEventsFailure, - scope: this - }); - }, + getEvents: function() { + deluge.client.web.get_events({ + success: this.onGetEventsSuccess, + failure: this.onGetEventsFailure, + scope: this + }); + }, - /** - * Starts the EventsManagerManager checking for events. - */ - start: function() { - Ext.each(this.toRegister, function(eventName) { - deluge.client.web.register_event_listener(eventName); - }); - this.running = true; - this.errorCount = 0; - this.getEvents(); - }, + /** + * Starts the EventsManagerManager checking for events. + */ + start: function() { + Ext.each(this.toRegister, function(eventName) { + deluge.client.web.register_event_listener(eventName); + }); + this.running = true; + this.errorCount = 0; + this.getEvents(); + }, - /** - * Stops the EventsManagerManager checking for events. - */ - stop: function() { - this.running = false; - }, + /** + * Stops the EventsManagerManager checking for events. + */ + stop: function() { + this.running = false; + }, - // private - onLogin: function() { - this.start(); - }, + // private + onLogin: function() { + this.start(); + }, - onGetEventsSuccess: function(events) { - if (!events) return; - Ext.each(events, function(event) { - var name = event[0], args = event[1]; - args.splice(0, 0, name); - this.fireEvent.apply(this, args); - }, this); - if (this.running) this.getEvents(); - }, + onGetEventsSuccess: function(events) { + if (!events) return; + Ext.each(events, function(event) { + var name = event[0], args = event[1]; + args.splice(0, 0, name); + this.fireEvent.apply(this, args); + }, this); + if (this.running) this.getEvents(); + }, - // private - onGetEventsFailure: function(result, error) { - // the request timed out or we had a communication failure - if (!this.running) return; - if (!error.isTimeout && this.errorCount++ >= 3) { - this.stop(); - return; - } - this.getEvents(); - } + // private + onGetEventsFailure: function(result, error) { + // the request timed out or we had a communication failure + if (!this.running) return; + if (!error.isTimeout && this.errorCount++ >= 3) { + this.stop(); + return; + } + this.getEvents(); + } }); /** diff --git a/deluge/ui/web/js/deluge-all/FileBrowser.js b/deluge/ui/web/js/deluge-all/FileBrowser.js index 0bb89236d..0487847bd 100644 --- a/deluge/ui/web/js/deluge-all/FileBrowser.js +++ b/deluge/ui/web/js/deluge-all/FileBrowser.js @@ -33,30 +33,30 @@ Ext.namespace('Deluge'); Deluge.FileBrowser = Ext.extend(Ext.Window, { - title: _('File Browser'), + title: _('File Browser'), - width: 500, - height: 400, + width: 500, + height: 400, - initComponent: function() { - Deluge.FileBrowser.superclass.initComponent.call(this); + initComponent: function() { + Deluge.FileBrowser.superclass.initComponent.call(this); - this.add({ - xtype: 'toolbar', - items: [{ - text: _('Back'), - iconCls: 'icon-back' - }, { - text: _('Forward'), - iconCls: 'icon-forward' - }, { - text: _('Up'), - iconCls: 'icon-up' - }, { - text: _('Home'), - iconCls: 'icon-home' - }] - }); - } + this.add({ + xtype: 'toolbar', + items: [{ + text: _('Back'), + iconCls: 'icon-back' + }, { + text: _('Forward'), + iconCls: 'icon-forward' + }, { + text: _('Up'), + iconCls: 'icon-up' + }, { + text: _('Home'), + iconCls: 'icon-home' + }] + }); + } }); diff --git a/deluge/ui/web/js/deluge-all/FilterPanel.js b/deluge/ui/web/js/deluge-all/FilterPanel.js index d40f5e8a0..703836d77 100644 --- a/deluge/ui/web/js/deluge-all/FilterPanel.js +++ b/deluge/ui/web/js/deluge-all/FilterPanel.js @@ -1,6 +1,6 @@ /*! * Deluge.FilterPanel.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -36,133 +36,135 @@ Ext.ns('Deluge'); * @extends Ext.list.ListView */ Deluge.FilterPanel = Ext.extend(Ext.Panel, { - - border: false, - show_zero: null, + autoScroll: true, - initComponent: function() { - Deluge.FilterPanel.superclass.initComponent.call(this); - this.filterType = this.initialConfig.filter; + border: false, - var title = this.filterType.replace('_', ' '), - parts = title.split(' '), - title = ''; - Ext.each(parts, function(p) { - fl = p.substring(0, 1).toUpperCase(); - title += fl + p.substring(1) + ' '; - }); - this.setTitle(_(title)); + show_zero: null, - if (Deluge.FilterPanel.templates[this.filterType]) { - var tpl = Deluge.FilterPanel.templates[this.filterType]; - } else { - var tpl = '
{filter} ({count})
'; - } + initComponent: function() { + Deluge.FilterPanel.superclass.initComponent.call(this); + this.filterType = this.initialConfig.filter; - this.list = this.add({ - xtype: 'listview', - singleSelect: true, - hideHeaders: true, - reserveScrollOffset: true, - store: new Ext.data.ArrayStore({ - idIndex: 0, - fields: ['filter', 'count'] - }), - columns: [{ - id: 'filter', - sortable: false, - tpl: tpl, - dataIndex: 'filter' - }] - }); - this.relayEvents(this.list, ['selectionchange']); - }, + var title = this.filterType.replace('_', ' '), + parts = title.split(' '), + title = ''; + Ext.each(parts, function(p) { + fl = p.substring(0, 1).toUpperCase(); + title += fl + p.substring(1) + ' '; + }); + this.setTitle(_(title)); - /** - * Return the currently selected filter state - * @returns {String} the current filter state - */ - getState: function() { - if (!this.list.getSelectionCount()) return; + if (Deluge.FilterPanel.templates[this.filterType]) { + var tpl = Deluge.FilterPanel.templates[this.filterType]; + } else { + var tpl = '
{filter} ({count})
'; + } - var state = this.list.getSelectedRecords()[0]; - if (state.id == 'All') return; - return state.id; - }, + this.list = this.add({ + xtype: 'listview', + singleSelect: true, + hideHeaders: true, + reserveScrollOffset: true, + store: new Ext.data.ArrayStore({ + idIndex: 0, + fields: ['filter', 'count'] + }), + columns: [{ + id: 'filter', + sortable: false, + tpl: tpl, + dataIndex: 'filter' + }] + }); + this.relayEvents(this.list, ['selectionchange']); + }, - /** - * Return the current states in the filter - */ - getStates: function() { - return this.states; - }, + /** + * Return the currently selected filter state + * @returns {String} the current filter state + */ + getState: function() { + if (!this.list.getSelectionCount()) return; - /** - * Return the Store for the ListView of the FilterPanel - * @returns {Ext.data.Store} the ListView store - */ - getStore: function() { - return this.list.getStore(); - }, + var state = this.list.getSelectedRecords()[0]; + if (state.id == 'All') return; + return state.id; + }, - /** - * Update the states in the FilterPanel - */ - updateStates: function(states) { - this.states = {}; - Ext.each(states, function(state) { - this.states[state[0]] = state[1]; - }, this); + /** + * Return the current states in the filter + */ + getStates: function() { + return this.states; + }, - var show_zero = (this.show_zero == null) ? deluge.config.sidebar_show_zero : this.show_zero; - if (!show_zero) { - var newStates = []; - Ext.each(states, function(state) { - if (state[1] > 0 || state[0] == _('All')) { - newStates.push(state); - } - }); - states = newStates; - } + /** + * Return the Store for the ListView of the FilterPanel + * @returns {Ext.data.Store} the ListView store + */ + getStore: function() { + return this.list.getStore(); + }, - var store = this.getStore(); - var filters = {}; - Ext.each(states, function(s, i) { - var record = store.getById(s[0]); - if (!record) { - record = new store.recordType({ - filter: s[0], - count: s[1] - }); - record.id = s[0]; - store.insert(i, record); - } - record.beginEdit(); - record.set('filter', s[0]); - record.set('count', s[1]); - record.endEdit(); - filters[s[0]] = true; - }, this); + /** + * Update the states in the FilterPanel + */ + updateStates: function(states) { + this.states = {}; + Ext.each(states, function(state) { + this.states[state[0]] = state[1]; + }, this); - store.each(function(record) { - if (filters[record.id]) return; - var r = this.list.getSelectedRecords()[0]; - store.remove(record); - if (r.id == record.id) { - this.list.select(0); - } - }, this); + var show_zero = (this.show_zero == null) ? deluge.config.sidebar_show_zero : this.show_zero; + if (!show_zero) { + var newStates = []; + Ext.each(states, function(state) { + if (state[1] > 0 || state[0] == _('All')) { + newStates.push(state); + } + }); + states = newStates; + } - store.commitChanges(); + var store = this.getStore(); + var filters = {}; + Ext.each(states, function(s, i) { + var record = store.getById(s[0]); + if (!record) { + record = new store.recordType({ + filter: s[0], + count: s[1] + }); + record.id = s[0]; + store.insert(i, record); + } + record.beginEdit(); + record.set('filter', s[0]); + record.set('count', s[1]); + record.endEdit(); + filters[s[0]] = true; + }, this); - if (!this.list.getSelectionCount()) { - this.list.select(0); - } - } + store.each(function(record) { + if (filters[record.id]) return; + var r = this.list.getSelectedRecords()[0]; + store.remove(record); + if (r.id == record.id) { + this.list.select(0); + } + }, this); + + store.commitChanges(); + + if (!this.list.getSelectionCount()) { + this.list.select(0); + } + } }); Deluge.FilterPanel.templates = { - 'tracker_host': '
{filter} ({count})
' + 'tracker_host': '
{filter} ({count})
' } diff --git a/deluge/ui/web/js/deluge-all/Formatters.js b/deluge/ui/web/js/deluge-all/Formatters.js index 902922461..0ea3b6903 100644 --- a/deluge/ui/web/js/deluge-all/Formatters.js +++ b/deluge/ui/web/js/deluge-all/Formatters.js @@ -38,115 +38,115 @@ * @singleton */ Deluge.Formatters = { - /** - * Formats a date string in the locale's date representation based on the - * systems timezone. - * - * @param {Number} timestamp time in seconds since the Epoch - * @return {String} a string in the locale's date representation or "" - * if seconds < 0 - */ - date: function(timestamp) { - function zeroPad(num, count) { - var numZeropad = num + ''; - while (numZeropad.length < count) { - numZeropad = '0' + numZeropad; - } - return numZeropad; - } - timestamp = timestamp * 1000; - var date = new Date(timestamp); - return String.format('{0}/{1}/{2} {3}:{4}:{5}', - zeroPad(date.getDate(), 2), zeroPad(date.getMonth() + 1, 2), date.getFullYear(), - zeroPad(date.getHours(), 2), zeroPad(date.getMinutes(), 2), zeroPad(date.getSeconds(), 2)); - }, - - /** - * Formats the bytes value into a string with KiB, MiB or GiB units. - * - * @param {Number} bytes the filesize in bytes - * @param {Boolean} showZero pass in true to displays 0 values - * @return {String} formatted string with KiB, MiB or GiB units. - */ - size: function(bytes, showZero) { - if (!bytes && !showZero) return ''; - bytes = bytes / 1024.0; - - if (bytes < 1024) { return bytes.toFixed(1) + ' KiB'; } - else { bytes = bytes / 1024; } - - if (bytes < 1024) { return bytes.toFixed(1) + ' MiB'; } - else { bytes = bytes / 1024; } - - return bytes.toFixed(1) + ' GiB' - }, - - /** - * Formats a string to display a transfer speed utilizing {@link #size} - * - * @param {Number} bytes the number of bytes per second - * @param {Boolean} showZero pass in true to displays 0 values - * @return {String} formatted string with KiB, MiB or GiB units. - */ - speed: function(bytes, showZero) { - return (!bytes && !showZero) ? '' : fsize(bytes, showZero) + '/s'; - }, - - /** - * Formats a string to show time in a human readable form. - * - * @param {Number} time the number of seconds - * @return {String} a formatted time string. will return '' if seconds == 0 - */ - timeRemaining: function(time) { - if (time == 0) { return '∞' } + /** + * Formats a date string in the locale's date representation based on the + * systems timezone. + * + * @param {Number} timestamp time in seconds since the Epoch + * @return {String} a string in the locale's date representation or "" + * if seconds < 0 + */ + date: function(timestamp) { + function zeroPad(num, count) { + var numZeropad = num + ''; + while (numZeropad.length < count) { + numZeropad = '0' + numZeropad; + } + return numZeropad; + } + timestamp = timestamp * 1000; + var date = new Date(timestamp); + return String.format('{0}/{1}/{2} {3}:{4}:{5}', + zeroPad(date.getDate(), 2), zeroPad(date.getMonth() + 1, 2), date.getFullYear(), + zeroPad(date.getHours(), 2), zeroPad(date.getMinutes(), 2), zeroPad(date.getSeconds(), 2)); + }, + + /** + * Formats the bytes value into a string with KiB, MiB or GiB units. + * + * @param {Number} bytes the filesize in bytes + * @param {Boolean} showZero pass in true to displays 0 values + * @return {String} formatted string with KiB, MiB or GiB units. + */ + size: function(bytes, showZero) { + if (!bytes && !showZero) return ''; + bytes = bytes / 1024.0; + + if (bytes < 1024) { return bytes.toFixed(1) + ' KiB'; } + else { bytes = bytes / 1024; } + + if (bytes < 1024) { return bytes.toFixed(1) + ' MiB'; } + else { bytes = bytes / 1024; } + + return bytes.toFixed(1) + ' GiB' + }, + + /** + * Formats a string to display a transfer speed utilizing {@link #size} + * + * @param {Number} bytes the number of bytes per second + * @param {Boolean} showZero pass in true to displays 0 values + * @return {String} formatted string with KiB, MiB or GiB units. + */ + speed: function(bytes, showZero) { + return (!bytes && !showZero) ? '' : fsize(bytes, showZero) + '/s'; + }, + + /** + * Formats a string to show time in a human readable form. + * + * @param {Number} time the number of seconds + * @return {String} a formatted time string. will return '' if seconds == 0 + */ + timeRemaining: function(time) { + if (time == 0) { return '∞' } time = time.toFixed(0); - if (time < 60) { return time + 's'; } - else { time = time / 60; } - - if (time < 60) { - var minutes = Math.floor(time) - var seconds = Math.round(60 * (time - minutes)) - if (seconds > 0) { - return minutes + 'm ' + seconds + 's'; - } else { - return minutes + 'm'; } - } - else { time = time / 60; } - - if (time < 24) { - var hours = Math.floor(time) - var minutes = Math.round(60 * (time - hours)) - if (minutes > 0) { - return hours + 'h ' + minutes + 'm'; - } else { - return hours + 'h'; - } - } - else { time = time / 24; } - - var days = Math.floor(time) - var hours = Math.round(24 * (time - days)) - if (hours > 0) { - return days + 'd ' + hours + 'h'; - } else { - return days + 'd'; - } - }, - - /** - * Simply returns the value untouched, for when no formatting is required. - * - * @param {Mixed} value the value to be displayed - * @return the untouched value. - */ - plain: function(value) { - return value; - }, + if (time < 60) { return time + 's'; } + else { time = time / 60; } + + if (time < 60) { + var minutes = Math.floor(time) + var seconds = Math.round(60 * (time - minutes)) + if (seconds > 0) { + return minutes + 'm ' + seconds + 's'; + } else { + return minutes + 'm'; } + } + else { time = time / 60; } + + if (time < 24) { + var hours = Math.floor(time) + var minutes = Math.round(60 * (time - hours)) + if (minutes > 0) { + return hours + 'h ' + minutes + 'm'; + } else { + return hours + 'h'; + } + } + else { time = time / 24; } + + var days = Math.floor(time) + var hours = Math.round(24 * (time - days)) + if (hours > 0) { + return days + 'd ' + hours + 'h'; + } else { + return days + 'd'; + } + }, + + /** + * Simply returns the value untouched, for when no formatting is required. + * + * @param {Mixed} value the value to be displayed + * @return the untouched value. + */ + plain: function(value) { + return value; + }, - cssClassEscape: function(value) { - return value.toLowerCase().replace('.', '_'); - } + cssClassEscape: function(value) { + return value.toLowerCase().replace('.', '_'); + } } var fsize = Deluge.Formatters.size; var fspeed = Deluge.Formatters.speed; diff --git a/deluge/ui/web/js/deluge-all/Keys.js b/deluge/ui/web/js/deluge-all/Keys.js index fe1c0b4b1..f41f53ae5 100644 --- a/deluge/ui/web/js/deluge-all/Keys.js +++ b/deluge/ui/web/js/deluge-all/Keys.js @@ -1,6 +1,6 @@ /*! * Deluge.Keys.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -37,69 +37,70 @@ */ Deluge.Keys = { - /** - * Keys that are used within the torrent grid. - *
['queue', 'name', 'total_size', 'state', 'progress', 'num_seeds',
-	 * 'total_seeds', 'num_peers', 'total_peers', 'download_payload_rate',
-	 * 'upload_payload_rate', 'eta', 'ratio', 'distributed_copies',
-	 * 'is_auto_managed', 'time_added', 'tracker_host']
- */ + /** + * Keys that are used within the torrent grid. + *
['queue', 'name', 'total_size', 'state', 'progress', 'num_seeds',
+     * 'total_seeds', 'num_peers', 'total_peers', 'download_payload_rate',
+     * 'upload_payload_rate', 'eta', 'ratio', 'distributed_copies',
+     * 'is_auto_managed', 'time_added', 'tracker_host']
+ */ Grid: [ 'queue', 'name', 'total_size', 'state', 'progress', 'num_seeds', 'total_seeds', 'num_peers', 'total_peers', 'download_payload_rate', 'upload_payload_rate', 'eta', 'ratio', 'distributed_copies', 'is_auto_managed', 'time_added', 'tracker_host', 'save_path' ], - + /** * Keys used in the status tab of the statistics panel. * These get updated to include the keys in {@link #Grid}. - *
['total_done', 'total_payload_download', 'total_uploaded',
-	 * 'total_payload_upload', 'next_announce', 'tracker_status', 'num_pieces',
-	 * 'piece_length', 'is_auto_managed', 'active_time', 'seeding_time',
-	 * 'seed_rank']
- */ + *
['total_done', 'total_payload_download', 'total_uploaded',
+     * 'total_payload_upload', 'next_announce', 'tracker_status', 'num_pieces',
+     * 'piece_length', 'is_auto_managed', 'active_time', 'seeding_time',
+     * 'seed_rank']
+ */ Status: [ 'total_done', 'total_payload_download', 'total_uploaded', 'total_payload_upload', 'next_announce', 'tracker_status', 'num_pieces', 'piece_length', 'is_auto_managed', 'active_time', 'seeding_time', - 'seed_rank' + 'seed_rank', 'last_seen_complete', 'owner', 'public', 'shared' ], - + /** * Keys used in the files tab of the statistics panel. *
['files', 'file_progress', 'file_priorities']
- */ + */ Files: [ 'files', 'file_progress', 'file_priorities' ], - + /** * Keys used in the peers tab of the statistics panel. *
['peers']
- */ + */ Peers: [ 'peers' ], - + /** * Keys used in the details tab of the statistics panel. - */ + */ Details: [ - 'name', 'save_path', 'total_size', 'num_files', 'tracker_status', + 'name', 'save_path', 'total_size', 'num_files', 'message', 'tracker', 'comment' ], - + /** - * Keys used in the options tab of the statistics panel. - *
['max_download_speed', 'max_upload_speed', 'max_connections', 'max_upload_slots',
-	 *  'is_auto_managed', 'stop_at_ratio', 'stop_ratio', 'remove_at_ratio', 'private',
-	 *  'prioritize_first_last']
- */ + * Keys used in the options tab of the statistics panel. + *
['max_download_speed', 'max_upload_speed', 'max_connections', 'max_upload_slots',
+     *  'is_auto_managed', 'stop_at_ratio', 'stop_ratio', 'remove_at_ratio', 'private',
+     *  'prioritize_first_last']
+ */ Options: [ 'max_download_speed', 'max_upload_speed', 'max_connections', 'max_upload_slots','is_auto_managed', 'stop_at_ratio', 'stop_ratio', - 'remove_at_ratio', 'private', 'prioritize_first_last' + 'remove_at_ratio', 'private', 'prioritize_first_last', + 'move_completed', 'move_completed_path' ] }; diff --git a/deluge/ui/web/js/deluge-all/LoginWindow.js b/deluge/ui/web/js/deluge-all/LoginWindow.js index 527c21842..37b18a184 100644 --- a/deluge/ui/web/js/deluge-all/LoginWindow.js +++ b/deluge/ui/web/js/deluge-all/LoginWindow.js @@ -31,123 +31,123 @@ */ Deluge.LoginWindow = Ext.extend(Ext.Window, { - - firstShow: true, - bodyStyle: 'padding: 10px 5px;', - buttonAlign: 'center', - closable: false, - closeAction: 'hide', - iconCls: 'x-deluge-login-window-icon', - layout: 'fit', - modal: true, - plain: true, - resizable: false, - title: _('Login'), - width: 300, - height: 120, - - initComponent: function() { - Deluge.LoginWindow.superclass.initComponent.call(this); - this.on('show', this.onShow, this); - - this.addButton({ - text: _('Login'), - handler: this.onLogin, - scope: this - }); - - this.form = this.add({ - xtype: 'form', - baseCls: 'x-plain', - labelWidth: 55, - width: 300, - defaults: {width: 200}, - defaultType: 'textfield' - }); + + firstShow: true, + bodyStyle: 'padding: 10px 5px;', + buttonAlign: 'center', + closable: false, + closeAction: 'hide', + iconCls: 'x-deluge-login-window-icon', + layout: 'fit', + modal: true, + plain: true, + resizable: false, + title: _('Login'), + width: 300, + height: 120, + + initComponent: function() { + Deluge.LoginWindow.superclass.initComponent.call(this); + this.on('show', this.onShow, this); + + this.addButton({ + text: _('Login'), + handler: this.onLogin, + scope: this + }); + + this.form = this.add({ + xtype: 'form', + baseCls: 'x-plain', + labelWidth: 55, + width: 300, + defaults: {width: 200}, + defaultType: 'textfield' + }); - this.passwordField = this.form.add({ - xtype: 'textfield', - fieldLabel: _('Password'), - id: '_password', - name: 'password', - inputType: 'password' - }); - this.passwordField.on('specialkey', this.onSpecialKey, this); - }, - - logout: function() { - deluge.events.fire('logout'); - deluge.client.auth.delete_session({ - success: function(result) { - this.show(true); - }, - scope: this - }); - }, - - show: function(skipCheck) { - if (this.firstShow) { - deluge.client.on('error', this.onClientError, this); - this.firstShow = false; - } - - if (skipCheck) { - return Deluge.LoginWindow.superclass.show.call(this); - } - - deluge.client.auth.check_session({ - success: function(result) { - if (result) { - deluge.events.fire('login'); - } else { - this.show(true); - } - }, - failure: function(result) { - this.show(true); - }, - scope: this - }); - }, - - onSpecialKey: function(field, e) { - if (e.getKey() == 13) this.onLogin(); - }, - - onLogin: function() { - var passwordField = this.passwordField; - deluge.client.auth.login(passwordField.getValue(), { - success: function(result) { - if (result) { - deluge.events.fire('login'); - this.hide(); - passwordField.setRawValue(''); - } else { - Ext.MessageBox.show({ - title: _('Login Failed'), - msg: _('You entered an incorrect password'), - buttons: Ext.MessageBox.OK, - modal: false, - fn: function() { - passwordField.focus(true, 10); - }, - icon: Ext.MessageBox.WARNING, - iconCls: 'x-deluge-icon-warning' - }); - } - }, - scope: this - }); - }, - - onClientError: function(errorObj, response, requestOptions) { - if (errorObj.error.code == 1) { - deluge.events.fire('logout'); - this.show(true); - } - }, - - onShow: function() { - this.passwordField.focus(true, true); - } + this.passwordField = this.form.add({ + xtype: 'textfield', + fieldLabel: _('Password'), + id: '_password', + name: 'password', + inputType: 'password' + }); + this.passwordField.on('specialkey', this.onSpecialKey, this); + }, + + logout: function() { + deluge.events.fire('logout'); + deluge.client.auth.delete_session({ + success: function(result) { + this.show(true); + }, + scope: this + }); + }, + + show: function(skipCheck) { + if (this.firstShow) { + deluge.client.on('error', this.onClientError, this); + this.firstShow = false; + } + + if (skipCheck) { + return Deluge.LoginWindow.superclass.show.call(this); + } + + deluge.client.auth.check_session({ + success: function(result) { + if (result) { + deluge.events.fire('login'); + } else { + this.show(true); + } + }, + failure: function(result) { + this.show(true); + }, + scope: this + }); + }, + + onSpecialKey: function(field, e) { + if (e.getKey() == 13) this.onLogin(); + }, + + onLogin: function() { + var passwordField = this.passwordField; + deluge.client.auth.login(passwordField.getValue(), { + success: function(result) { + if (result) { + deluge.events.fire('login'); + this.hide(); + passwordField.setRawValue(''); + } else { + Ext.MessageBox.show({ + title: _('Login Failed'), + msg: _('You entered an incorrect password'), + buttons: Ext.MessageBox.OK, + modal: false, + fn: function() { + passwordField.focus(true, 10); + }, + icon: Ext.MessageBox.WARNING, + iconCls: 'x-deluge-icon-warning' + }); + } + }, + scope: this + }); + }, + + onClientError: function(errorObj, response, requestOptions) { + if (errorObj.error.code == 1) { + deluge.events.fire('logout'); + this.show(true); + } + }, + + onShow: function() { + this.passwordField.focus(true, true); + } }); diff --git a/deluge/ui/web/js/deluge-all/Menus.js b/deluge/ui/web/js/deluge-all/Menus.js index 78c5ac108..3f71ef94c 100644 --- a/deluge/ui/web/js/deluge-all/Menus.js +++ b/deluge/ui/web/js/deluge-all/Menus.js @@ -31,240 +31,240 @@ */ deluge.menus = { - onTorrentAction: function(item, e) { - var ids = deluge.torrents.getSelectedIds(); - var action = item.initialConfig.torrentAction; - - switch (action) { - case 'pause': - case 'resume': - deluge.client.core[action + '_torrent'](ids, { - success: function() { - deluge.ui.update(); - } - }); - break; - case 'top': - case 'up': - case 'down': - case 'bottom': - deluge.client.core['queue_' + action](ids, { - success: function() { - deluge.ui.update(); - } - }); - break; - case 'edit_trackers': - deluge.editTrackers.show(); - break; - case 'update': - deluge.client.core.force_reannounce(ids, { - success: function() { - deluge.ui.update(); - } - }); - break; - case 'remove': - deluge.removeWindow.show(ids); - break; - case 'recheck': - deluge.client.core.force_recheck(ids, { - success: function() { - deluge.ui.update(); - } - }); - break; - case 'move': - deluge.moveStorage.show(ids); - break; - } - } + onTorrentAction: function(item, e) { + var ids = deluge.torrents.getSelectedIds(); + var action = item.initialConfig.torrentAction; + + switch (action) { + case 'pause': + case 'resume': + deluge.client.core[action + '_torrent'](ids, { + success: function() { + deluge.ui.update(); + } + }); + break; + case 'top': + case 'up': + case 'down': + case 'bottom': + deluge.client.core['queue_' + action](ids, { + success: function() { + deluge.ui.update(); + } + }); + break; + case 'edit_trackers': + deluge.editTrackers.show(); + break; + case 'update': + deluge.client.core.force_reannounce(ids, { + success: function() { + deluge.ui.update(); + } + }); + break; + case 'remove': + deluge.removeWindow.show(ids); + break; + case 'recheck': + deluge.client.core.force_recheck(ids, { + success: function() { + deluge.ui.update(); + } + }); + break; + case 'move': + deluge.moveStorage.show(ids); + break; + } + } } deluge.menus.torrent = new Ext.menu.Menu({ - id: 'torrentMenu', - items: [{ - torrentAction: 'pause', - text: _('Pause'), - iconCls: 'icon-pause', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }, { - torrentAction: 'resume', - text: _('Resume'), - iconCls: 'icon-resume', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }, '-', { - text: _('Options'), - iconCls: 'icon-options', - menu: new Ext.menu.Menu({ - items: [{ - text: _('D/L Speed Limit'), - iconCls: 'x-deluge-downloading', - menu: new Ext.menu.Menu({ - items: [{ - text: _('5 KiB/s') - }, { - text: _('10 KiB/s') - }, { - text: _('30 KiB/s') - }, { - text: _('80 KiB/s') - }, { - text: _('300 KiB/s') - },{ - text: _('Unlimited') - }] - }) - }, { - text: _('U/L Speed Limit'), - iconCls: 'x-deluge-seeding', - menu: new Ext.menu.Menu({ - items: [{ - text: _('5 KiB/s') - }, { - text: _('10 KiB/s') - }, { - text: _('30 KiB/s') - }, { - text: _('80 KiB/s') - }, { - text: _('300 KiB/s') - },{ - text: _('Unlimited') - }] - }) - }, { - text: _('Connection Limit'), - iconCls: 'x-deluge-connections', - menu: new Ext.menu.Menu({ - items: [{ - text: _('50') - }, { - text: _('100') - }, { - text: _('200') - }, { - text: _('300') - }, { - text: _('500') - },{ - text: _('Unlimited') - }] - }) - }, { - text: _('Upload Slot Limit'), - iconCls: 'icon-upload-slots', - menu: new Ext.menu.Menu({ - items: [{ - text: _('0') - }, { - text: _('1') - }, { - text: _('2') - }, { - text: _('3') - }, { - text: _('5') - },{ - text: _('Unlimited') - }] - }) - }, { - id: 'auto_managed', - text: _('Auto Managed'), - checked: false - }] - }) - }, '-', { - text: _('Queue'), - iconCls: 'icon-queue', - menu: new Ext.menu.Menu({ - items: [{ - torrentAction: 'top', - text: _('Top'), - iconCls: 'icon-top', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - },{ - torrentAction: 'up', - text: _('Up'), - iconCls: 'icon-up', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - },{ - torrentAction: 'down', - text: _('Down'), - iconCls: 'icon-down', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - },{ - torrentAction: 'bottom', - text: _('Bottom'), - iconCls: 'icon-bottom', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }] - }) - }, '-', { - torrentAction: 'update', - text: _('Update Tracker'), - iconCls: 'icon-update-tracker', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }, { - torrentAction: 'edit_trackers', - text: _('Edit Trackers'), - iconCls: 'icon-edit-trackers', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }, '-', { - torrentAction: 'remove', - text: _('Remove Torrent'), - iconCls: 'icon-remove', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }, '-', { - torrentAction: 'recheck', - text: _('Force Recheck'), - iconCls: 'icon-recheck', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }, { - torrentAction: 'move', - text: _('Move Storage'), - iconCls: 'icon-move', - handler: deluge.menus.onTorrentAction, - scope: deluge.menus - }] + id: 'torrentMenu', + items: [{ + torrentAction: 'pause', + text: _('Pause'), + iconCls: 'icon-pause', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }, { + torrentAction: 'resume', + text: _('Resume'), + iconCls: 'icon-resume', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }, '-', { + text: _('Options'), + iconCls: 'icon-options', + menu: new Ext.menu.Menu({ + items: [{ + text: _('D/L Speed Limit'), + iconCls: 'x-deluge-downloading', + menu: new Ext.menu.Menu({ + items: [{ + text: _('5 KiB/s') + }, { + text: _('10 KiB/s') + }, { + text: _('30 KiB/s') + }, { + text: _('80 KiB/s') + }, { + text: _('300 KiB/s') + },{ + text: _('Unlimited') + }] + }) + }, { + text: _('U/L Speed Limit'), + iconCls: 'x-deluge-seeding', + menu: new Ext.menu.Menu({ + items: [{ + text: _('5 KiB/s') + }, { + text: _('10 KiB/s') + }, { + text: _('30 KiB/s') + }, { + text: _('80 KiB/s') + }, { + text: _('300 KiB/s') + },{ + text: _('Unlimited') + }] + }) + }, { + text: _('Connection Limit'), + iconCls: 'x-deluge-connections', + menu: new Ext.menu.Menu({ + items: [{ + text: _('50') + }, { + text: _('100') + }, { + text: _('200') + }, { + text: _('300') + }, { + text: _('500') + },{ + text: _('Unlimited') + }] + }) + }, { + text: _('Upload Slot Limit'), + iconCls: 'icon-upload-slots', + menu: new Ext.menu.Menu({ + items: [{ + text: _('0') + }, { + text: _('1') + }, { + text: _('2') + }, { + text: _('3') + }, { + text: _('5') + },{ + text: _('Unlimited') + }] + }) + }, { + id: 'auto_managed', + text: _('Auto Managed'), + checked: false + }] + }) + }, '-', { + text: _('Queue'), + iconCls: 'icon-queue', + menu: new Ext.menu.Menu({ + items: [{ + torrentAction: 'top', + text: _('Top'), + iconCls: 'icon-top', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + },{ + torrentAction: 'up', + text: _('Up'), + iconCls: 'icon-up', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + },{ + torrentAction: 'down', + text: _('Down'), + iconCls: 'icon-down', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + },{ + torrentAction: 'bottom', + text: _('Bottom'), + iconCls: 'icon-bottom', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }] + }) + }, '-', { + torrentAction: 'update', + text: _('Update Tracker'), + iconCls: 'icon-update-tracker', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }, { + torrentAction: 'edit_trackers', + text: _('Edit Trackers'), + iconCls: 'icon-edit-trackers', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }, '-', { + torrentAction: 'remove', + text: _('Remove Torrent'), + iconCls: 'icon-remove', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }, '-', { + torrentAction: 'recheck', + text: _('Force Recheck'), + iconCls: 'icon-recheck', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }, { + torrentAction: 'move', + text: _('Move Storage'), + iconCls: 'icon-move', + handler: deluge.menus.onTorrentAction, + scope: deluge.menus + }] }); deluge.menus.filePriorities = new Ext.menu.Menu({ - id: 'filePrioritiesMenu', - items: [{ - id: 'expandAll', - text: _('Expand All'), - iconCls: 'icon-expand-all' - }, '-', { - id: 'no_download', - text: _('Do Not Download'), - iconCls: 'icon-do-not-download', - filePriority: 0 - }, { - id: 'normal', - text: _('Normal Priority'), - iconCls: 'icon-normal', - filePriority: 1 - }, { - id: 'high', - text: _('High Priority'), - iconCls: 'icon-high', - filePriority: 2 - }, { - id: 'highest', - text: _('Highest Priority'), - iconCls: 'icon-highest', - filePriority: 5 - }] + id: 'filePrioritiesMenu', + items: [{ + id: 'expandAll', + text: _('Expand All'), + iconCls: 'icon-expand-all' + }, '-', { + id: 'no_download', + text: _('Do Not Download'), + iconCls: 'icon-do-not-download', + filePriority: 0 + }, { + id: 'normal', + text: _('Normal Priority'), + iconCls: 'icon-normal', + filePriority: 1 + }, { + id: 'high', + text: _('High Priority'), + iconCls: 'icon-high', + filePriority: 2 + }, { + id: 'highest', + text: _('Highest Priority'), + iconCls: 'icon-highest', + filePriority: 5 + }] }); diff --git a/deluge/ui/web/js/deluge-all/MoveStorage.js b/deluge/ui/web/js/deluge-all/MoveStorage.js index 67c5f6b6c..f9342df38 100644 --- a/deluge/ui/web/js/deluge-all/MoveStorage.js +++ b/deluge/ui/web/js/deluge-all/MoveStorage.js @@ -32,73 +32,73 @@ Ext.namespace('Deluge'); Deluge.MoveStorage = Ext.extend(Ext.Window, { - - constructor: function(config) { - config = Ext.apply({ - title: _('Move Storage'), - width: 375, - height: 110, - layout: 'fit', - buttonAlign: 'right', - closeAction: 'hide', - closable: true, - iconCls: 'x-deluge-move-storage', - plain: true, - resizable: false - }, config); - Deluge.MoveStorage.superclass.constructor.call(this, config); - }, + + constructor: function(config) { + config = Ext.apply({ + title: _('Move Storage'), + width: 375, + height: 110, + layout: 'fit', + buttonAlign: 'right', + closeAction: 'hide', + closable: true, + iconCls: 'x-deluge-move-storage', + plain: true, + resizable: false + }, config); + Deluge.MoveStorage.superclass.constructor.call(this, config); + }, - initComponent: function() { - Deluge.MoveStorage.superclass.initComponent.call(this); + initComponent: function() { + Deluge.MoveStorage.superclass.initComponent.call(this); - this.addButton(_('Cancel'), this.onCancel, this); - this.addButton(_('Move'), this.onMove, this); + this.addButton(_('Cancel'), this.onCancel, this); + this.addButton(_('Move'), this.onMove, this); - this.form = this.add({ - xtype: 'form', - border: false, - defaultType: 'textfield', - width: 300, - bodyStyle: 'padding: 5px' - }); + this.form = this.add({ + xtype: 'form', + border: false, + defaultType: 'textfield', + width: 300, + bodyStyle: 'padding: 5px' + }); - this.moveLocation = this.form.add({ - fieldLabel: _('Location'), - name: 'location', - width: 240 - }); - //this.form.add({ - // xtype: 'button', - // text: _('Browse'), - // handler: function() { - // if (!this.fileBrowser) { - // this.fileBrowser = new Deluge.FileBrowser(); - // } - // this.fileBrowser.show(); - // }, - // scope: this - //}); - }, + this.moveLocation = this.form.add({ + fieldLabel: _('Location'), + name: 'location', + width: 240 + }); + //this.form.add({ + // xtype: 'button', + // text: _('Browse'), + // handler: function() { + // if (!this.fileBrowser) { + // this.fileBrowser = new Deluge.FileBrowser(); + // } + // this.fileBrowser.show(); + // }, + // scope: this + //}); + }, - hide: function() { - Deluge.MoveStorage.superclass.hide.call(this); - this.torrentIds = null; - }, + hide: function() { + Deluge.MoveStorage.superclass.hide.call(this); + this.torrentIds = null; + }, - show: function(torrentIds) { - Deluge.MoveStorage.superclass.show.call(this); - this.torrentIds = torrentIds; - }, + show: function(torrentIds) { + Deluge.MoveStorage.superclass.show.call(this); + this.torrentIds = torrentIds; + }, - onCancel: function() { - this.hide(); - }, + onCancel: function() { + this.hide(); + }, - onMove: function() { - var dest = this.moveLocation.getValue(); - deluge.client.core.move_storage(this.torrentIds, dest); - this.hide(); - } + onMove: function() { + var dest = this.moveLocation.getValue(); + deluge.client.core.move_storage(this.torrentIds, dest); + this.hide(); + } }); deluge.moveStorage = new Deluge.MoveStorage(); diff --git a/deluge/ui/web/js/deluge-all/MultiOptionsManager.js b/deluge/ui/web/js/deluge-all/MultiOptionsManager.js index 4d2a23c3f..fac4c666f 100644 --- a/deluge/ui/web/js/deluge-all/MultiOptionsManager.js +++ b/deluge/ui/web/js/deluge-all/MultiOptionsManager.js @@ -38,177 +38,177 @@ */ Deluge.MultiOptionsManager = Ext.extend(Deluge.OptionsManager, { - constructor: function(config) { - this.currentId = null; - this.stored = {}; - Deluge.MultiOptionsManager.superclass.constructor.call(this, config); - }, - - /** - * Changes bound fields to use the specified id. - * @param {String} id - */ - changeId: function(id, dontUpdateBinds) { - var oldId = this.currentId; - this.currentId = id; - if (!dontUpdateBinds) { - for (var option in this.options) { - if (!this.binds[option]) continue; - Ext.each(this.binds[option], function(bind) { - bind.setValue(this.get(option)); - }, this); - } - } - return oldId; - }, + constructor: function(config) { + this.currentId = null; + this.stored = {}; + Deluge.MultiOptionsManager.superclass.constructor.call(this, config); + }, + + /** + * Changes bound fields to use the specified id. + * @param {String} id + */ + changeId: function(id, dontUpdateBinds) { + var oldId = this.currentId; + this.currentId = id; + if (!dontUpdateBinds) { + for (var option in this.options) { + if (!this.binds[option]) continue; + Ext.each(this.binds[option], function(bind) { + bind.setValue(this.get(option)); + }, this); + } + } + return oldId; + }, - /** - * Changes all the changed values to be the default values - * @param {String} id - */ - commit: function() { - this.stored[this.currentId] = Ext.apply(this.stored[this.currentId], this.changed[this.currentId]); - this.reset(); - }, - - /** - * Get the value for an option - * @param {String/Array} option A single option or an array of options to return. - * @returns {Object} the options value. - */ - get: function() { - if (arguments.length == 1) { - var option = arguments[0]; - return (this.isDirty(option)) ? this.changed[this.currentId][option] : this.getDefault(option); - } else if (arguments.length == 0) { - var options = {}; - for (var option in this.options) { - options[option] = (this.isDirty(option)) ? this.changed[this.currentId][option] : this.getDefault(option); - } - return options; - } else { - var options = {}; - Ext.each(arguments, function(option) { - options[option] = (this.isDirty(option)) ? this.changed[this.currentId][option] : this.getDefault(option); - }, this); - return options; - } - }, + /** + * Changes all the changed values to be the default values + * @param {String} id + */ + commit: function() { + this.stored[this.currentId] = Ext.apply(this.stored[this.currentId], this.changed[this.currentId]); + this.reset(); + }, + + /** + * Get the value for an option + * @param {String/Array} option A single option or an array of options to return. + * @returns {Object} the options value. + */ + get: function() { + if (arguments.length == 1) { + var option = arguments[0]; + return (this.isDirty(option)) ? this.changed[this.currentId][option] : this.getDefault(option); + } else if (arguments.length == 0) { + var options = {}; + for (var option in this.options) { + options[option] = (this.isDirty(option)) ? this.changed[this.currentId][option] : this.getDefault(option); + } + return options; + } else { + var options = {}; + Ext.each(arguments, function(option) { + options[option] = (this.isDirty(option)) ? this.changed[this.currentId][option] : this.getDefault(option); + }, this); + return options; + } + }, - /** - * Get the default value for an option. - * @param {String} option A single option. - * @returns {Object} the value of the option - */ - getDefault: function(option) { - return (this.has(option)) ? this.stored[this.currentId][option] : this.options[option]; - }, + /** + * Get the default value for an option. + * @param {String} option A single option. + * @returns {Object} the value of the option + */ + getDefault: function(option) { + return (this.has(option)) ? this.stored[this.currentId][option] : this.options[option]; + }, - /** - * Returns the dirty (changed) values. - * @returns {Object} the changed options - */ - getDirty: function() { - return (this.changed[this.currentId]) ? this.changed[this.currentId] : {}; - }, + /** + * Returns the dirty (changed) values. + * @returns {Object} the changed options + */ + getDirty: function() { + return (this.changed[this.currentId]) ? this.changed[this.currentId] : {}; + }, - /** - * Check to see if the option has been changed. - * @param {String} option - * @returns {Boolean} true if the option has been changed, else false. - */ - isDirty: function(option) { - return (this.changed[this.currentId] && !Ext.isEmpty(this.changed[this.currentId][option])); - }, + /** + * Check to see if the option has been changed. + * @param {String} option + * @returns {Boolean} true if the option has been changed, else false. + */ + isDirty: function(option) { + return (this.changed[this.currentId] && !Ext.isEmpty(this.changed[this.currentId][option])); + }, - /** - * Check to see if an id has had an option set to something other than the - * default value. - * @param {String} option - * @returns {Boolean} true if the id has an option, else false. - */ - has: function(option) { - return (this.stored[this.currentId] && !Ext.isEmpty(this.stored[this.currentId][option])); - }, + /** + * Check to see if an id has had an option set to something other than the + * default value. + * @param {String} option + * @returns {Boolean} true if the id has an option, else false. + */ + has: function(option) { + return (this.stored[this.currentId] && !Ext.isEmpty(this.stored[this.currentId][option])); + }, - /** - * Reset the options back to the default values for the specified id. - */ - reset: function() { - if (this.changed[this.currentId]) delete this.changed[this.currentId]; - if (this.stored[this.currentId]) delete this.stored[this.currentId]; - }, + /** + * Reset the options back to the default values for the specified id. + */ + reset: function() { + if (this.changed[this.currentId]) delete this.changed[this.currentId]; + if (this.stored[this.currentId]) delete this.stored[this.currentId]; + }, - /** - * Reset the options back to their defaults for all ids. - */ - resetAll: function() { - this.changed = {}; - this.stored = {}; - this.changeId(null); - }, + /** + * Reset the options back to their defaults for all ids. + */ + resetAll: function() { + this.changed = {}; + this.stored = {}; + this.changeId(null); + }, - /** - * Sets the value of specified option for the passed in id. - * @param {String} id - * @param {String} option - * @param {Object} value The value for the option - */ - setDefault: function(option, value) { - if (option === undefined) { - return; - } else if (value === undefined) { - for (var key in option) { - this.setDefault(key, option[key]); - } - } else { - var oldValue = this.getDefault(option); - value = this.convertValueType(oldValue, value); - - // If the value is the same as the old value there is - // no point in setting it again. - if (oldValue == value) return; - - // Store the new default - if (!this.stored[this.currentId]) this.stored[this.currentId] = {}; - this.stored[this.currentId][option] = value; - - if (!this.isDirty(option)) { - this.fireEvent('changed', option, value, oldValue); - } - } - }, + /** + * Sets the value of specified option for the passed in id. + * @param {String} id + * @param {String} option + * @param {Object} value The value for the option + */ + setDefault: function(option, value) { + if (option === undefined) { + return; + } else if (value === undefined) { + for (var key in option) { + this.setDefault(key, option[key]); + } + } else { + var oldValue = this.getDefault(option); + value = this.convertValueType(oldValue, value); + + // If the value is the same as the old value there is + // no point in setting it again. + if (oldValue == value) return; + + // Store the new default + if (!this.stored[this.currentId]) this.stored[this.currentId] = {}; + this.stored[this.currentId][option] = value; + + if (!this.isDirty(option)) { + this.fireEvent('changed', option, value, oldValue); + } + } + }, - /** - * Update the value for the specified option and id. - * @param {String} id - * @param {String/Object} option or options to update - * @param {Object} [value]; - */ - update: function(option, value) { - if (option === undefined) { - return; - } else if (value === undefined) { - for (var key in option) { - this.update(key, option[key]); - } - } else { - if (!this.changed[this.currentId]) this.changed[this.currentId] = {}; + /** + * Update the value for the specified option and id. + * @param {String} id + * @param {String/Object} option or options to update + * @param {Object} [value]; + */ + update: function(option, value) { + if (option === undefined) { + return; + } else if (value === undefined) { + for (var key in option) { + this.update(key, option[key]); + } + } else { + if (!this.changed[this.currentId]) this.changed[this.currentId] = {}; - var defaultValue = this.getDefault(option); - value = this.convertValueType(defaultValue, value); - - var oldValue = this.get(option); - if (oldValue == value) return; + var defaultValue = this.getDefault(option); + value = this.convertValueType(defaultValue, value); + + var oldValue = this.get(option); + if (oldValue == value) return; - if (defaultValue == value) { - if (this.isDirty(option)) delete this.changed[this.currentId][option]; - this.fireEvent('changed', option, value, oldValue); - return; - } else { - this.changed[this.currentId][option] = value; - this.fireEvent('changed', option, value, oldValue); - } - } - } + if (defaultValue == value) { + if (this.isDirty(option)) delete this.changed[this.currentId][option]; + this.fireEvent('changed', option, value, oldValue); + return; + } else { + this.changed[this.currentId][option] = value; + this.fireEvent('changed', option, value, oldValue); + } + } + } }); diff --git a/deluge/ui/web/js/deluge-all/OptionsManager.js b/deluge/ui/web/js/deluge-all/OptionsManager.js index abbcbb54d..fedd439ef 100644 --- a/deluge/ui/web/js/deluge-all/OptionsManager.js +++ b/deluge/ui/web/js/deluge-all/OptionsManager.js @@ -40,250 +40,250 @@ Ext.namespace('Deluge'); * @param {Object} config Configuration options */ Deluge.OptionsManager = Ext.extend(Ext.util.Observable, { - - constructor: function(config) { - config = config || {}; - this.binds = {}; - this.changed = {}; - this.options = (config && config['options']) || {}; - this.focused = null; + + constructor: function(config) { + config = config || {}; + this.binds = {}; + this.changed = {}; + this.options = (config && config['options']) || {}; + this.focused = null; - this.addEvents({ - /** - * @event add - * Fires when an option is added - */ - 'add': true, + this.addEvents({ + /** + * @event add + * Fires when an option is added + */ + 'add': true, - /** - * @event changed - * Fires when an option is changed - * @param {String} option The changed option - * @param {Mixed} value The options new value - * @param {Mixed} oldValue The options old value - */ - 'changed': true, + /** + * @event changed + * Fires when an option is changed + * @param {String} option The changed option + * @param {Mixed} value The options new value + * @param {Mixed} oldValue The options old value + */ + 'changed': true, - /** - * @event reset - * Fires when the options are reset - */ - 'reset': true - }); - this.on('changed', this.onChange, this); + /** + * @event reset + * Fires when the options are reset + */ + 'reset': true + }); + this.on('changed', this.onChange, this); - Deluge.OptionsManager.superclass.constructor.call(this); - }, + Deluge.OptionsManager.superclass.constructor.call(this); + }, - /** - * Add a set of default options and values to the options manager - * @param {Object} options The default options. - */ - addOptions: function(options) { - this.options = Ext.applyIf(this.options, options); - }, - - /** - * Binds a form field to the specified option. - * @param {String} option - * @param {Ext.form.Field} field - */ - bind: function(option, field) { - this.binds[option] = this.binds[option] || []; - this.binds[option].push(field); - field._doption = option; + /** + * Add a set of default options and values to the options manager + * @param {Object} options The default options. + */ + addOptions: function(options) { + this.options = Ext.applyIf(this.options, options); + }, + + /** + * Binds a form field to the specified option. + * @param {String} option + * @param {Ext.form.Field} field + */ + bind: function(option, field) { + this.binds[option] = this.binds[option] || []; + this.binds[option].push(field); + field._doption = option; - field.on('focus', this.onFieldFocus, this); - field.on('blur', this.onFieldBlur, this); - field.on('change', this.onFieldChange, this); - field.on('check', this.onFieldChange, this); - field.on('spin', this.onFieldChange, this); - return field; - }, + field.on('focus', this.onFieldFocus, this); + field.on('blur', this.onFieldBlur, this); + field.on('change', this.onFieldChange, this); + field.on('check', this.onFieldChange, this); + field.on('spin', this.onFieldChange, this); + return field; + }, - /** - * Changes all the changed values to be the default values - */ - commit: function() { - this.options = Ext.apply(this.options, this.changed); - this.reset(); - }, + /** + * Changes all the changed values to be the default values + */ + commit: function() { + this.options = Ext.apply(this.options, this.changed); + this.reset(); + }, - /** - * Converts the value so it matches the originals type - * @param {Mixed} oldValue The original value - * @param {Mixed} value The new value to convert - */ - convertValueType: function(oldValue, value) { - if (Ext.type(oldValue) != Ext.type(value)) { - switch (Ext.type(oldValue)) { - case 'string': - value = String(value); - break; - case 'number': - value = Number(value); - break; - case 'boolean': - if (Ext.type(value) == 'string') { - value = value.toLowerCase(); - value = (value == 'true' || value == '1' || value == 'on') ? true : false; - } else { - value = Boolean(value); - } - break; - } - } - return value; - }, + /** + * Converts the value so it matches the originals type + * @param {Mixed} oldValue The original value + * @param {Mixed} value The new value to convert + */ + convertValueType: function(oldValue, value) { + if (Ext.type(oldValue) != Ext.type(value)) { + switch (Ext.type(oldValue)) { + case 'string': + value = String(value); + break; + case 'number': + value = Number(value); + break; + case 'boolean': + if (Ext.type(value) == 'string') { + value = value.toLowerCase(); + value = (value == 'true' || value == '1' || value == 'on') ? true : false; + } else { + value = Boolean(value); + } + break; + } + } + return value; + }, - /** - * Get the value for an option or options. - * @param {String} [option] A single option or an array of options to return. - * @returns {Object} the options value. - */ - get: function() { - if (arguments.length == 1) { - var option = arguments[0]; - return (this.isDirty(option)) ? this.changed[option] : this.options[option]; - } else { - var options = {}; - Ext.each(arguments, function(option) { - if (!this.has(option)) return; - options[option] = (this.isDirty(option)) ? this.changed[option] : this.options[option]; - }, this); - return options; - } - }, + /** + * Get the value for an option or options. + * @param {String} [option] A single option or an array of options to return. + * @returns {Object} the options value. + */ + get: function() { + if (arguments.length == 1) { + var option = arguments[0]; + return (this.isDirty(option)) ? this.changed[option] : this.options[option]; + } else { + var options = {}; + Ext.each(arguments, function(option) { + if (!this.has(option)) return; + options[option] = (this.isDirty(option)) ? this.changed[option] : this.options[option]; + }, this); + return options; + } + }, - /** - * Get the default value for an option or options. - * @param {String|Array} [option] A single option or an array of options to return. - * @returns {Object} the value of the option - */ - getDefault: function(option) { - return this.options[option]; - }, + /** + * Get the default value for an option or options. + * @param {String|Array} [option] A single option or an array of options to return. + * @returns {Object} the value of the option + */ + getDefault: function(option) { + return this.options[option]; + }, - /** - * Returns the dirty (changed) values. - * @returns {Object} the changed options - */ - getDirty: function() { - return this.changed; - }, + /** + * Returns the dirty (changed) values. + * @returns {Object} the changed options + */ + getDirty: function() { + return this.changed; + }, - /** - * @param {String} [option] The option to check - * @returns {Boolean} true if the option has been changed from the default. - */ - isDirty: function(option) { - return !Ext.isEmpty(this.changed[option]); - }, + /** + * @param {String} [option] The option to check + * @returns {Boolean} true if the option has been changed from the default. + */ + isDirty: function(option) { + return !Ext.isEmpty(this.changed[option]); + }, - /** - * Check to see if an option exists in the options manager - * @param {String} option - * @returns {Boolean} true if the option exists, else false. - */ - has: function(option) { - return (this.options[option]); - }, + /** + * Check to see if an option exists in the options manager + * @param {String} option + * @returns {Boolean} true if the option exists, else false. + */ + has: function(option) { + return (this.options[option]); + }, - /** - * Reset the options back to the default values. - */ - reset: function() { - this.changed = {}; - }, + /** + * Reset the options back to the default values. + */ + reset: function() { + this.changed = {}; + }, - /** - * Sets the value of specified option(s) for the passed in id. - * @param {String} option - * @param {Object} value The value for the option - */ - set: function(option, value) { - if (option === undefined) { - return; - } else if (typeof option == 'object') { - var options = option; - this.options = Ext.apply(this.options, options); - for (var option in options) { - this.onChange(option, options[option]); - } - } else { - this.options[option] = value; - this.onChange(option, value) - } - }, + /** + * Sets the value of specified option(s) for the passed in id. + * @param {String} option + * @param {Object} value The value for the option + */ + set: function(option, value) { + if (option === undefined) { + return; + } else if (typeof option == 'object') { + var options = option; + this.options = Ext.apply(this.options, options); + for (var option in options) { + this.onChange(option, options[option]); + } + } else { + this.options[option] = value; + this.onChange(option, value) + } + }, - /** - * Update the value for the specified option and id. - * @param {String/Object} option or options to update - * @param {Object} [value]; - */ - update: function(option, value) { - if (option === undefined) { - return; - } else if (value === undefined) { - for (var key in option) { - this.update(key, option[key]); - } - } else { - var defaultValue = this.getDefault(option); - value = this.convertValueType(defaultValue, value); + /** + * Update the value for the specified option and id. + * @param {String/Object} option or options to update + * @param {Object} [value]; + */ + update: function(option, value) { + if (option === undefined) { + return; + } else if (value === undefined) { + for (var key in option) { + this.update(key, option[key]); + } + } else { + var defaultValue = this.getDefault(option); + value = this.convertValueType(defaultValue, value); - var oldValue = this.get(option); - if (oldValue == value) return; + var oldValue = this.get(option); + if (oldValue == value) return; - if (defaultValue == value) { - if (this.isDirty(option)) delete this.changed[option]; - this.fireEvent('changed', option, value, oldValue); - return; - } + if (defaultValue == value) { + if (this.isDirty(option)) delete this.changed[option]; + this.fireEvent('changed', option, value, oldValue); + return; + } - this.changed[option] = value; - this.fireEvent('changed', option, value, oldValue); - } - }, + this.changed[option] = value; + this.fireEvent('changed', option, value, oldValue); + } + }, - /** - * Lets the option manager know when a field is blurred so if a value - * so value changing operations can continue on that field. - */ - onFieldBlur: function(field, event) { - if (this.focused == field) { - this.focused = null; - } - }, + /** + * Lets the option manager know when a field is blurred so if a value + * so value changing operations can continue on that field. + */ + onFieldBlur: function(field, event) { + if (this.focused == field) { + this.focused = null; + } + }, - /** - * Stops a form fields value from being blocked by the change functions - * @param {Ext.form.Field} field - * @private - */ - onFieldChange: function(field, event) { - if (field.field) field = field.field // fix for spinners - this.update(field._doption, field.getValue()); - }, + /** + * Stops a form fields value from being blocked by the change functions + * @param {Ext.form.Field} field + * @private + */ + onFieldChange: function(field, event) { + if (field.field) field = field.field // fix for spinners + this.update(field._doption, field.getValue()); + }, - /** - * Lets the option manager know when a field is focused so if a value - * changing operation is performed it won't change the value of the - * field. - */ - onFieldFocus: function(field, event) { - this.focused = field; - }, + /** + * Lets the option manager know when a field is focused so if a value + * changing operation is performed it won't change the value of the + * field. + */ + onFieldFocus: function(field, event) { + this.focused = field; + }, - onChange: function(option, newValue, oldValue) { - // If we don't have a bind there's nothing to do. - if (Ext.isEmpty(this.binds[option])) return; - Ext.each(this.binds[option], function(bind) { - // The field is currently focused so we don't want to - // change it. - if (bind == this.focused) return; - // Set the form field to the new value. - bind.setValue(newValue); - }, this); - } + onChange: function(option, newValue, oldValue) { + // If we don't have a bind there's nothing to do. + if (Ext.isEmpty(this.binds[option])) return; + Ext.each(this.binds[option], function(bind) { + // The field is currently focused so we don't want to + // change it. + if (bind == this.focused) return; + // Set the form field to the new value. + bind.setValue(newValue); + }, this); + } }); diff --git a/deluge/ui/web/js/deluge-all/OtherLimitWindow.js b/deluge/ui/web/js/deluge-all/OtherLimitWindow.js index aee1b4246..28c647d9a 100644 --- a/deluge/ui/web/js/deluge-all/OtherLimitWindow.js +++ b/deluge/ui/web/js/deluge-all/OtherLimitWindow.js @@ -36,65 +36,65 @@ Ext.ns('Deluge'); * @extends Ext.Window */ Deluge.OtherLimitWindow = Ext.extend(Ext.Window, { - - layout: 'fit', - width: 210, - height: 100, + + layout: 'fit', + width: 210, + height: 100, - closeAction: 'hide', + closeAction: 'hide', - initComponent: function() { - Deluge.OtherLimitWindow.superclass.initComponent.call(this); - this.form = this.add({ - xtype: 'form', - baseCls: 'x-plain', - bodyStyle: 'padding: 5px', - layout: 'hbox', - layoutConfig: { - pack: 'start' - }, - items: [{ - xtype: 'spinnerfield', - name: 'limit' - }] - }); - if (this.initialConfig.unit) { - this.form.add({ - border: false, - baseCls: 'x-plain', - bodyStyle: 'padding: 5px', - html: this.initialConfig.unit - }); - } else { - this.setSize(180, 100); - } + initComponent: function() { + Deluge.OtherLimitWindow.superclass.initComponent.call(this); + this.form = this.add({ + xtype: 'form', + baseCls: 'x-plain', + bodyStyle: 'padding: 5px', + layout: 'hbox', + layoutConfig: { + pack: 'start' + }, + items: [{ + xtype: 'spinnerfield', + name: 'limit' + }] + }); + if (this.initialConfig.unit) { + this.form.add({ + border: false, + baseCls: 'x-plain', + bodyStyle: 'padding: 5px', + html: this.initialConfig.unit + }); + } else { + this.setSize(180, 100); + } - this.addButton(_('Cancel'), this.onCancelClick, this); - this.addButton(_('Ok'), this.onOkClick, this); - this.afterMethod('show', this.doFocusField, this); - }, + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Ok'), this.onOkClick, this); + this.afterMethod('show', this.doFocusField, this); + }, - setValue: function(value) { - this.form.getForm().setValues({limit: value}); - }, + setValue: function(value) { + this.form.getForm().setValues({limit: value}); + }, - onCancelClick: function() { - this.form.getForm().reset(); - this.hide(); - }, + onCancelClick: function() { + this.form.getForm().reset(); + this.hide(); + }, - onOkClick: function() { - var config = {}; - config[this.group] = this.form.getForm().getValues().limit; - deluge.client.core.set_config(config, { - success: function() { - deluge.ui.update(); - } - }); - this.hide(); - }, + onOkClick: function() { + var config = {}; + config[this.group] = this.form.getForm().getValues().limit; + deluge.client.core.set_config(config, { + success: function() { + deluge.ui.update(); + } + }); + this.hide(); + }, - doFocusField: function() { - this.form.getForm().findField('limit').focus(true, 10); - } + doFocusField: function() { + this.form.getForm().findField('limit').focus(true, 10); + } }); diff --git a/deluge/ui/web/js/deluge-all/Plugin.js b/deluge/ui/web/js/deluge-all/Plugin.js index 40468e35c..2453abe05 100644 --- a/deluge/ui/web/js/deluge-all/Plugin.js +++ b/deluge/ui/web/js/deluge-all/Plugin.js @@ -37,91 +37,91 @@ Ext.ns('Deluge'); */ Deluge.Plugin = Ext.extend(Ext.util.Observable, { - /** - * The plugins name - * @property name - * @type {String} - */ - name: null, + /** + * The plugins name + * @property name + * @type {String} + */ + name: null, - constructor: function(config) { - this.isDelugePlugin = true; - this.addEvents({ - /** - * @event enabled - * @param {Plugin} plugin the plugin instance - */ - "enabled": true, + constructor: function(config) { + this.isDelugePlugin = true; + this.addEvents({ + /** + * @event enabled + * @param {Plugin} plugin the plugin instance + */ + "enabled": true, - /** - * @event disabled - * @param {Plugin} plugin the plugin instance - */ - "disabled": true - }); - Deluge.Plugin.superclass.constructor.call(this, config); - }, - - /** - * Disables the plugin, firing the "{@link #disabled}" event and - * then executing the plugins clean up method onDisabled. - */ - disable: function() { - this.fireEvent("disabled", this); - if (this.onDisable) this.onDisable(); - }, - - /** - * Enables the plugin, firing the "{@link #enabled}" event and - * then executes the plugins setup method, onEnabled. - */ - enable: function() { - this.fireEvent("enable", this); - if (this.onEnable) this.onEnable(); - }, + /** + * @event disabled + * @param {Plugin} plugin the plugin instance + */ + "disabled": true + }); + Deluge.Plugin.superclass.constructor.call(this, config); + }, + + /** + * Disables the plugin, firing the "{@link #disabled}" event and + * then executing the plugins clean up method onDisabled. + */ + disable: function() { + this.fireEvent("disabled", this); + if (this.onDisable) this.onDisable(); + }, + + /** + * Enables the plugin, firing the "{@link #enabled}" event and + * then executes the plugins setup method, onEnabled. + */ + enable: function() { + this.fireEvent("enable", this); + if (this.onEnable) this.onEnable(); + }, - registerTorrentStatus: function(key, header, options) { - options = options || {}; - var cc = options.colCfg || {}, sc = options.storeCfg || {}; - sc = Ext.apply(sc, {name: key}); - deluge.torrents.meta.fields.push(sc); - deluge.torrents.getStore().reader.onMetaChange(deluge.torrents.meta); + registerTorrentStatus: function(key, header, options) { + options = options || {}; + var cc = options.colCfg || {}, sc = options.storeCfg || {}; + sc = Ext.apply(sc, {name: key}); + deluge.torrents.meta.fields.push(sc); + deluge.torrents.getStore().reader.onMetaChange(deluge.torrents.meta); - cc = Ext.apply(cc, { - header: header, - dataIndex: key - }); - var cols = deluge.torrents.columns.slice(0); - cols.push(cc); - deluge.torrents.colModel.setConfig(cols); - deluge.torrents.columns = cols; + cc = Ext.apply(cc, { + header: header, + dataIndex: key + }); + var cols = deluge.torrents.columns.slice(0); + cols.push(cc); + deluge.torrents.colModel.setConfig(cols); + deluge.torrents.columns = cols; - Deluge.Keys.Grid.push(key); - deluge.torrents.getView().refresh(true); - }, + Deluge.Keys.Grid.push(key); + deluge.torrents.getView().refresh(true); + }, - deregisterTorrentStatus: function(key) { - var fields = []; - Ext.each(deluge.torrents.meta.fields, function(field) { - if (field.name != key) fields.push(field); - }); - deluge.torrents.meta.fields = fields; - deluge.torrents.getStore().reader.onMetaChange(deluge.torrents.meta); + deregisterTorrentStatus: function(key) { + var fields = []; + Ext.each(deluge.torrents.meta.fields, function(field) { + if (field.name != key) fields.push(field); + }); + deluge.torrents.meta.fields = fields; + deluge.torrents.getStore().reader.onMetaChange(deluge.torrents.meta); - var cols = []; - Ext.each(deluge.torrents.columns, function(col) { - if (col.dataIndex != key) cols.push(col); - }); - deluge.torrents.colModel.setConfig(cols); - deluge.torrents.columns = cols; + var cols = []; + Ext.each(deluge.torrents.columns, function(col) { + if (col.dataIndex != key) cols.push(col); + }); + deluge.torrents.colModel.setConfig(cols); + deluge.torrents.columns = cols; - var keys = []; - Ext.each(Deluge.Keys.Grid, function(k) { - if (k == key) keys.push(k); - }); - Deluge.Keys.Grid = keys; - deluge.torrents.getView().refresh(true); - } + var keys = []; + Ext.each(Deluge.Keys.Grid, function(k) { + if (k == key) keys.push(k); + }); + Deluge.Keys.Grid = keys; + deluge.torrents.getView().refresh(true); + } }); Ext.ns('Deluge.plugins'); diff --git a/deluge/ui/web/js/deluge-all/RemoveWindow.js b/deluge/ui/web/js/deluge-all/RemoveWindow.js index 237ef0bf3..43e872dd3 100644 --- a/deluge/ui/web/js/deluge-all/RemoveWindow.js +++ b/deluge/ui/web/js/deluge-all/RemoveWindow.js @@ -39,7 +39,7 @@ Deluge.RemoveWindow = Ext.extend(Ext.Window, { title: _('Remove Torrent'), layout: 'fit', width: 350, - height: 100, + height: 100, buttonAlign: 'right', closeAction: 'hide', @@ -49,50 +49,50 @@ Deluge.RemoveWindow = Ext.extend(Ext.Window, { bodyStyle: 'padding: 5px; padding-left: 10px;', html: 'Are you sure you wish to remove the torrent (s)?', - - initComponent: function() { - Deluge.RemoveWindow.superclass.initComponent.call(this); - this.addButton(_('Cancel'), this.onCancel, this); - this.addButton(_('Remove With Data'), this.onRemoveData, this); - this.addButton(_('Remove Torrent'), this.onRemove, this); - }, - - remove: function(removeData) { - Ext.each(this.torrentIds, function(torrentId) { - deluge.client.core.remove_torrent(torrentId, removeData, { - success: function() { - this.onRemoved(torrentId); - }, - scope: this, - torrentId: torrentId - }); - }, this); - - }, - - show: function(ids) { - Deluge.RemoveWindow.superclass.show.call(this); - this.torrentIds = ids; - }, - - onCancel: function() { - this.hide(); - this.torrentIds = null; - }, - - onRemove: function() { - this.remove(false); - }, - - onRemoveData: function() { - this.remove(true); - }, - - onRemoved: function(torrentId) { - deluge.events.fire('torrentRemoved', torrentId); - this.hide(); - deluge.ui.update(); - } + + initComponent: function() { + Deluge.RemoveWindow.superclass.initComponent.call(this); + this.addButton(_('Cancel'), this.onCancel, this); + this.addButton(_('Remove With Data'), this.onRemoveData, this); + this.addButton(_('Remove Torrent'), this.onRemove, this); + }, + + remove: function(removeData) { + Ext.each(this.torrentIds, function(torrentId) { + deluge.client.core.remove_torrent(torrentId, removeData, { + success: function() { + this.onRemoved(torrentId); + }, + scope: this, + torrentId: torrentId + }); + }, this); + + }, + + show: function(ids) { + Deluge.RemoveWindow.superclass.show.call(this); + this.torrentIds = ids; + }, + + onCancel: function() { + this.hide(); + this.torrentIds = null; + }, + + onRemove: function() { + this.remove(false); + }, + + onRemoveData: function() { + this.remove(true); + }, + + onRemoved: function(torrentId) { + deluge.events.fire('torrentRemoved', torrentId); + this.hide(); + deluge.ui.update(); + } }); deluge.removeWindow = new Deluge.RemoveWindow(); diff --git a/deluge/ui/web/js/deluge-all/Sidebar.js b/deluge/ui/web/js/deluge-all/Sidebar.js index b8e8cbda6..aa9713860 100644 --- a/deluge/ui/web/js/deluge-all/Sidebar.js +++ b/deluge/ui/web/js/deluge-all/Sidebar.js @@ -41,122 +41,122 @@ */ Deluge.Sidebar = Ext.extend(Ext.Panel, { - // private - panels: {}, + // private + panels: {}, - // private - selected: null, + // private + selected: null, - constructor: function(config) { - config = Ext.apply({ - id: 'sidebar', - region: 'west', - cls: 'deluge-sidebar', - title: _('Filters'), - layout: 'accordion', - split: true, - width: 200, - minSize: 175, - collapsible: true, - margins: '5 0 0 5', - cmargins: '5 0 0 5' - }, config); - Deluge.Sidebar.superclass.constructor.call(this, config); - }, + constructor: function(config) { + config = Ext.apply({ + id: 'sidebar', + region: 'west', + cls: 'deluge-sidebar', + title: _('Filters'), + layout: 'accordion', + split: true, + width: 200, + minSize: 175, + collapsible: true, + margins: '5 0 0 5', + cmargins: '5 0 0 5' + }, config); + Deluge.Sidebar.superclass.constructor.call(this, config); + }, - // private - initComponent: function() { - Deluge.Sidebar.superclass.initComponent.call(this); - deluge.events.on("disconnect", this.onDisconnect, this); - }, + // private + initComponent: function() { + Deluge.Sidebar.superclass.initComponent.call(this); + deluge.events.on("disconnect", this.onDisconnect, this); + }, - createFilter: function(filter, states) { - var panel = new Deluge.FilterPanel({ - filter: filter - }); - panel.on('selectionchange', function(view, nodes) { - deluge.ui.update(); - }); - this.add(panel); - - this.doLayout(); - this.panels[filter] = panel; + createFilter: function(filter, states) { + var panel = new Deluge.FilterPanel({ + filter: filter + }); + panel.on('selectionchange', function(view, nodes) { + deluge.ui.update(); + }); + this.add(panel); + + this.doLayout(); + this.panels[filter] = panel; - panel.header.on('click', function(header) { - if (!deluge.config.sidebar_multiple_filters) { - deluge.ui.update(); - } - if (!panel.list.getSelectionCount()) { - panel.list.select(0); - } - }); - this.fireEvent('filtercreate', this, panel); + panel.header.on('click', function(header) { + if (!deluge.config.sidebar_multiple_filters) { + deluge.ui.update(); + } + if (!panel.list.getSelectionCount()) { + panel.list.select(0); + } + }); + this.fireEvent('filtercreate', this, panel); - panel.updateStates(states); - this.fireEvent('afterfiltercreate', this, panel); - }, + panel.updateStates(states); + this.fireEvent('afterfiltercreate', this, panel); + }, - getFilter: function(filter) { - return this.panels[filter]; - }, + getFilter: function(filter) { + return this.panels[filter]; + }, - getFilterStates: function() { - var states = {} + getFilterStates: function() { + var states = {} - if (deluge.config.sidebar_multiple_filters) { - // Grab the filters from each of the filter panels - this.items.each(function(panel) { - var state = panel.getState(); - if (state == null) return; - states[panel.filterType] = state; - }, this); - } else { - var panel = this.getLayout().activeItem; - if (panel) { - var state = panel.getState(); - if (!state == null) return; - states[panel.filterType] = state; - } - } + if (deluge.config.sidebar_multiple_filters) { + // Grab the filters from each of the filter panels + this.items.each(function(panel) { + var state = panel.getState(); + if (state == null) return; + states[panel.filterType] = state; + }, this); + } else { + var panel = this.getLayout().activeItem; + if (panel) { + var state = panel.getState(); + if (!state == null) return; + states[panel.filterType] = state; + } + } - return states; - }, + return states; + }, - hasFilter: function(filter) { - return (this.panels[filter]) ? true : false; - }, + hasFilter: function(filter) { + return (this.panels[filter]) ? true : false; + }, - // private - onDisconnect: function() { - for (var filter in this.panels) { - this.remove(this.panels[filter]); - } - this.panels = {}; - this.selected = null; - }, + // private + onDisconnect: function() { + for (var filter in this.panels) { + this.remove(this.panels[filter]); + } + this.panels = {}; + this.selected = null; + }, - onFilterSelect: function(selModel, rowIndex, record) { - deluge.ui.update(); - }, + onFilterSelect: function(selModel, rowIndex, record) { + deluge.ui.update(); + }, - update: function(filters) { - for (var filter in filters) { - var states = filters[filter]; - if (Ext.getKeys(this.panels).indexOf(filter) > -1) { - this.panels[filter].updateStates(states); - } else { - this.createFilter(filter, states); - } - } + update: function(filters) { + for (var filter in filters) { + var states = filters[filter]; + if (Ext.getKeys(this.panels).indexOf(filter) > -1) { + this.panels[filter].updateStates(states); + } else { + this.createFilter(filter, states); + } + } - // Perform a cleanup of fitlers that aren't enabled any more - Ext.each(Ext.keys(this.panels), function(filter) { - if (Ext.keys(filters).indexOf(filter) == -1) { - // We need to remove the panel - this.remove(this.panels[filter]); - this.doLayout(); - delete this.panels[filter]; - } - }, this); - } + // Perform a cleanup of fitlers that aren't enabled any more + Ext.each(Ext.keys(this.panels), function(filter) { + if (Ext.keys(filters).indexOf(filter) == -1) { + // We need to remove the panel + this.remove(this.panels[filter]); + this.doLayout(); + delete this.panels[filter]; + } + }, this); + } }); diff --git a/deluge/ui/web/js/deluge-all/Statusbar.js b/deluge/ui/web/js/deluge-all/Statusbar.js index ed122a61e..4f1c48f86 100644 --- a/deluge/ui/web/js/deluge-all/Statusbar.js +++ b/deluge/ui/web/js/deluge-all/Statusbar.js @@ -32,284 +32,284 @@ Ext.namespace('Deluge'); Deluge.Statusbar = Ext.extend(Ext.ux.StatusBar, { - constructor: function(config) { - config = Ext.apply({ - id: 'deluge-statusbar', - defaultIconCls: 'x-deluge-statusbar x-not-connected', - defaultText: _('Not Connected') - }, config); - Deluge.Statusbar.superclass.constructor.call(this, config); - }, - - initComponent: function() { - Deluge.Statusbar.superclass.initComponent.call(this); - - deluge.events.on('connect', this.onConnect, this); - deluge.events.on('disconnect', this.onDisconnect, this); - }, - - createButtons: function() { - this.buttons = this.add({ - id: 'statusbar-connections', - text: ' ', - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-connections', - tooltip: _('Connections'), - menu: new Deluge.StatusbarMenu({ - items: [{ - text: '50', - value: '50', - group: 'max_connections_global', - checked: false - },{ - text: '100', - value: '100', - group: 'max_connections_global', - checked: false - },{ - text: '200', - value: '200', - group: 'max_connections_global', - checked: false - },{ - text: '300', - value: '300', - group: 'max_connections_global', - checked: false - },{ - text: '500', - value: '500', - group: 'max_connections_global', - checked: false - },{ - text: _('Unlimited'), - value: '-1', - group: 'max_connections_global', - checked: false - },'-',{ - text: _('Other'), - value: 'other', - group: 'max_connections_global', - checked: false - }], - otherWin: { - title: _('Set Maximum Connections') - } - }) - }, '-', { - id: 'statusbar-downspeed', - text: ' ', - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-downloading', - tooltip: _('Download Speed'), - menu: new Deluge.StatusbarMenu({ - items: [{ - value: '5', - text: '5 KiB/s', - group: 'max_download_speed', - checked: false - },{ - value: '10', - text: '10 KiB/s', - group: 'max_download_speed', - checked: false - },{ - value: '30', - text: '30 KiB/s', - group: 'max_download_speed', - checked: false - },{ - value: '80', - text: '80 KiB/s', - group: 'max_download_speed', - checked: false - },{ - value: '300', - text: '300 KiB/s', - group: 'max_download_speed', - checked: false - },{ - value: '-1', - text: _('Unlimited'), - group: 'max_download_speed', - checked: false - },'-',{ - value: 'other', - text: _('Other'), - group: 'max_download_speed', - checked: false - }], - otherWin: { - title: _('Set Maximum Download Speed'), - unit: _('Kib/s') - } - }) - }, '-', { - id: 'statusbar-upspeed', - text: ' ', - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-seeding', - tooltip: _('Upload Speed'), - menu: new Deluge.StatusbarMenu({ - items: [{ - value: '5', - text: '5 KiB/s', - group: 'max_upload_speed', - checked: false - },{ - value: '10', - text: '10 KiB/s', - group: 'max_upload_speed', - checked: false - },{ - value: '30', - text: '30 KiB/s', - group: 'max_upload_speed', - checked: false - },{ - value: '80', - text: '80 KiB/s', - group: 'max_upload_speed', - checked: false - },{ - value: '300', - text: '300 KiB/s', - group: 'max_upload_speed', - checked: false - },{ - value: '-1', - text: _('Unlimited'), - group: 'max_upload_speed', - checked: false - },'-',{ - value: 'other', - text: _('Other'), - group: 'max_upload_speed', - checked: false - }], - otherWin: { - title: _('Set Maximum Upload Speed'), - unit: _('Kib/s') - } - }) - }, '-', { - id: 'statusbar-traffic', - text: ' ', - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-traffic', - tooltip: _('Protocol Traffic Download/Upload'), - handler: function() { - deluge.preferences.show(); - deluge.preferences.selectPage('Network'); - } - }, '-', { - id: 'statusbar-dht', - text: ' ', - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-dht', - tooltip: _('DHT Nodes') - }, '-', { - id: 'statusbar-freespace', - text: ' ', - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-freespace', - tooltip: _('Freespace in download location'), - handler: function() { - deluge.preferences.show(); - deluge.preferences.selectPage('Downloads'); - } - }); - this.created = true; - }, - - onConnect: function() { - this.setStatus({ - iconCls: 'x-connected', - text: '' - }); - if (!this.created) { - this.createButtons(); - } else { - Ext.each(this.buttons, function(item) { - item.show(); - item.enable(); - }); - } - this.doLayout(); - }, + constructor: function(config) { + config = Ext.apply({ + id: 'deluge-statusbar', + defaultIconCls: 'x-deluge-statusbar x-not-connected', + defaultText: _('Not Connected') + }, config); + Deluge.Statusbar.superclass.constructor.call(this, config); + }, + + initComponent: function() { + Deluge.Statusbar.superclass.initComponent.call(this); + + deluge.events.on('connect', this.onConnect, this); + deluge.events.on('disconnect', this.onDisconnect, this); + }, + + createButtons: function() { + this.buttons = this.add({ + id: 'statusbar-connections', + text: ' ', + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-connections', + tooltip: _('Connections'), + menu: new Deluge.StatusbarMenu({ + items: [{ + text: '50', + value: '50', + group: 'max_connections_global', + checked: false + },{ + text: '100', + value: '100', + group: 'max_connections_global', + checked: false + },{ + text: '200', + value: '200', + group: 'max_connections_global', + checked: false + },{ + text: '300', + value: '300', + group: 'max_connections_global', + checked: false + },{ + text: '500', + value: '500', + group: 'max_connections_global', + checked: false + },{ + text: _('Unlimited'), + value: '-1', + group: 'max_connections_global', + checked: false + },'-',{ + text: _('Other'), + value: 'other', + group: 'max_connections_global', + checked: false + }], + otherWin: { + title: _('Set Maximum Connections') + } + }) + }, '-', { + id: 'statusbar-downspeed', + text: ' ', + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-downloading', + tooltip: _('Download Speed'), + menu: new Deluge.StatusbarMenu({ + items: [{ + value: '5', + text: '5 KiB/s', + group: 'max_download_speed', + checked: false + },{ + value: '10', + text: '10 KiB/s', + group: 'max_download_speed', + checked: false + },{ + value: '30', + text: '30 KiB/s', + group: 'max_download_speed', + checked: false + },{ + value: '80', + text: '80 KiB/s', + group: 'max_download_speed', + checked: false + },{ + value: '300', + text: '300 KiB/s', + group: 'max_download_speed', + checked: false + },{ + value: '-1', + text: _('Unlimited'), + group: 'max_download_speed', + checked: false + },'-',{ + value: 'other', + text: _('Other'), + group: 'max_download_speed', + checked: false + }], + otherWin: { + title: _('Set Maximum Download Speed'), + unit: _('Kib/s') + } + }) + }, '-', { + id: 'statusbar-upspeed', + text: ' ', + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-seeding', + tooltip: _('Upload Speed'), + menu: new Deluge.StatusbarMenu({ + items: [{ + value: '5', + text: '5 KiB/s', + group: 'max_upload_speed', + checked: false + },{ + value: '10', + text: '10 KiB/s', + group: 'max_upload_speed', + checked: false + },{ + value: '30', + text: '30 KiB/s', + group: 'max_upload_speed', + checked: false + },{ + value: '80', + text: '80 KiB/s', + group: 'max_upload_speed', + checked: false + },{ + value: '300', + text: '300 KiB/s', + group: 'max_upload_speed', + checked: false + },{ + value: '-1', + text: _('Unlimited'), + group: 'max_upload_speed', + checked: false + },'-',{ + value: 'other', + text: _('Other'), + group: 'max_upload_speed', + checked: false + }], + otherWin: { + title: _('Set Maximum Upload Speed'), + unit: _('Kib/s') + } + }) + }, '-', { + id: 'statusbar-traffic', + text: ' ', + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-traffic', + tooltip: _('Protocol Traffic Download/Upload'), + handler: function() { + deluge.preferences.show(); + deluge.preferences.selectPage('Network'); + } + }, '-', { + id: 'statusbar-dht', + text: ' ', + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-dht', + tooltip: _('DHT Nodes') + }, '-', { + id: 'statusbar-freespace', + text: ' ', + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-freespace', + tooltip: _('Freespace in download location'), + handler: function() { + deluge.preferences.show(); + deluge.preferences.selectPage('Downloads'); + } + }); + this.created = true; + }, + + onConnect: function() { + this.setStatus({ + iconCls: 'x-connected', + text: '' + }); + if (!this.created) { + this.createButtons(); + } else { + Ext.each(this.buttons, function(item) { + item.show(); + item.enable(); + }); + } + this.doLayout(); + }, - onDisconnect: function() { - this.clearStatus({useDefaults:true}); - Ext.each(this.buttons, function(item) { - item.hide(); - item.disable(); - }); - this.doLayout(); - }, - - update: function(stats) { - if (!stats) return; - - function addSpeed(val) {return val + ' KiB/s'} - - var updateStat = function(name, config) { - var item = this.items.get('statusbar-' + name); - if (config.limit.value > 0) { - var value = (config.value.formatter) ? config.value.formatter(config.value.value, true) : config.value.value; - var limit = (config.limit.formatter) ? config.limit.formatter(config.limit.value, true) : config.limit.value; - var str = String.format(config.format, value, limit); - } else { - var str = (config.value.formatter) ? config.value.formatter(config.value.value, true) : config.value.value; - } - item.setText(str); + onDisconnect: function() { + this.clearStatus({useDefaults:true}); + Ext.each(this.buttons, function(item) { + item.hide(); + item.disable(); + }); + this.doLayout(); + }, + + update: function(stats) { + if (!stats) return; + + function addSpeed(val) {return val + ' KiB/s'} + + var updateStat = function(name, config) { + var item = this.items.get('statusbar-' + name); + if (config.limit.value > 0) { + var value = (config.value.formatter) ? config.value.formatter(config.value.value, true) : config.value.value; + var limit = (config.limit.formatter) ? config.limit.formatter(config.limit.value, true) : config.limit.value; + var str = String.format(config.format, value, limit); + } else { + var str = (config.value.formatter) ? config.value.formatter(config.value.value, true) : config.value.value; + } + item.setText(str); - if (!item.menu) return; - item.menu.setValue(config.limit.value); - }.createDelegate(this); - - updateStat('connections', { - value: {value: stats.num_connections}, - limit: {value: stats.max_num_connections}, - format: '{0} ({1})' - }); + if (!item.menu) return; + item.menu.setValue(config.limit.value); + }.createDelegate(this); + + updateStat('connections', { + value: {value: stats.num_connections}, + limit: {value: stats.max_num_connections}, + format: '{0} ({1})' + }); - updateStat('downspeed', { - value: { - value: stats.download_rate, - formatter: Deluge.Formatters.speed - }, - limit: { - value: stats.max_download, - formatter: addSpeed - }, - format: '{0} ({1})' - }); + updateStat('downspeed', { + value: { + value: stats.download_rate, + formatter: Deluge.Formatters.speed + }, + limit: { + value: stats.max_download, + formatter: addSpeed + }, + format: '{0} ({1})' + }); - updateStat('upspeed', { - value: { - value: stats.upload_rate, - formatter: Deluge.Formatters.speed - }, - limit: { - value: stats.max_upload, - formatter: addSpeed - }, - format: '{0} ({1})' - }); + updateStat('upspeed', { + value: { + value: stats.upload_rate, + formatter: Deluge.Formatters.speed + }, + limit: { + value: stats.max_upload, + formatter: addSpeed + }, + format: '{0} ({1})' + }); - updateStat('traffic', { - value: { - value: stats.download_protocol_rate, - formatter: Deluge.Formatters.speed - }, - limit: { - value: stats.upload_protocol_rate, - formatter: Deluge.Formatters.speed - }, - format: '{0}/{1}' - }); + updateStat('traffic', { + value: { + value: stats.download_protocol_rate, + formatter: Deluge.Formatters.speed + }, + limit: { + value: stats.upload_protocol_rate, + formatter: Deluge.Formatters.speed + }, + format: '{0}/{1}' + }); - this.items.get('statusbar-dht').setText(stats.dht_nodes); - this.items.get('statusbar-freespace').setText(fsize(stats.free_space)); - } + this.items.get('statusbar-dht').setText(stats.dht_nodes); + this.items.get('statusbar-freespace').setText(fsize(stats.free_space)); + } }); diff --git a/deluge/ui/web/js/deluge-all/StatusbarMenu.js b/deluge/ui/web/js/deluge-all/StatusbarMenu.js index 78adf9bd1..939318ade 100644 --- a/deluge/ui/web/js/deluge-all/StatusbarMenu.js +++ b/deluge/ui/web/js/deluge-all/StatusbarMenu.js @@ -37,20 +37,20 @@ Ext.ns('Deluge'); * @extends Ext.menu.Menu */ Deluge.StatusbarMenu = Ext.extend(Ext.menu.Menu, { - - initComponent: function() { - Deluge.StatusbarMenu.superclass.initComponent.call(this); - this.otherWin = new Deluge.OtherLimitWindow(this.initialConfig.otherWin || {}); + + initComponent: function() { + Deluge.StatusbarMenu.superclass.initComponent.call(this); + this.otherWin = new Deluge.OtherLimitWindow(this.initialConfig.otherWin || {}); - this.items.each(function(item) { - if (item.getXType() != 'menucheckitem') return; - if (item.value == 'other') { - item.on('click', this.onOtherClicked, this); - } else { - item.on('checkchange', this.onLimitChanged, this); - } - }, this); - }, + this.items.each(function(item) { + if (item.getXType() != 'menucheckitem') return; + if (item.value == 'other') { + item.on('click', this.onOtherClicked, this); + } else { + item.on('checkchange', this.onLimitChanged, this); + } + }, this); + }, setValue: function(value) { var beenSet = false; @@ -81,20 +81,20 @@ Deluge.StatusbarMenu = Ext.extend(Ext.menu.Menu, { other.resumeEvents(); }, - onLimitChanged: function(item, checked) { - if (!checked || item.value == 'other') return; // we don't care about unchecks or other - var config = {} - config[item.group] = item.value - deluge.client.core.set_config(config, { - success: function() { - deluge.ui.update(); - } - }); - }, + onLimitChanged: function(item, checked) { + if (!checked || item.value == 'other') return; // we don't care about unchecks or other + var config = {} + config[item.group] = item.value + deluge.client.core.set_config(config, { + success: function() { + deluge.ui.update(); + } + }); + }, - onOtherClicked: function(item, e) { - this.otherWin.group = item.group; - this.otherWin.setValue(this.value); - this.otherWin.show(); - } + onOtherClicked: function(item, e) { + this.otherWin.group = item.group; + this.otherWin.setValue(this.value); + this.otherWin.show(); + } }); diff --git a/deluge/ui/web/js/deluge-all/Toolbar.js b/deluge/ui/web/js/deluge-all/Toolbar.js index 3558f28d2..a95f2a381 100644 --- a/deluge/ui/web/js/deluge-all/Toolbar.js +++ b/deluge/ui/web/js/deluge-all/Toolbar.js @@ -36,157 +36,157 @@ * @extends Ext.Toolbar */ Deluge.Toolbar = Ext.extend(Ext.Toolbar, { - constructor: function(config) { - config = Ext.apply({ - items: [ - { - id: 'create', - disabled: true, - text: _('Create'), - iconCls: 'icon-create', - handler: this.onTorrentAction - },{ - id: 'add', - disabled: true, - text: _('Add'), - iconCls: 'icon-add', - handler: this.onTorrentAdd - },{ - id: 'remove', - disabled: true, - text: _('Remove'), - iconCls: 'icon-remove', - handler: this.onTorrentAction - },'|',{ - id: 'pause', - disabled: true, - text: _('Pause'), - iconCls: 'icon-pause', - handler: this.onTorrentAction - },{ - id: 'resume', - disabled: true, - text: _('Resume'), - iconCls: 'icon-resume', - handler: this.onTorrentAction - },'|',{ - id: 'up', - cls: 'x-btn-text-icon', - disabled: true, - text: _('Up'), - iconCls: 'icon-up', - handler: this.onTorrentAction - },{ - id: 'down', - disabled: true, - text: _('Down'), - iconCls: 'icon-down', - handler: this.onTorrentAction - },'|',{ - id: 'preferences', - text: _('Preferences'), - iconCls: 'x-deluge-preferences', - handler: this.onPreferencesClick, - scope: this - },{ - id: 'connectionman', - text: _('Connection Manager'), - iconCls: 'x-deluge-connection-manager', - handler: this.onConnectionManagerClick, - scope: this - },'->',{ - id: 'help', - iconCls: 'icon-help', - text: _('Help'), - handler: this.onHelpClick, - scope: this - },{ - id: 'logout', - iconCls: 'icon-logout', - disabled: true, - text: _('Logout'), - handler: this.onLogout, - scope: this - } - ] - }, config); - Deluge.Toolbar.superclass.constructor.call(this, config); - }, + constructor: function(config) { + config = Ext.apply({ + items: [ + { + id: 'create', + disabled: true, + text: _('Create'), + iconCls: 'icon-create', + handler: this.onTorrentAction + },{ + id: 'add', + disabled: true, + text: _('Add'), + iconCls: 'icon-add', + handler: this.onTorrentAdd + },{ + id: 'remove', + disabled: true, + text: _('Remove'), + iconCls: 'icon-remove', + handler: this.onTorrentAction + },'|',{ + id: 'pause', + disabled: true, + text: _('Pause'), + iconCls: 'icon-pause', + handler: this.onTorrentAction + },{ + id: 'resume', + disabled: true, + text: _('Resume'), + iconCls: 'icon-resume', + handler: this.onTorrentAction + },'|',{ + id: 'up', + cls: 'x-btn-text-icon', + disabled: true, + text: _('Up'), + iconCls: 'icon-up', + handler: this.onTorrentAction + },{ + id: 'down', + disabled: true, + text: _('Down'), + iconCls: 'icon-down', + handler: this.onTorrentAction + },'|',{ + id: 'preferences', + text: _('Preferences'), + iconCls: 'x-deluge-preferences', + handler: this.onPreferencesClick, + scope: this + },{ + id: 'connectionman', + text: _('Connection Manager'), + iconCls: 'x-deluge-connection-manager', + handler: this.onConnectionManagerClick, + scope: this + },'->',{ + id: 'help', + iconCls: 'icon-help', + text: _('Help'), + handler: this.onHelpClick, + scope: this + },{ + id: 'logout', + iconCls: 'icon-logout', + disabled: true, + text: _('Logout'), + handler: this.onLogout, + scope: this + } + ] + }, config); + Deluge.Toolbar.superclass.constructor.call(this, config); + }, - connectedButtons: [ - 'add', 'remove', 'pause', 'resume', 'up', 'down' - ], - - initComponent: function() { - Deluge.Toolbar.superclass.initComponent.call(this); - deluge.events.on('connect', this.onConnect, this); - deluge.events.on('login', this.onLogin, this); - }, - - onConnect: function() { - Ext.each(this.connectedButtons, function(buttonId) { - this.items.get(buttonId).enable(); - }, this); - }, - - onDisconnect: function() { - Ext.each(this.connectedButtons, function(buttonId) { - this.items.get(buttonId).disable(); - }, this); - }, - - onLogin: function() { - this.items.get('logout').enable(); - }, - - onLogout: function() { - this.items.get('logout').disable(); - deluge.login.logout(); - }, - - onConnectionManagerClick: function() { - deluge.connectionManager.show(); - }, - - onHelpClick: function() { - window.open('http://dev.deluge-torrent.org/wiki/UserGuide'); - }, - - onPreferencesClick: function() { - deluge.preferences.show(); - }, - - onTorrentAction: function(item) { - var selection = deluge.torrents.getSelections(); - var ids = []; - Ext.each(selection, function(record) { - ids.push(record.id); - }); - - switch (item.id) { - case 'remove': - deluge.removeWindow.show(ids); - break; - case 'pause': - case 'resume': - deluge.client.core[item.id + '_torrent'](ids, { - success: function() { - deluge.ui.update(); - } - }); - break; - case 'up': - case 'down': - deluge.client.core['queue_' + item.id](ids, { - success: function() { - deluge.ui.update(); - } - }); - break; - } - }, - - onTorrentAdd: function() { - deluge.add.show(); - } + connectedButtons: [ + 'add', 'remove', 'pause', 'resume', 'up', 'down' + ], + + initComponent: function() { + Deluge.Toolbar.superclass.initComponent.call(this); + deluge.events.on('connect', this.onConnect, this); + deluge.events.on('login', this.onLogin, this); + }, + + onConnect: function() { + Ext.each(this.connectedButtons, function(buttonId) { + this.items.get(buttonId).enable(); + }, this); + }, + + onDisconnect: function() { + Ext.each(this.connectedButtons, function(buttonId) { + this.items.get(buttonId).disable(); + }, this); + }, + + onLogin: function() { + this.items.get('logout').enable(); + }, + + onLogout: function() { + this.items.get('logout').disable(); + deluge.login.logout(); + }, + + onConnectionManagerClick: function() { + deluge.connectionManager.show(); + }, + + onHelpClick: function() { + window.open('http://dev.deluge-torrent.org/wiki/UserGuide'); + }, + + onPreferencesClick: function() { + deluge.preferences.show(); + }, + + onTorrentAction: function(item) { + var selection = deluge.torrents.getSelections(); + var ids = []; + Ext.each(selection, function(record) { + ids.push(record.id); + }); + + switch (item.id) { + case 'remove': + deluge.removeWindow.show(ids); + break; + case 'pause': + case 'resume': + deluge.client.core[item.id + '_torrent'](ids, { + success: function() { + deluge.ui.update(); + } + }); + break; + case 'up': + case 'down': + deluge.client.core['queue_' + item.id](ids, { + success: function() { + deluge.ui.update(); + } + }); + break; + } + }, + + onTorrentAdd: function() { + deluge.add.show(); + } }); diff --git a/deluge/ui/web/js/deluge-all/TorrentGrid.js b/deluge/ui/web/js/deluge-all/TorrentGrid.js index 0455113c5..022aac7a3 100644 --- a/deluge/ui/web/js/deluge-all/TorrentGrid.js +++ b/deluge/ui/web/js/deluge-all/TorrentGrid.js @@ -1,6 +1,6 @@ /*! * Deluge.TorrentGrid.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -32,323 +32,347 @@ (function() { - /* Renderers for the Torrent Grid */ - function queueRenderer(value) { - return (value == -1) ? '' : value + 1; - } - function torrentNameRenderer(value, p, r) { - return String.format('
{1}
', r.data['state'].toLowerCase(), value); - } - function torrentSpeedRenderer(value) { - if (!value) return; - return fspeed(value); - } - function torrentProgressRenderer(value, p, r) { - value = new Number(value); - var progress = value; - var text = r.data['state'] + ' ' + value.toFixed(2) + '%'; - var width = new Number(this.style.match(/\w+:\s*(\d+)\w+/)[1]); - return Deluge.progressBar(value, width - 8, text); - } - function seedsRenderer(value, p, r) { - if (r.data['total_seeds'] > -1) { - return String.format('{0} ({1})', value, r.data['total_seeds']); - } else { - return value; - } - } - function peersRenderer(value, p, r) { - if (r.data['total_peers'] > -1) { - return String.format('{0} ({1})', value, r.data['total_peers']); - } else { - return value; - } - } - function availRenderer(value, p, r) { - return (value < 0) ? '∞' : new Number(value).toFixed(3); - } - function trackerRenderer(value, p, r) { - return String.format('
{0}
', value); - } - - function etaSorter(eta) { - return eta * -1; - } + /* Renderers for the Torrent Grid */ + function queueRenderer(value) { + return (value == -1) ? '' : value + 1; + } + function torrentNameRenderer(value, p, r) { + return String.format('
{1}
', r.data['state'].toLowerCase(), value); + } + function torrentSpeedRenderer(value) { + if (!value) return; + return fspeed(value); + } + function torrentProgressRenderer(value, p, r) { + value = new Number(value); + var progress = value; + var text = r.data['state'] + ' ' + value.toFixed(2) + '%'; + var width = new Number(this.style.match(/\w+:\s*(\d+)\w+/)[1]); + return Deluge.progressBar(value, width - 8, text); + } + function seedsRenderer(value, p, r) { + if (r.data['total_seeds'] > -1) { + return String.format('{0} ({1})', value, r.data['total_seeds']); + } else { + return value; + } + } + function peersRenderer(value, p, r) { + if (r.data['total_peers'] > -1) { + return String.format('{0} ({1})', value, r.data['total_peers']); + } else { + return value; + } + } + function availRenderer(value, p, r) { + return (value < 0) ? '∞' : new Number(value).toFixed(3); + } + function trackerRenderer(value, p, r) { + return String.format('
{0}
', value); + } - /** - * Deluge.TorrentGrid Class - * - * @author Damien Churchill - * @version 1.3 - * - * @class Deluge.TorrentGrid - * @extends Ext.grid.GridPanel - * @constructor - * @param {Object} config Configuration options - */ - Deluge.TorrentGrid = Ext.extend(Ext.grid.GridPanel, { + function etaSorter(eta) { + return eta * -1; + } - // object to store contained torrent ids - torrents: {}, + /** + * Deluge.TorrentGrid Class + * + * @author Damien Churchill + * @version 1.3 + * + * @class Deluge.TorrentGrid + * @extends Ext.grid.GridPanel + * @constructor + * @param {Object} config Configuration options + */ + Deluge.TorrentGrid = Ext.extend(Ext.grid.GridPanel, { - columns: [{ - id:'queue', - header: _('#'), - width: 30, - sortable: true, - renderer: queueRenderer, - dataIndex: 'queue' - }, { - id:'name', - header: _('Name'), - width: 150, - sortable: true, - renderer: torrentNameRenderer, - dataIndex: 'name' - }, { - header: _('Size'), - width: 75, - sortable: true, - renderer: fsize, - dataIndex: 'total_size' - }, { - header: _('Progress'), - width: 150, - sortable: true, - renderer: torrentProgressRenderer, - dataIndex: 'progress' - }, { - header: _('Seeders'), - width: 60, - sortable: true, - renderer: seedsRenderer, - dataIndex: 'num_seeds' - }, { - header: _('Peers'), - width: 60, - sortable: true, - renderer: peersRenderer, - dataIndex: 'num_peers' - }, { - header: _('Down Speed'), - width: 80, - sortable: true, - renderer: torrentSpeedRenderer, - dataIndex: 'download_payload_rate' - }, { - header: _('Up Speed'), - width: 80, - sortable: true, - renderer: torrentSpeedRenderer, - dataIndex: 'upload_payload_rate' - }, { - header: _('ETA'), - width: 60, - sortable: true, - renderer: ftime, - dataIndex: 'eta' - }, { - header: _('Ratio'), - width: 60, - sortable: true, - renderer: availRenderer, - dataIndex: 'ratio' - }, { - header: _('Avail'), - width: 60, - sortable: true, - renderer: availRenderer, - dataIndex: 'distributed_copies' - }, { - header: _('Added'), - width: 80, - sortable: true, - renderer: fdate, - dataIndex: 'time_added' - }, { - header: _('Tracker'), - width: 120, - sortable: true, - renderer: trackerRenderer, - dataIndex: 'tracker_host' - }, { - header: _('Save Path'), - width: 120, - sortable: true, - renderer: fplain, - dataIndex: 'save_path' - }], + // object to store contained torrent ids + torrents: {}, - meta: { - root: 'torrents', - idProperty: 'id', - fields: [ - {name: 'queue', sortType: Deluge.data.SortTypes.asQueuePosition}, - {name: 'name'}, - {name: 'total_size', type: 'int'}, - {name: 'state'}, - {name: 'progress', type: 'float'}, - {name: 'num_seeds', type: 'int'}, - {name: 'total_seeds', type: 'int'}, - {name: 'num_peers', type: 'int'}, - {name: 'total_peers', type: 'int'}, - {name: 'download_payload_rate', type: 'int'}, - {name: 'upload_payload_speed', type: 'int'}, - {name: 'eta', type: 'int', sortType: etaSorter}, - {name: 'ratio', type: 'float'}, - {name: 'distributed_copies', type: 'float'}, - {name: 'time_added', type: 'int'}, - {name: 'tracker_host'}, - {name: 'save_path'} - ] - }, + columns: [{ + id:'queue', + header: _('#'), + width: 30, + sortable: true, + renderer: queueRenderer, + dataIndex: 'queue' + }, { + id:'name', + header: _('Name'), + width: 150, + sortable: true, + renderer: torrentNameRenderer, + dataIndex: 'name' + }, { + header: _('Size'), + width: 75, + sortable: true, + renderer: fsize, + dataIndex: 'total_size' + }, { + header: _('Progress'), + width: 150, + sortable: true, + renderer: torrentProgressRenderer, + dataIndex: 'progress' + }, { + header: _('Seeders'), + width: 60, + sortable: true, + renderer: seedsRenderer, + dataIndex: 'num_seeds' + }, { + header: _('Peers'), + width: 60, + sortable: true, + renderer: peersRenderer, + dataIndex: 'num_peers' + }, { + header: _('Down Speed'), + width: 80, + sortable: true, + renderer: torrentSpeedRenderer, + dataIndex: 'download_payload_rate' + }, { + header: _('Up Speed'), + width: 80, + sortable: true, + renderer: torrentSpeedRenderer, + dataIndex: 'upload_payload_rate' + }, { + header: _('ETA'), + width: 60, + sortable: true, + renderer: ftime, + dataIndex: 'eta' + }, { + header: _('Ratio'), + width: 60, + sortable: true, + renderer: availRenderer, + dataIndex: 'ratio' + }, { + header: _('Avail'), + width: 60, + sortable: true, + renderer: availRenderer, + dataIndex: 'distributed_copies' + }, { + header: _('Added'), + width: 80, + sortable: true, + renderer: fdate, + dataIndex: 'time_added' + }, { + header: _('Last Seen Complete'), + width: 80, + sortable: true, + renderer: fdate, + dataIndex: 'last_seen_complete' + }, { + header: _('Tracker'), + width: 120, + sortable: true, + renderer: trackerRenderer, + dataIndex: 'tracker_host' + }, { + header: _('Save Path'), + width: 120, + sortable: true, + renderer: fplain, + dataIndex: 'save_path' + }, { + header: _('Owner'), + width: 80, + sortable: true, + renderer: fplain, + dataIndex: 'owner' + }, { + header: _('Public'), + width: 80, + sortable: true, + renderer: fplain, + dataIndex: 'public' + }, { + header: _('Shared'), + width: 80, + sortable: true, + renderer: fplain, + dataIndex: 'shared' + }], - constructor: function(config) { - config = Ext.apply({ - id: 'torrentGrid', - store: new Ext.data.JsonStore(this.meta), - columns: this.columns, - region: 'center', - cls: 'deluge-torrents', - stripeRows: true, - autoExpandColumn: 'name', - deferredRender:false, - autoScroll:true, - margins: '5 5 0 0', - stateful: true, - view: new Ext.ux.grid.BufferView({ - rowHeight: 26, - scrollDelay: false - }) - }, config); - Deluge.TorrentGrid.superclass.constructor.call(this, config); - }, + meta: { + root: 'torrents', + idProperty: 'id', + fields: [ + {name: 'queue', sortType: Deluge.data.SortTypes.asQueuePosition}, + {name: 'name'}, + {name: 'total_size', type: 'int'}, + {name: 'state'}, + {name: 'progress', type: 'float'}, + {name: 'num_seeds', type: 'int'}, + {name: 'total_seeds', type: 'int'}, + {name: 'num_peers', type: 'int'}, + {name: 'total_peers', type: 'int'}, + {name: 'download_payload_rate', type: 'int'}, + {name: 'upload_payload_speed', type: 'int'}, + {name: 'eta', type: 'int', sortType: etaSorter}, + {name: 'ratio', type: 'float'}, + {name: 'distributed_copies', type: 'float'}, + {name: 'time_added', type: 'int'}, + {name: 'tracker_host'}, + {name: 'save_path'} + ] + }, - initComponent: function() { - Deluge.TorrentGrid.superclass.initComponent.call(this); - deluge.events.on('torrentRemoved', this.onTorrentRemoved, this); - deluge.events.on('disconnect', this.onDisconnect, this); + constructor: function(config) { + config = Ext.apply({ + id: 'torrentGrid', + store: new Ext.data.JsonStore(this.meta), + columns: this.columns, + region: 'center', + cls: 'deluge-torrents', + stripeRows: true, + autoExpandColumn: 'name', + deferredRender:false, + autoScroll:true, + margins: '5 5 0 0', + stateful: true, + view: new Ext.ux.grid.BufferView({ + rowHeight: 26, + scrollDelay: false + }) + }, config); + Deluge.TorrentGrid.superclass.constructor.call(this, config); + }, - this.on('rowcontextmenu', function(grid, rowIndex, e) { - e.stopEvent(); - var selection = grid.getSelectionModel(); - if (!selection.hasSelection()) { - selection.selectRow(rowIndex); - } - deluge.menus.torrent.showAt(e.getPoint()); - }); - }, + initComponent: function() { + Deluge.TorrentGrid.superclass.initComponent.call(this); + deluge.events.on('torrentRemoved', this.onTorrentRemoved, this); + deluge.events.on('disconnect', this.onDisconnect, this); - /** - * Returns the record representing the torrent at the specified index. - * - * @param index {int} The row index of the torrent you wish to retrieve. - * @return {Ext.data.Record} The record representing the torrent. - */ - getTorrent: function(index) { - return this.getStore().getAt(index); - }, + this.on('rowcontextmenu', function(grid, rowIndex, e) { + e.stopEvent(); + var selection = grid.getSelectionModel(); + if (!selection.hasSelection()) { + selection.selectRow(rowIndex); + } + deluge.menus.torrent.showAt(e.getPoint()); + }); + }, - /** - * Returns the currently selected record. - * @ return {Array/Ext.data.Record} The record(s) representing the rows - */ - getSelected: function() { - return this.getSelectionModel().getSelected(); - }, + /** + * Returns the record representing the torrent at the specified index. + * + * @param index {int} The row index of the torrent you wish to retrieve. + * @return {Ext.data.Record} The record representing the torrent. + */ + getTorrent: function(index) { + return this.getStore().getAt(index); + }, - /** - * Returns the currently selected records. - */ - getSelections: function() { - return this.getSelectionModel().getSelections(); - }, + /** + * Returns the currently selected record. + * @ return {Array/Ext.data.Record} The record(s) representing the rows + */ + getSelected: function() { + return this.getSelectionModel().getSelected(); + }, - /** - * Return the currently selected torrent id. - * @return {String} The currently selected id. - */ - getSelectedId: function() { - return this.getSelectionModel().getSelected().id - }, + /** + * Returns the currently selected records. + */ + getSelections: function() { + return this.getSelectionModel().getSelections(); + }, - /** - * Return the currently selected torrent ids. - * @return {Array} The currently selected ids. - */ - getSelectedIds: function() { - var ids = []; - Ext.each(this.getSelectionModel().getSelections(), function(r) { - ids.push(r.id); - }); - return ids; - }, + /** + * Return the currently selected torrent id. + * @return {String} The currently selected id. + */ + getSelectedId: function() { + return this.getSelectionModel().getSelected().id + }, - update: function(torrents, wipe) { - var store = this.getStore(); + /** + * Return the currently selected torrent ids. + * @return {Array} The currently selected ids. + */ + getSelectedIds: function() { + var ids = []; + Ext.each(this.getSelectionModel().getSelections(), function(r) { + ids.push(r.id); + }); + return ids; + }, - // Need to perform a complete reload of the torrent grid. - if (wipe) { - store.removeAll(); - this.torrents = {}; - } + update: function(torrents, wipe) { + var store = this.getStore(); - var newTorrents = []; + // Need to perform a complete reload of the torrent grid. + if (wipe) { + store.removeAll(); + this.torrents = {}; + } - // Update and add any new torrents. - for (var t in torrents) { - var torrent = torrents[t]; + var newTorrents = []; - if (this.torrents[t]) { - var record = store.getById(t); - record.beginEdit(); - for (var k in torrent) { - if (record.get(k) != torrent[k]) { - record.set(k, torrent[k]); - } - } - record.endEdit(); - } else { - var record = new Deluge.data.Torrent(torrent); - record.id = t; - this.torrents[t] = 1; - newTorrents.push(record); - } - } - store.add(newTorrents); + // Update and add any new torrents. + for (var t in torrents) { + var torrent = torrents[t]; - // Remove any torrents that should not be in the store. - store.each(function(record) { - if (!torrents[record.id]) { - store.remove(record); - delete this.torrents[record.id]; - } - }, this); - store.commitChanges(); + if (this.torrents[t]) { + var record = store.getById(t); + record.beginEdit(); + for (var k in torrent) { + if (record.get(k) != torrent[k]) { + record.set(k, torrent[k]); + } + } + record.endEdit(); + } else { + var record = new Deluge.data.Torrent(torrent); + record.id = t; + this.torrents[t] = 1; + newTorrents.push(record); + } + } + store.add(newTorrents); - var sortState = store.getSortState() - if (!sortState) return; - store.sort(sortState.field, sortState.direction); - }, + // Remove any torrents that should not be in the store. + store.each(function(record) { + if (!torrents[record.id]) { + store.remove(record); + delete this.torrents[record.id]; + } + }, this); + store.commitChanges(); - // private - onDisconnect: function() { - this.getStore().removeAll(); - this.torrents = {}; - }, + var sortState = store.getSortState() + if (!sortState) return; + store.sort(sortState.field, sortState.direction); + }, - // private - onTorrentRemoved: function(torrentIds) { - var selModel = this.getSelectionModel(); - Ext.each(torrentIds, function(torrentId) { - var record = this.getStore().getById(torrentId); - if (selModel.isSelected(record)) { - selModel.deselectRow(this.getStore().indexOf(record)); - } - this.getStore().remove(record); - delete this.torrents[torrentId]; - }, this); - } + // private + onDisconnect: function() { + this.getStore().removeAll(); + this.torrents = {}; + }, + + // private + onTorrentRemoved: function(torrentIds) { + var selModel = this.getSelectionModel(); + Ext.each(torrentIds, function(torrentId) { + var record = this.getStore().getById(torrentId); + if (selModel.isSelected(record)) { + selModel.deselectRow(this.getStore().indexOf(record)); + } + this.getStore().remove(record); + delete this.torrents[torrentId]; + }, this); + } }); deluge.torrents = new Deluge.TorrentGrid(); })(); diff --git a/deluge/ui/web/js/deluge-all/UI.js b/deluge/ui/web/js/deluge-all/UI.js index 4a1b96278..c8e721fdd 100644 --- a/deluge/ui/web/js/deluge-all/UI.js +++ b/deluge/ui/web/js/deluge-all/UI.js @@ -1,6 +1,6 @@ /*! * Deluge.UI.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -38,234 +38,234 @@ */ deluge.ui = { - errorCount: 0, + errorCount: 0, - filters: null, + filters: null, - /** - * @description Create all the interface components, the json-rpc client - * and set up various events that the UI will utilise. - */ - initialize: function() { - deluge.add = new Deluge.add.AddWindow(); - deluge.details = new Deluge.details.DetailsPanel(); - deluge.connectionManager = new Deluge.ConnectionManager(); - deluge.editTrackers = new Deluge.EditTrackersWindow(); - deluge.login = new Deluge.LoginWindow(); - deluge.preferences = new Deluge.preferences.PreferencesWindow(); - deluge.sidebar = new Deluge.Sidebar(); - deluge.statusbar = new Deluge.Statusbar(); - deluge.toolbar = new Deluge.Toolbar(); + /** + * @description Create all the interface components, the json-rpc client + * and set up various events that the UI will utilise. + */ + initialize: function() { + deluge.add = new Deluge.add.AddWindow(); + deluge.details = new Deluge.details.DetailsPanel(); + deluge.connectionManager = new Deluge.ConnectionManager(); + deluge.editTrackers = new Deluge.EditTrackersWindow(); + deluge.login = new Deluge.LoginWindow(); + deluge.preferences = new Deluge.preferences.PreferencesWindow(); + deluge.sidebar = new Deluge.Sidebar(); + deluge.statusbar = new Deluge.Statusbar(); + deluge.toolbar = new Deluge.Toolbar(); - this.MainPanel = new Ext.Panel({ - id: 'mainPanel', - iconCls: 'x-deluge-main-panel', - title: 'Deluge', - layout: 'border', - tbar: deluge.toolbar, - items: [ - deluge.sidebar, - deluge.details, - deluge.torrents - ], - bbar: deluge.statusbar - }); + this.MainPanel = new Ext.Panel({ + id: 'mainPanel', + iconCls: 'x-deluge-main-panel', + title: 'Deluge', + layout: 'border', + tbar: deluge.toolbar, + items: [ + deluge.sidebar, + deluge.details, + deluge.torrents + ], + bbar: deluge.statusbar + }); - this.Viewport = new Ext.Viewport({ - layout: 'fit', - items: [this.MainPanel] - }); - - deluge.events.on("connect", this.onConnect, this); - deluge.events.on("disconnect", this.onDisconnect, this); - deluge.events.on('PluginDisabledEvent', this.onPluginDisabled, this); - deluge.events.on('PluginEnabledEvent', this.onPluginEnabled, this); - deluge.client = new Ext.ux.util.RpcClient({ - url: deluge.config.base + 'json' - }); - - // enable all the already active plugins - for (var plugin in Deluge.pluginStore) { - plugin = Deluge.createPlugin(plugin); - plugin.enable(); - deluge.plugins[plugin.name] = plugin; - } + this.Viewport = new Ext.Viewport({ + layout: 'fit', + items: [this.MainPanel] + }); - // Initialize quicktips so all the tooltip configs start working. - Ext.QuickTips.init(); - - deluge.client.on('connected', function(e) { - deluge.login.show(); - }, this, {single: true}); - - this.update = this.update.createDelegate(this); - this.checkConnection = this.checkConnection.createDelegate(this); + deluge.events.on("connect", this.onConnect, this); + deluge.events.on("disconnect", this.onDisconnect, this); + deluge.events.on('PluginDisabledEvent', this.onPluginDisabled, this); + deluge.events.on('PluginEnabledEvent', this.onPluginEnabled, this); + deluge.client = new Ext.ux.util.RpcClient({ + url: deluge.config.base + 'json' + }); - this.originalTitle = document.title; - }, + // enable all the already active plugins + for (var plugin in Deluge.pluginStore) { + plugin = Deluge.createPlugin(plugin); + plugin.enable(); + deluge.plugins[plugin.name] = plugin; + } - checkConnection: function() { - deluge.client.web.connected({ - success: this.onConnectionSuccess, - failure: this.onConnectionError, - scope: this - }); - }, + // Initialize quicktips so all the tooltip configs start working. + Ext.QuickTips.init(); - update: function() { - var filters = deluge.sidebar.getFilterStates(); - this.oldFilters = this.filters; - this.filters = filters; + deluge.client.on('connected', function(e) { + deluge.login.show(); + }, this, {single: true}); - deluge.client.web.update_ui(Deluge.Keys.Grid, filters, { - success: this.onUpdate, - failure: this.onUpdateError, - scope: this - }); - deluge.details.update(); - }, + this.update = this.update.createDelegate(this); + this.checkConnection = this.checkConnection.createDelegate(this); - onConnectionError: function(error) { - - }, + this.originalTitle = document.title; + }, - onConnectionSuccess: function(result) { - deluge.statusbar.setStatus({ - iconCls: 'x-deluge-statusbar icon-ok', - text: _('Connection restored') - }); - clearInterval(this.checking); - if (!result) { - deluge.connectionManager.show(); - } - }, + checkConnection: function() { + deluge.client.web.connected({ + success: this.onConnectionSuccess, + failure: this.onConnectionError, + scope: this + }); + }, - onUpdateError: function(error) { - if (this.errorCount == 2) { - Ext.MessageBox.show({ - title: 'Lost Connection', - msg: 'The connection to the webserver has been lost!', - buttons: Ext.MessageBox.OK, - icon: Ext.MessageBox.ERROR - }); - deluge.events.fire('disconnect'); - deluge.statusbar.setStatus({ - text: 'Lost connection to webserver'} - ); - this.checking = setInterval(this.checkConnection, 2000); - } - this.errorCount++; - }, + update: function() { + var filters = deluge.sidebar.getFilterStates(); + this.oldFilters = this.filters; + this.filters = filters; - /** - * @static - * @private - * Updates the various components in the interface. - */ - onUpdate: function(data) { - if (!data['connected']) { - deluge.connectionManager.disconnect(true); - return; - } + deluge.client.web.update_ui(Deluge.Keys.Grid, filters, { + success: this.onUpdate, + failure: this.onUpdateError, + scope: this + }); + deluge.details.update(); + }, - if (deluge.config.show_session_speed) { - document.title = this.originalTitle + - ' (Down: ' + fspeed(data['stats'].download_rate, true) + - ' Up: ' + fspeed(data['stats'].upload_rate, true) + ')'; - } - if (Ext.areObjectsEqual(this.filters, this.oldFilters)) { - deluge.torrents.update(data['torrents']); - } else { - deluge.torrents.update(data['torrents'], true); - } - deluge.statusbar.update(data['stats']); - deluge.sidebar.update(data['filters']); - this.errorCount = 0; - }, + onConnectionError: function(error) { - /** - * @static - * @private - * Start the Deluge UI polling the server and update the interface. - */ - onConnect: function() { - if (!this.running) { - this.running = setInterval(this.update, 2000); - this.update(); - } - deluge.client.web.get_plugins({ - success: this.onGotPlugins, - scope: this - }); - }, + }, - /** - * @static - * @private - */ - onDisconnect: function() { - this.stop(); - }, + onConnectionSuccess: function(result) { + deluge.statusbar.setStatus({ + iconCls: 'x-deluge-statusbar icon-ok', + text: _('Connection restored') + }); + clearInterval(this.checking); + if (!result) { + deluge.connectionManager.show(); + } + }, - onGotPlugins: function(plugins) { - Ext.each(plugins.enabled_plugins, function(plugin) { - if (deluge.plugins[plugin]) return; - deluge.client.web.get_plugin_resources(plugin, { - success: this.onGotPluginResources, - scope: this - }); - }, this); - }, + onUpdateError: function(error) { + if (this.errorCount == 2) { + Ext.MessageBox.show({ + title: 'Lost Connection', + msg: 'The connection to the webserver has been lost!', + buttons: Ext.MessageBox.OK, + icon: Ext.MessageBox.ERROR + }); + deluge.events.fire('disconnect'); + deluge.statusbar.setStatus({ + text: 'Lost connection to webserver'} + ); + this.checking = setInterval(this.checkConnection, 2000); + } + this.errorCount++; + }, - onPluginEnabled: function(pluginName) { - if (deluge.plugins[pluginName]) { - deluge.plugins[pluginName].enable(); - } else { - deluge.client.web.get_plugin_resources(pluginName, { - success: this.onGotPluginResources, - scope: this - }); - } - }, + /** + * @static + * @private + * Updates the various components in the interface. + */ + onUpdate: function(data) { + if (!data['connected']) { + deluge.connectionManager.disconnect(true); + return; + } - onGotPluginResources: function(resources) { - var scripts = (Deluge.debug) ? resources.debug_scripts : resources.scripts; - Ext.each(scripts, function(script) { - Ext.ux.JSLoader({ - url: script, - onLoad: this.onPluginLoaded, - pluginName: resources.name - }); - }, this); - }, + if (deluge.config.show_session_speed) { + document.title = this.originalTitle + + ' (Down: ' + fspeed(data['stats'].download_rate, true) + + ' Up: ' + fspeed(data['stats'].upload_rate, true) + ')'; + } + if (Ext.areObjectsEqual(this.filters, this.oldFilters)) { + deluge.torrents.update(data['torrents']); + } else { + deluge.torrents.update(data['torrents'], true); + } + deluge.statusbar.update(data['stats']); + deluge.sidebar.update(data['filters']); + this.errorCount = 0; + }, - onPluginDisabled: function(pluginName) { - deluge.plugins[pluginName].disable(); - }, + /** + * @static + * @private + * Start the Deluge UI polling the server and update the interface. + */ + onConnect: function() { + if (!this.running) { + this.running = setInterval(this.update, 2000); + this.update(); + } + deluge.client.web.get_plugins({ + success: this.onGotPlugins, + scope: this + }); + }, - onPluginLoaded: function(options) { - // This could happen if the plugin has multiple scripts - if (!Deluge.hasPlugin(options.pluginName)) return; + /** + * @static + * @private + */ + onDisconnect: function() { + this.stop(); + }, - // Enable the plugin - plugin = Deluge.createPlugin(options.pluginName); - plugin.enable(); - deluge.plugins[plugin.name] = plugin; - }, + onGotPlugins: function(plugins) { + Ext.each(plugins.enabled_plugins, function(plugin) { + if (deluge.plugins[plugin]) return; + deluge.client.web.get_plugin_resources(plugin, { + success: this.onGotPluginResources, + scope: this + }); + }, this); + }, - /** - * @static - * Stop the Deluge UI polling the server and clear the interface. - */ - stop: function() { - if (this.running) { - clearInterval(this.running); - this.running = false; - deluge.torrents.getStore().removeAll(); - } - } + onPluginEnabled: function(pluginName) { + if (deluge.plugins[pluginName]) { + deluge.plugins[pluginName].enable(); + } else { + deluge.client.web.get_plugin_resources(pluginName, { + success: this.onGotPluginResources, + scope: this + }); + } + }, + + onGotPluginResources: function(resources) { + var scripts = (Deluge.debug) ? resources.debug_scripts : resources.scripts; + Ext.each(scripts, function(script) { + Ext.ux.JSLoader({ + url: deluge.config.base + script, + onLoad: this.onPluginLoaded, + pluginName: resources.name + }); + }, this); + }, + + onPluginDisabled: function(pluginName) { + deluge.plugins[pluginName].disable(); + }, + + onPluginLoaded: function(options) { + // This could happen if the plugin has multiple scripts + if (!Deluge.hasPlugin(options.pluginName)) return; + + // Enable the plugin + plugin = Deluge.createPlugin(options.pluginName); + plugin.enable(); + deluge.plugins[plugin.name] = plugin; + }, + + /** + * @static + * Stop the Deluge UI polling the server and clear the interface. + */ + stop: function() { + if (this.running) { + clearInterval(this.running); + this.running = false; + deluge.torrents.getStore().removeAll(); + } + } } Ext.onReady(function(e) { - deluge.ui.initialize(); + deluge.ui.initialize(); }); diff --git a/deluge/ui/web/js/deluge-all/add/AddWindow.js b/deluge/ui/web/js/deluge-all/add/AddWindow.js index 4dc4edeb5..85c0da162 100644 --- a/deluge/ui/web/js/deluge-all/add/AddWindow.js +++ b/deluge/ui/web/js/deluge-all/add/AddWindow.js @@ -34,195 +34,195 @@ Ext.namespace('Deluge.add'); Deluge.add.AddWindow = Ext.extend(Deluge.add.Window, { - title: _('Add Torrents'), - layout: 'border', - width: 470, - height: 450, - bodyStyle: 'padding: 10px 5px;', - buttonAlign: 'right', - closeAction: 'hide', - closable: true, - plain: true, - iconCls: 'x-deluge-add-window-icon', + title: _('Add Torrents'), + layout: 'border', + width: 470, + height: 450, + bodyStyle: 'padding: 10px 5px;', + buttonAlign: 'right', + closeAction: 'hide', + closable: true, + plain: true, + iconCls: 'x-deluge-add-window-icon', - initComponent: function() { - Deluge.add.AddWindow.superclass.initComponent.call(this); + initComponent: function() { + Deluge.add.AddWindow.superclass.initComponent.call(this); - this.addButton(_('Cancel'), this.onCancelClick, this); - this.addButton(_('Add'), this.onAddClick, this); - - function torrentRenderer(value, p, r) { - if (r.data['info_hash']) { - return String.format('
{0}
', value); - } else { - return String.format('
{0}
', value); - } - } + this.addButton(_('Cancel'), this.onCancelClick, this); + this.addButton(_('Add'), this.onAddClick, this); + + function torrentRenderer(value, p, r) { + if (r.data['info_hash']) { + return String.format('
{0}
', value); + } else { + return String.format('
{0}
', value); + } + } - this.list = new Ext.list.ListView({ - store: new Ext.data.SimpleStore({ - fields: [ - {name: 'info_hash', mapping: 1}, - {name: 'text', mapping: 2} - ], - id: 0 - }), - columns: [{ - id: 'torrent', - width: 150, - sortable: true, - renderer: torrentRenderer, - dataIndex: 'text' - }], - stripeRows: true, - singleSelect: true, - listeners: { - 'selectionchange': { - fn: this.onSelect, - scope: this - } - }, - hideHeaders: true, - autoExpandColumn: 'torrent', - autoScroll: true - }); + this.list = new Ext.list.ListView({ + store: new Ext.data.SimpleStore({ + fields: [ + {name: 'info_hash', mapping: 1}, + {name: 'text', mapping: 2} + ], + id: 0 + }), + columns: [{ + id: 'torrent', + width: 150, + sortable: true, + renderer: torrentRenderer, + dataIndex: 'text' + }], + stripeRows: true, + singleSelect: true, + listeners: { + 'selectionchange': { + fn: this.onSelect, + scope: this + } + }, + hideHeaders: true, + autoExpandColumn: 'torrent', + autoScroll: true + }); - this.add({ - region: 'center', - items: [this.list], - margins: '5 5 5 5', - bbar: new Ext.Toolbar({ - items: [{ - iconCls: 'x-deluge-add-file', - text: _('File'), - handler: this.onFile, - scope: this - }, { - text: _('Url'), - iconCls: 'icon-add-url', - handler: this.onUrl, - scope: this - }, { - text: _('Infohash'), - iconCls: 'icon-add-magnet', - disabled: true - }, '->', { - text: _('Remove'), - iconCls: 'icon-remove', - handler: this.onRemove, - scope: this - }] - }) - }); - - this.optionsPanel = this.add(new Deluge.add.OptionsPanel()); - this.on('hide', this.onHide, this); - this.on('show', this.onShow, this); - }, + this.add({ + region: 'center', + items: [this.list], + margins: '5 5 5 5', + bbar: new Ext.Toolbar({ + items: [{ + iconCls: 'x-deluge-add-file', + text: _('File'), + handler: this.onFile, + scope: this + }, { + text: _('Url'), + iconCls: 'icon-add-url', + handler: this.onUrl, + scope: this + }, { + text: _('Infohash'), + iconCls: 'icon-add-magnet', + disabled: true + }, '->', { + text: _('Remove'), + iconCls: 'icon-remove', + handler: this.onRemove, + scope: this + }] + }) + }); + + this.optionsPanel = this.add(new Deluge.add.OptionsPanel()); + this.on('hide', this.onHide, this); + this.on('show', this.onShow, this); + }, - clear: function() { - this.list.getStore().removeAll(); - this.optionsPanel.clear(); - }, + clear: function() { + this.list.getStore().removeAll(); + this.optionsPanel.clear(); + }, - onAddClick: function() { - var torrents = []; - if (!this.list) return; - this.list.getStore().each(function(r) { - var id = r.get('info_hash'); - torrents.push({ - path: this.optionsPanel.getFilename(id), - options: this.optionsPanel.getOptions(id) - }); - }, this); + onAddClick: function() { + var torrents = []; + if (!this.list) return; + this.list.getStore().each(function(r) { + var id = r.get('info_hash'); + torrents.push({ + path: this.optionsPanel.getFilename(id), + options: this.optionsPanel.getOptions(id) + }); + }, this); - deluge.client.web.add_torrents(torrents, { - success: function(result) { - } - }) - this.clear(); - this.hide(); - }, + deluge.client.web.add_torrents(torrents, { + success: function(result) { + } + }) + this.clear(); + this.hide(); + }, - onCancelClick: function() { - this.clear(); - this.hide(); - }, + onCancelClick: function() { + this.clear(); + this.hide(); + }, - onFile: function() { - if (!this.file) this.file = new Deluge.add.FileWindow(); - this.file.show(); - }, + onFile: function() { + if (!this.file) this.file = new Deluge.add.FileWindow(); + this.file.show(); + }, - onHide: function() { - this.optionsPanel.setActiveTab(0); - this.optionsPanel.files.setDisabled(true); - this.optionsPanel.form.setDisabled(true); - }, + onHide: function() { + this.optionsPanel.setActiveTab(0); + this.optionsPanel.files.setDisabled(true); + this.optionsPanel.form.setDisabled(true); + }, - onRemove: function() { - if (!this.list.getSelectionCount()) return; - var torrent = this.list.getSelectedRecords()[0]; - this.list.getStore().remove(torrent); - this.optionsPanel.clear(); - - if (this.torrents && this.torrents[torrent.id]) delete this.torrents[torrent.id]; - }, + onRemove: function() { + if (!this.list.getSelectionCount()) return; + var torrent = this.list.getSelectedRecords()[0]; + this.list.getStore().remove(torrent); + this.optionsPanel.clear(); + + if (this.torrents && this.torrents[torrent.id]) delete this.torrents[torrent.id]; + }, - onSelect: function(list, selections) { - if (selections.length) { - var record = this.list.getRecord(selections[0]); - this.optionsPanel.setTorrent(record.get('info_hash')); - this.optionsPanel.files.setDisabled(false); - this.optionsPanel.form.setDisabled(false); - } else { - this.optionsPanel.files.setDisabled(true); - this.optionsPanel.form.setDisabled(true); - } - }, + onSelect: function(list, selections) { + if (selections.length) { + var record = this.list.getRecord(selections[0]); + this.optionsPanel.setTorrent(record.get('info_hash')); + this.optionsPanel.files.setDisabled(false); + this.optionsPanel.form.setDisabled(false); + } else { + this.optionsPanel.files.setDisabled(true); + this.optionsPanel.form.setDisabled(true); + } + }, - onShow: function() { - if (!this.url) { - this.url = new Deluge.add.UrlWindow(); - this.url.on('beforeadd', this.onTorrentBeforeAdd, this); - this.url.on('add', this.onTorrentAdd, this); - } + onShow: function() { + if (!this.url) { + this.url = new Deluge.add.UrlWindow(); + this.url.on('beforeadd', this.onTorrentBeforeAdd, this); + this.url.on('add', this.onTorrentAdd, this); + } - if (!this.file) { - this.file = new Deluge.add.FileWindow(); - this.file.on('beforeadd', this.onTorrentBeforeAdd, this); - this.file.on('add', this.onTorrentAdd, this); - } - - this.optionsPanel.form.getDefaults(); - }, + if (!this.file) { + this.file = new Deluge.add.FileWindow(); + this.file.on('beforeadd', this.onTorrentBeforeAdd, this); + this.file.on('add', this.onTorrentAdd, this); + } + + this.optionsPanel.form.getDefaults(); + }, - onTorrentBeforeAdd: function(torrentId, text) { - var store = this.list.getStore(); - store.loadData([[torrentId, null, text]], true); - }, + onTorrentBeforeAdd: function(torrentId, text) { + var store = this.list.getStore(); + store.loadData([[torrentId, null, text]], true); + }, - onTorrentAdd: function(torrentId, info) { - var r = this.list.getStore().getById(torrentId); - if (!info) { - Ext.MessageBox.show({ - title: _('Error'), - msg: _('Not a valid torrent'), - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.ERROR, - iconCls: 'x-deluge-icon-error' - }); - this.list.getStore().remove(r); - } else { - r.set('info_hash', info['info_hash']); - r.set('text', info['name']); - this.list.getStore().commitChanges(); - this.optionsPanel.addTorrent(info); - } - }, + onTorrentAdd: function(torrentId, info) { + var r = this.list.getStore().getById(torrentId); + if (!info) { + Ext.MessageBox.show({ + title: _('Error'), + msg: _('Not a valid torrent'), + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + this.list.getStore().remove(r); + } else { + r.set('info_hash', info['info_hash']); + r.set('text', info['name']); + this.list.getStore().commitChanges(); + this.optionsPanel.addTorrent(info); + } + }, - onUrl: function(button, event) { - this.url.show(); - } + onUrl: function(button, event) { + this.url.show(); + } }); diff --git a/deluge/ui/web/js/deluge-all/add/FileWindow.js b/deluge/ui/web/js/deluge-all/add/FileWindow.js index 632e7a306..65f9a6073 100644 --- a/deluge/ui/web/js/deluge-all/add/FileWindow.js +++ b/deluge/ui/web/js/deluge-all/add/FileWindow.js @@ -37,80 +37,80 @@ Ext.ns('Deluge.add'); */ Deluge.add.FileWindow = Ext.extend(Deluge.add.Window, { - title: _('Add from File'), - layout: 'fit', - width: 350, - height: 115, - modal: true, - plain: true, - buttonAlign: 'center', - closeAction: 'hide', - bodyStyle: 'padding: 10px 5px;', - iconCls: 'x-deluge-add-file', + title: _('Add from File'), + layout: 'fit', + width: 350, + height: 115, + modal: true, + plain: true, + buttonAlign: 'center', + closeAction: 'hide', + bodyStyle: 'padding: 10px 5px;', + iconCls: 'x-deluge-add-file', - initComponent: function() { - Deluge.add.FileWindow.superclass.initComponent.call(this); - this.addButton(_('Add'), this.onAddClick, this); - - this.form = this.add({ - xtype: 'form', - baseCls: 'x-plain', - labelWidth: 35, - autoHeight: true, - fileUpload: true, - items: [{ - xtype: 'fileuploadfield', - id: 'torrentFile', - width: 280, - emptyText: _('Select a torrent'), - fieldLabel: _('File'), - name: 'file', - buttonCfg: { - text: _('Browse') + '...' - } - }] - }); - }, - - // private - onAddClick: function(field, e) { - if (this.form.getForm().isValid()) { - this.torrentId = this.createTorrentId(); - this.form.getForm().submit({ - url: '/upload', - waitMsg: _('Uploading your torrent...'), - failure: this.onUploadFailure, - success: this.onUploadSuccess, - scope: this - }); - var name = this.form.getForm().findField('torrentFile').value; - name = name.split('\\').slice(-1)[0]; - this.fireEvent('beforeadd', this.torrentId, name); - } - }, - - // private - onGotInfo: function(info, obj, response, request) { - info['filename'] = request.options.filename; - this.fireEvent('add', this.torrentId, info); - }, + initComponent: function() { + Deluge.add.FileWindow.superclass.initComponent.call(this); + this.addButton(_('Add'), this.onAddClick, this); + + this.form = this.add({ + xtype: 'form', + baseCls: 'x-plain', + labelWidth: 35, + autoHeight: true, + fileUpload: true, + items: [{ + xtype: 'fileuploadfield', + id: 'torrentFile', + width: 280, + emptyText: _('Select a torrent'), + fieldLabel: _('File'), + name: 'file', + buttonCfg: { + text: _('Browse') + '...' + } + }] + }); + }, + + // private + onAddClick: function(field, e) { + if (this.form.getForm().isValid()) { + this.torrentId = this.createTorrentId(); + this.form.getForm().submit({ + url: '/upload', + waitMsg: _('Uploading your torrent...'), + failure: this.onUploadFailure, + success: this.onUploadSuccess, + scope: this + }); + var name = this.form.getForm().findField('torrentFile').value; + name = name.split('\\').slice(-1)[0]; + this.fireEvent('beforeadd', this.torrentId, name); + } + }, + + // private + onGotInfo: function(info, obj, response, request) { + info['filename'] = request.options.filename; + this.fireEvent('add', this.torrentId, info); + }, - // private - onUploadFailure: function(form, action) { - this.hide(); - }, - - // private - onUploadSuccess: function(fp, upload) { - this.hide(); - if (upload.result.success) { - var filename = upload.result.files[0]; - this.form.getForm().findField('torrentFile').setValue(''); - deluge.client.web.get_torrent_info(filename, { - success: this.onGotInfo, - scope: this, - filename: filename - }); - } - } + // private + onUploadFailure: function(form, action) { + this.hide(); + }, + + // private + onUploadSuccess: function(fp, upload) { + this.hide(); + if (upload.result.success) { + var filename = upload.result.files[0]; + this.form.getForm().findField('torrentFile').setValue(''); + deluge.client.web.get_torrent_info(filename, { + success: this.onGotInfo, + scope: this, + filename: filename + }); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/add/FilesTab.js b/deluge/ui/web/js/deluge-all/add/FilesTab.js index e1eccc063..3e624823d 100644 --- a/deluge/ui/web/js/deluge-all/add/FilesTab.js +++ b/deluge/ui/web/js/deluge-all/add/FilesTab.js @@ -37,78 +37,78 @@ Ext.ns('Deluge.add'); */ Deluge.add.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, { - layout: 'fit', - title: _('Files'), + layout: 'fit', + title: _('Files'), - autoScroll: true, - animate: false, - border: false, - disabled: true, - rootVisible: false, + autoScroll: true, + animate: false, + border: false, + disabled: true, + rootVisible: false, - columns: [{ - header: _('Filename'), - width: 295, - dataIndex: 'filename' - },{ - header: _('Size'), - width: 60, - dataIndex: 'size', - tpl: new Ext.XTemplate('{size:this.fsize}', { - fsize: function(v) { - return fsize(v); - } - }) - },{ - header: _('Download'), - width: 65, - dataIndex: 'download', - tpl: new Ext.XTemplate('{download:this.format}', { - format: function(v) { - return '
'; - } - }) - }], + columns: [{ + header: _('Filename'), + width: 295, + dataIndex: 'filename' + },{ + header: _('Size'), + width: 60, + dataIndex: 'size', + tpl: new Ext.XTemplate('{size:this.fsize}', { + fsize: function(v) { + return fsize(v); + } + }) + },{ + header: _('Download'), + width: 65, + dataIndex: 'download', + tpl: new Ext.XTemplate('{download:this.format}', { + format: function(v) { + return '
'; + } + }) + }], - initComponent: function() { - Deluge.add.FilesTab.superclass.initComponent.call(this); - this.on('click', this.onNodeClick, this); - }, + initComponent: function() { + Deluge.add.FilesTab.superclass.initComponent.call(this); + this.on('click', this.onNodeClick, this); + }, - clearFiles: function() { - var root = this.getRootNode(); - if (!root.hasChildNodes()) return; - root.cascade(function(node) { - if (!node.parentNode || !node.getOwnerTree()) return; - node.remove(); - }); - }, + clearFiles: function() { + var root = this.getRootNode(); + if (!root.hasChildNodes()) return; + root.cascade(function(node) { + if (!node.parentNode || !node.getOwnerTree()) return; + node.remove(); + }); + }, - setDownload: function(node, value, suppress) { - node.attributes.download = value; - node.ui.updateColumns(); + setDownload: function(node, value, suppress) { + node.attributes.download = value; + node.ui.updateColumns(); - if (node.isLeaf()) { - if (!suppress) { - return this.fireEvent('fileschecked', [node], value, !value); - } - } else { - var nodes = [node]; - node.cascade(function(n) { - n.attributes.download = value; - n.ui.updateColumns(); - nodes.push(n); - }, this); - if (!suppress) { - return this.fireEvent('fileschecked', nodes, value, !value); - } - } - }, + if (node.isLeaf()) { + if (!suppress) { + return this.fireEvent('fileschecked', [node], value, !value); + } + } else { + var nodes = [node]; + node.cascade(function(n) { + n.attributes.download = value; + n.ui.updateColumns(); + nodes.push(n); + }, this); + if (!suppress) { + return this.fireEvent('fileschecked', nodes, value, !value); + } + } + }, - onNodeClick: function(node, e) { - var el = new Ext.Element(e.target); - if (el.getAttribute('rel') == 'chkbox') { - this.setDownload(node, !node.attributes.download); - } - } + onNodeClick: function(node, e) { + var el = new Ext.Element(e.target); + if (el.getAttribute('rel') == 'chkbox') { + this.setDownload(node, !node.attributes.download); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/add/OptionsPanel.js b/deluge/ui/web/js/deluge-all/add/OptionsPanel.js index 2d37bde91..5578a4520 100644 --- a/deluge/ui/web/js/deluge-all/add/OptionsPanel.js +++ b/deluge/ui/web/js/deluge-all/add/OptionsPanel.js @@ -33,127 +33,127 @@ Ext.ns('Deluge.add'); Deluge.add.OptionsPanel = Ext.extend(Ext.TabPanel, { - torrents: {}, + torrents: {}, - // layout options - region: 'south', - margins: '5 5 5 5', - activeTab: 0, - height: 220, + // layout options + region: 'south', + margins: '5 5 5 5', + activeTab: 0, + height: 220, - initComponent: function() { - Deluge.add.OptionsPanel.superclass.initComponent.call(this); - this.files = this.add(new Deluge.add.FilesTab()); - this.form = this.add(new Deluge.add.OptionsTab()); + initComponent: function() { + Deluge.add.OptionsPanel.superclass.initComponent.call(this); + this.files = this.add(new Deluge.add.FilesTab()); + this.form = this.add(new Deluge.add.OptionsTab()); - this.files.on('fileschecked', this.onFilesChecked, this); - }, + this.files.on('fileschecked', this.onFilesChecked, this); + }, - addTorrent: function(torrent) { - this.torrents[torrent['info_hash']] = torrent; - var fileIndexes = {}; - this.walkFileTree(torrent['files_tree'], function(filename, type, entry, parent) { - if (type != 'file') return; - fileIndexes[entry.index] = entry.download; - }, this); + addTorrent: function(torrent) { + this.torrents[torrent['info_hash']] = torrent; + var fileIndexes = {}; + this.walkFileTree(torrent['files_tree'], function(filename, type, entry, parent) { + if (type != 'file') return; + fileIndexes[entry.index] = entry.download; + }, this); - var priorities = []; - Ext.each(Ext.keys(fileIndexes), function(index) { - priorities[index] = fileIndexes[index]; - }); - - var oldId = this.form.optionsManager.changeId(torrent['info_hash'], true); - this.form.optionsManager.setDefault('file_priorities', priorities); - this.form.optionsManager.changeId(oldId, true); - }, + var priorities = []; + Ext.each(Ext.keys(fileIndexes), function(index) { + priorities[index] = fileIndexes[index]; + }); + + var oldId = this.form.optionsManager.changeId(torrent['info_hash'], true); + this.form.optionsManager.setDefault('file_priorities', priorities); + this.form.optionsManager.changeId(oldId, true); + }, - clear: function() { - this.files.clearFiles(); - this.form.optionsManager.resetAll(); - }, + clear: function() { + this.files.clearFiles(); + this.form.optionsManager.resetAll(); + }, - getFilename: function(torrentId) { - return this.torrents[torrentId]['filename']; - }, + getFilename: function(torrentId) { + return this.torrents[torrentId]['filename']; + }, - getOptions: function(torrentId) { - var oldId = this.form.optionsManager.changeId(torrentId, true); - var options = this.form.optionsManager.get(); - this.form.optionsManager.changeId(oldId, true); - Ext.each(options['file_priorities'], function(priority, index) { - options['file_priorities'][index] = (priority) ? 1 : 0; - }); - return options; - }, + getOptions: function(torrentId) { + var oldId = this.form.optionsManager.changeId(torrentId, true); + var options = this.form.optionsManager.get(); + this.form.optionsManager.changeId(oldId, true); + Ext.each(options['file_priorities'], function(priority, index) { + options['file_priorities'][index] = (priority) ? 1 : 0; + }); + return options; + }, - setTorrent: function(torrentId) { - if (!torrentId) return; + setTorrent: function(torrentId) { + if (!torrentId) return; - this.torrentId = torrentId; - this.form.optionsManager.changeId(torrentId); - - this.files.clearFiles(); - var root = this.files.getRootNode(); - var priorities = this.form.optionsManager.get('file_priorities'); + this.torrentId = torrentId; + this.form.optionsManager.changeId(torrentId); + + this.files.clearFiles(); + var root = this.files.getRootNode(); + var priorities = this.form.optionsManager.get('file_priorities'); - this.walkFileTree(this.torrents[torrentId]['files_tree'], function(filename, type, entry, parentNode) { - var node = new Ext.tree.TreeNode({ - download: (entry.index) ? priorities[entry.index] : true, - filename: filename, - fileindex: entry.index, - leaf: type != 'dir', - size: entry.length - }); - parentNode.appendChild(node); - if (type == 'dir') return node; - }, this, root); - root.firstChild.expand(); - }, + this.walkFileTree(this.torrents[torrentId]['files_tree'], function(filename, type, entry, parentNode) { + var node = new Ext.tree.TreeNode({ + download: (entry.index) ? priorities[entry.index] : true, + filename: filename, + fileindex: entry.index, + leaf: type != 'dir', + size: entry.length + }); + parentNode.appendChild(node); + if (type == 'dir') return node; + }, this, root); + root.firstChild.expand(); + }, - walkFileTree: function(files, callback, scope, parentNode) { - for (var filename in files.contents) { - var entry = files.contents[filename]; - var type = entry.type; + walkFileTree: function(files, callback, scope, parentNode) { + for (var filename in files.contents) { + var entry = files.contents[filename]; + var type = entry.type; - if (scope) { - var ret = callback.apply(scope, [filename, type, entry, parentNode]); - } else { - var ret = callback(filename, type, entry, parentNode); - } - - if (type == 'dir') this.walkFileTree(entry, callback, scope, ret); - } - }, + if (scope) { + var ret = callback.apply(scope, [filename, type, entry, parentNode]); + } else { + var ret = callback(filename, type, entry, parentNode); + } + + if (type == 'dir') this.walkFileTree(entry, callback, scope, ret); + } + }, - onFilesChecked: function(nodes, newValue, oldValue) { - if (this.form.optionsManager.get('compact_allocation')) { - Ext.Msg.show({ - title: _('Unable to set file priority!'), - msg: _('File prioritization is unavailable when using Compact allocation. Would you like to switch to Full allocation?'), - buttons: Ext.Msg.YESNO, - fn: function(result) { - if (result == 'yes') { - this.form.optionsManager.update('compact_allocation', false); - Ext.each(nodes, function(node) { - if (node.attributes.fileindex < 0) return; - var priorities = this.form.optionsManager.get('file_priorities'); - priorities[node.attributes.fileindex] = newValue; - this.form.optionsManager.update('file_priorities', priorities); - }, this); - } else { - this.files.setDownload(nodes[0], oldValue, true); - } - }, - scope: this, - icon: Ext.MessageBox.QUESTION - }); - } else { - Ext.each(nodes, function(node) { - if (node.attributes.fileindex < 0) return; - var priorities = this.form.optionsManager.get('file_priorities'); - priorities[node.attributes.fileindex] = newValue; - this.form.optionsManager.update('file_priorities', priorities); - }, this); - } - } + onFilesChecked: function(nodes, newValue, oldValue) { + if (this.form.optionsManager.get('compact_allocation')) { + Ext.Msg.show({ + title: _('Unable to set file priority!'), + msg: _('File prioritization is unavailable when using Compact allocation. Would you like to switch to Full allocation?'), + buttons: Ext.Msg.YESNO, + fn: function(result) { + if (result == 'yes') { + this.form.optionsManager.update('compact_allocation', false); + Ext.each(nodes, function(node) { + if (node.attributes.fileindex < 0) return; + var priorities = this.form.optionsManager.get('file_priorities'); + priorities[node.attributes.fileindex] = newValue; + this.form.optionsManager.update('file_priorities', priorities); + }, this); + } else { + this.files.setDownload(nodes[0], oldValue, true); + } + }, + scope: this, + icon: Ext.MessageBox.QUESTION + }); + } else { + Ext.each(nodes, function(node) { + if (node.attributes.fileindex < 0) return; + var priorities = this.form.optionsManager.get('file_priorities'); + priorities[node.attributes.fileindex] = newValue; + this.form.optionsManager.update('file_priorities', priorities); + }, this); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/add/OptionsTab.js b/deluge/ui/web/js/deluge-all/add/OptionsTab.js index c1ff5bca8..c65dc6f40 100644 --- a/deluge/ui/web/js/deluge-all/add/OptionsTab.js +++ b/deluge/ui/web/js/deluge-all/add/OptionsTab.js @@ -37,148 +37,148 @@ Ext.ns('Deluge.add'); */ Deluge.add.OptionsTab = Ext.extend(Ext.form.FormPanel, { - title: _('Options'), - height: 170, + title: _('Options'), + height: 170, - border: false, - bodyStyle: 'padding: 5px', - disabled: true, - labelWidth: 1, + border: false, + bodyStyle: 'padding: 5px', + disabled: true, + labelWidth: 1, - initComponent: function() { - Deluge.add.OptionsTab.superclass.initComponent.call(this); + initComponent: function() { + Deluge.add.OptionsTab.superclass.initComponent.call(this); - this.optionsManager = new Deluge.MultiOptionsManager(); + this.optionsManager = new Deluge.MultiOptionsManager(); - var fieldset = this.add({ - xtype: 'fieldset', - title: _('Download Location'), - border: false, - autoHeight: true, - defaultType: 'textfield', - labelWidth: 1, - fieldLabel: '', - style: 'padding-bottom: 5px; margin-bottom: 0px;' - }); + var fieldset = this.add({ + xtype: 'fieldset', + title: _('Download Location'), + border: false, + autoHeight: true, + defaultType: 'textfield', + labelWidth: 1, + fieldLabel: '', + style: 'padding-bottom: 5px; margin-bottom: 0px;' + }); - this.optionsManager.bind('download_location', fieldset.add({ - fieldLabel: '', - name: 'download_location', - width: 400, - labelSeparator: '' - })); - - var panel = this.add({ - border: false, - layout: 'column', - defaultType: 'fieldset' - }); - fieldset = panel.add({ - title: _('Allocation'), - border: false, - autoHeight: true, - defaultType: 'radio', - width: 100 - }); + this.optionsManager.bind('download_location', fieldset.add({ + fieldLabel: '', + name: 'download_location', + width: 400, + labelSeparator: '' + })); + + var panel = this.add({ + border: false, + layout: 'column', + defaultType: 'fieldset' + }); + fieldset = panel.add({ + title: _('Allocation'), + border: false, + autoHeight: true, + defaultType: 'radio', + width: 100 + }); - this.optionsManager.bind('compact_allocation', fieldset.add({ - xtype: 'radiogroup', - columns: 1, - vertical: true, - labelSeparator: '', - items: [{ - name: 'compact_allocation', - value: false, - inputValue: false, - boxLabel: _('Full'), - fieldLabel: '', - labelSeparator: '' - }, { - name: 'compact_allocation', - value: true, - inputValue: true, - boxLabel: _('Compact'), - fieldLabel: '', - labelSeparator: '' - }] - })); + this.optionsManager.bind('compact_allocation', fieldset.add({ + xtype: 'radiogroup', + columns: 1, + vertical: true, + labelSeparator: '', + items: [{ + name: 'compact_allocation', + value: false, + inputValue: false, + boxLabel: _('Full'), + fieldLabel: '', + labelSeparator: '' + }, { + name: 'compact_allocation', + value: true, + inputValue: true, + boxLabel: _('Compact'), + fieldLabel: '', + labelSeparator: '' + }] + })); - fieldset = panel.add({ - title: _('Bandwidth'), - border: false, - autoHeight: true, - labelWidth: 100, - width: 200, - defaultType: 'spinnerfield' - }); - this.optionsManager.bind('max_download_speed', fieldset.add({ - fieldLabel: _('Max Down Speed'), - labelStyle: 'margin-left: 10px', - name: 'max_download_speed', - width: 60 - })); - this.optionsManager.bind('max_upload_speed', fieldset.add({ - fieldLabel: _('Max Up Speed'), - labelStyle: 'margin-left: 10px', - name: 'max_upload_speed', - width: 60 - })); - this.optionsManager.bind('max_connections', fieldset.add({ - fieldLabel: _('Max Connections'), - labelStyle: 'margin-left: 10px', - name: 'max_connections', - width: 60 - })); - this.optionsManager.bind('max_upload_slots', fieldset.add({ - fieldLabel: _('Max Upload Slots'), - labelStyle: 'margin-left: 10px', - name: 'max_upload_slots', - width: 60 - })); - - fieldset = panel.add({ - title: _('General'), - border: false, - autoHeight: true, - defaultType: 'checkbox' - }); - this.optionsManager.bind('add_paused', fieldset.add({ - name: 'add_paused', - boxLabel: _('Add In Paused State'), - fieldLabel: '', - labelSeparator: '' - })); - this.optionsManager.bind('prioritize_first_last_pieces', fieldset.add({ - name: 'prioritize_first_last_pieces', - boxLabel: _('Prioritize First/Last Pieces'), - fieldLabel: '', - labelSeparator: '' - })); - }, + fieldset = panel.add({ + title: _('Bandwidth'), + border: false, + autoHeight: true, + labelWidth: 100, + width: 200, + defaultType: 'spinnerfield' + }); + this.optionsManager.bind('max_download_speed', fieldset.add({ + fieldLabel: _('Max Down Speed'), + labelStyle: 'margin-left: 10px', + name: 'max_download_speed', + width: 60 + })); + this.optionsManager.bind('max_upload_speed', fieldset.add({ + fieldLabel: _('Max Up Speed'), + labelStyle: 'margin-left: 10px', + name: 'max_upload_speed', + width: 60 + })); + this.optionsManager.bind('max_connections', fieldset.add({ + fieldLabel: _('Max Connections'), + labelStyle: 'margin-left: 10px', + name: 'max_connections', + width: 60 + })); + this.optionsManager.bind('max_upload_slots', fieldset.add({ + fieldLabel: _('Max Upload Slots'), + labelStyle: 'margin-left: 10px', + name: 'max_upload_slots', + width: 60 + })); + + fieldset = panel.add({ + title: _('General'), + border: false, + autoHeight: true, + defaultType: 'checkbox' + }); + this.optionsManager.bind('add_paused', fieldset.add({ + name: 'add_paused', + boxLabel: _('Add In Paused State'), + fieldLabel: '', + labelSeparator: '' + })); + this.optionsManager.bind('prioritize_first_last_pieces', fieldset.add({ + name: 'prioritize_first_last_pieces', + boxLabel: _('Prioritize First/Last Pieces'), + fieldLabel: '', + labelSeparator: '' + })); + }, - getDefaults: function() { - var keys = ['add_paused','compact_allocation','download_location', - 'max_connections_per_torrent','max_download_speed_per_torrent', - 'max_upload_slots_per_torrent','max_upload_speed_per_torrent', - 'prioritize_first_last_pieces']; + getDefaults: function() { + var keys = ['add_paused','compact_allocation','download_location', + 'max_connections_per_torrent','max_download_speed_per_torrent', + 'max_upload_slots_per_torrent','max_upload_speed_per_torrent', + 'prioritize_first_last_pieces']; - deluge.client.core.get_config_values(keys, { - success: function(config) { - var options = { - 'file_priorities': [], - 'add_paused': config.add_paused, - 'compact_allocation': config.compact_allocation, - 'download_location': config.download_location, - 'max_connections': config.max_connections_per_torrent, - 'max_download_speed': config.max_download_speed_per_torrent, - 'max_upload_slots': config.max_upload_slots_per_torrent, - 'max_upload_speed': config.max_upload_speed_per_torrent, - 'prioritize_first_last_pieces': config.prioritize_first_last_pieces - } - this.optionsManager.options = options; - this.optionsManager.resetAll(); - }, - scope: this - }); - } + deluge.client.core.get_config_values(keys, { + success: function(config) { + var options = { + 'file_priorities': [], + 'add_paused': config.add_paused, + 'compact_allocation': config.compact_allocation, + 'download_location': config.download_location, + 'max_connections': config.max_connections_per_torrent, + 'max_download_speed': config.max_download_speed_per_torrent, + 'max_upload_slots': config.max_upload_slots_per_torrent, + 'max_upload_speed': config.max_upload_speed_per_torrent, + 'prioritize_first_last_pieces': config.prioritize_first_last_pieces + } + this.optionsManager.options = options; + this.optionsManager.resetAll(); + }, + scope: this + }); + } }); diff --git a/deluge/ui/web/js/deluge-all/add/UrlWindow.js b/deluge/ui/web/js/deluge-all/add/UrlWindow.js index 963be0264..2fa8199d6 100644 --- a/deluge/ui/web/js/deluge-all/add/UrlWindow.js +++ b/deluge/ui/web/js/deluge-all/add/UrlWindow.js @@ -1,6 +1,6 @@ /*! * Deluge.add.UrlWindow.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -33,75 +33,75 @@ Ext.namespace('Deluge.add'); Deluge.add.UrlWindow = Ext.extend(Deluge.add.Window, { - title: _('Add from Url'), - modal: true, - plain: true, - layout: 'fit', - width: 350, - height: 155, + title: _('Add from Url'), + modal: true, + plain: true, + layout: 'fit', + width: 350, + height: 155, - buttonAlign: 'center', - closeAction: 'hide', - bodyStyle: 'padding: 10px 5px;', - iconCls: 'x-deluge-add-url-window-icon', - - initComponent: function() { - Deluge.add.UrlWindow.superclass.initComponent.call(this); - this.addButton(_('Add'), this.onAddClick, this); - - var form = this.add({ - xtype: 'form', - defaultType: 'textfield', - baseCls: 'x-plain', - labelWidth: 55 - }); - - this.urlField = form.add({ - fieldLabel: _('Url'), - id: 'url', - name: 'url', - anchor: '100%' - }); - this.urlField.on('specialkey', this.onAdd, this); - - this.cookieField = form.add({ - fieldLabel: _('Cookies'), - id: 'cookies', - name: 'cookies', - anchor: '100%' - }); - this.cookieField.on('specialkey', this.onAdd, this); - }, - - onAddClick: function(field, e) { - if ((field.id == 'url' || field.id == 'cookies') && e.getKey() != e.ENTER) return; + buttonAlign: 'center', + closeAction: 'hide', + bodyStyle: 'padding: 10px 5px;', + iconCls: 'x-deluge-add-url-window-icon', - var field = this.urlField; - var url = field.getValue(); - var cookies = this.cookieField.getValue(); - var torrentId = this.createTorrentId(); - - deluge.client.web.download_torrent_from_url(url, cookies, { - success: this.onDownload, - scope: this, - torrentId: torrentId - }); - this.hide(); - this.fireEvent('beforeadd', torrentId, url); - }, - - onDownload: function(filename, obj, resp, req) { - this.urlField.setValue(''); - deluge.client.web.get_torrent_info(filename, { - success: this.onGotInfo, - scope: this, - filename: filename, - torrentId: req.options.torrentId - }); - }, - - onGotInfo: function(info, obj, response, request) { - info['filename'] = request.options.filename; - this.fireEvent('add', request.options.torrentId, info); - } + initComponent: function() { + Deluge.add.UrlWindow.superclass.initComponent.call(this); + this.addButton(_('Add'), this.onAddClick, this); + + var form = this.add({ + xtype: 'form', + defaultType: 'textfield', + baseCls: 'x-plain', + labelWidth: 55 + }); + + this.urlField = form.add({ + fieldLabel: _('Url'), + id: 'url', + name: 'url', + width: '97%' + }); + this.urlField.on('specialkey', this.onAdd, this); + + this.cookieField = form.add({ + fieldLabel: _('Cookies'), + id: 'cookies', + name: 'cookies', + width: '97%' + }); + this.cookieField.on('specialkey', this.onAdd, this); + }, + + onAddClick: function(field, e) { + if ((field.id == 'url' || field.id == 'cookies') && e.getKey() != e.ENTER) return; + + var field = this.urlField; + var url = field.getValue(); + var cookies = this.cookieField.getValue(); + var torrentId = this.createTorrentId(); + + deluge.client.web.download_torrent_from_url(url, cookies, { + success: this.onDownload, + scope: this, + torrentId: torrentId + }); + this.hide(); + this.fireEvent('beforeadd', torrentId, url); + }, + + onDownload: function(filename, obj, resp, req) { + this.urlField.setValue(''); + deluge.client.web.get_torrent_info(filename, { + success: this.onGotInfo, + scope: this, + filename: filename, + torrentId: req.options.torrentId + }); + }, + + onGotInfo: function(info, obj, response, request) { + info['filename'] = request.options.filename; + this.fireEvent('add', request.options.torrentId, info); + } }); diff --git a/deluge/ui/web/js/deluge-all/add/Window.js b/deluge/ui/web/js/deluge-all/add/Window.js index 39b7843f1..b47c5d658 100644 --- a/deluge/ui/web/js/deluge-all/add/Window.js +++ b/deluge/ui/web/js/deluge-all/add/Window.js @@ -45,9 +45,9 @@ Deluge.add.Window = Ext.extend(Ext.Window, { ); }, - /** - * Create an id for the torrent before we have any info about it. - */ + /** + * Create an id for the torrent before we have any info about it. + */ createTorrentId: function() { return new Date().getTime(); } diff --git a/deluge/ui/web/js/deluge-all/data/PeerRecord.js b/deluge/ui/web/js/deluge-all/data/PeerRecord.js index 79da154ba..0fdd58e9b 100644 --- a/deluge/ui/web/js/deluge-all/data/PeerRecord.js +++ b/deluge/ui/web/js/deluge-all/data/PeerRecord.js @@ -43,27 +43,27 @@ Ext.namespace('Deluge.data'); * @param {Object} data The peer data */ Deluge.data.Peer = Ext.data.Record.create([ - { - name: 'country', - type: 'string' - }, { - name: 'ip', - type: 'string', - sortType: Deluge.data.SortTypes.asIPAddress - }, { - name: 'client', - type: 'string' - }, { - name: 'progress', - type: 'float' - }, { - name: 'down_speed', - type: 'int' - }, { - name: 'up_speed', - type: 'int' - }, { - name: 'seed', - type: 'int' - } + { + name: 'country', + type: 'string' + }, { + name: 'ip', + type: 'string', + sortType: Deluge.data.SortTypes.asIPAddress + }, { + name: 'client', + type: 'string' + }, { + name: 'progress', + type: 'float' + }, { + name: 'down_speed', + type: 'int' + }, { + name: 'up_speed', + type: 'int' + }, { + name: 'seed', + type: 'int' + } ]); diff --git a/deluge/ui/web/js/deluge-all/data/SortTypes.js b/deluge/ui/web/js/deluge-all/data/SortTypes.js index 2acaf8b4a..e404ba348 100644 --- a/deluge/ui/web/js/deluge-all/data/SortTypes.js +++ b/deluge/ui/web/js/deluge-all/data/SortTypes.js @@ -41,12 +41,12 @@ Ext.namespace('Deluge.data'); * @singleton */ Deluge.data.SortTypes = { - asIPAddress: function(value) { - var d = value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\:(\d+)/); - return ((((((+d[1])*256)+(+d[2]))*256)+(+d[3]))*256)+(+d[4]); - }, + asIPAddress: function(value) { + var d = value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\:(\d+)/); + return ((((((+d[1])*256)+(+d[2]))*256)+(+d[3]))*256)+(+d[4]); + }, - asQueuePosition: function(value) { - return (value > -1) ? value : Number.MAX_VALUE; - } + asQueuePosition: function(value) { + return (value > -1) ? value : Number.MAX_VALUE; + } } diff --git a/deluge/ui/web/js/deluge-all/data/TorrentRecord.js b/deluge/ui/web/js/deluge-all/data/TorrentRecord.js index 2b89f3d7a..d13806c14 100644 --- a/deluge/ui/web/js/deluge-all/data/TorrentRecord.js +++ b/deluge/ui/web/js/deluge-all/data/TorrentRecord.js @@ -43,52 +43,52 @@ Ext.namespace('Deluge.data'); * @param {Object} data The torrents data */ Deluge.data.Torrent = Ext.data.Record.create([{ - name: 'queue', - type: 'int' - }, { - name: 'name', - type: 'string' - }, { - name: 'total_size', - type: 'int' - }, { - name: 'state', - type: 'string' - }, { - name: 'progress', - type: 'int' - }, { - name: 'num_seeds', - type: 'int' - }, { - name: 'total_seeds', - type: 'int' - }, { - name: 'num_peers', - type: 'int' - }, { - name: 'total_peers', - type: 'int' - }, { - name: 'download_payload_rate', - type: 'int' - }, { - name: 'upload_payload_rate', - type: 'int' - }, { - name: 'eta', - type: 'int' - }, { - name: 'ratio', - type: 'float' - }, { - name: 'distributed_copies', - type: 'float' - }, { - name: 'time_added', - type: 'int' - }, { - name: 'tracker_host', - type: 'string' - } + name: 'queue', + type: 'int' + }, { + name: 'name', + type: 'string' + }, { + name: 'total_size', + type: 'int' + }, { + name: 'state', + type: 'string' + }, { + name: 'progress', + type: 'int' + }, { + name: 'num_seeds', + type: 'int' + }, { + name: 'total_seeds', + type: 'int' + }, { + name: 'num_peers', + type: 'int' + }, { + name: 'total_peers', + type: 'int' + }, { + name: 'download_payload_rate', + type: 'int' + }, { + name: 'upload_payload_rate', + type: 'int' + }, { + name: 'eta', + type: 'int' + }, { + name: 'ratio', + type: 'float' + }, { + name: 'distributed_copies', + type: 'float' + }, { + name: 'time_added', + type: 'int' + }, { + name: 'tracker_host', + type: 'string' + } ]); diff --git a/deluge/ui/web/js/deluge-all/details/DetailsPanel.js b/deluge/ui/web/js/deluge-all/details/DetailsPanel.js index 0ac556e4a..382ae6815 100644 --- a/deluge/ui/web/js/deluge-all/details/DetailsPanel.js +++ b/deluge/ui/web/js/deluge-all/details/DetailsPanel.js @@ -36,69 +36,69 @@ Ext.namespace('Deluge.details'); */ Deluge.details.DetailsPanel = Ext.extend(Ext.TabPanel, { - region: 'south', - id: 'torrentDetails', - split: true, - height: 210, - minSize: 100, - collapsible: true, - margins: '0 5 5 5', - activeTab: 0, - - initComponent: function() { - Deluge.details.DetailsPanel.superclass.initComponent.call(this); - this.add(new Deluge.details.StatusTab()); - this.add(new Deluge.details.DetailsTab()); - this.add(new Deluge.details.FilesTab()); - this.add(new Deluge.details.PeersTab()); - this.add(new Deluge.details.OptionsTab()); - }, - - clear: function() { - this.items.each(function(panel) { - if (panel.clear) { - panel.clear.defer(100, panel); - panel.disable(); - } - }); - }, - - - update: function(tab) { - var torrent = deluge.torrents.getSelected(); - if (!torrent) { - this.clear(); - return; - } - - this.items.each(function(tab) { - if (tab.disabled) tab.enable(); - }); - - tab = tab || this.getActiveTab(); - if (tab.update) tab.update(torrent.id); - }, - - /* Event Handlers */ - - // We need to add the events in onRender since Deluge.Torrents hasn't - // been created yet. - onRender: function(ct, position) { - Deluge.details.DetailsPanel.superclass.onRender.call(this, ct, position); - deluge.events.on('disconnect', this.clear, this); - deluge.torrents.on('rowclick', this.onTorrentsClick, this); - this.on('tabchange', this.onTabChange, this); - - deluge.torrents.getSelectionModel().on('selectionchange', function(selModel) { - if (!selModel.hasSelection()) this.clear(); - }, this); - }, - - onTabChange: function(panel, tab) { - this.update(tab); - }, - - onTorrentsClick: function(grid, rowIndex, e) { - this.update(); - } + region: 'south', + id: 'torrentDetails', + split: true, + height: 210, + minSize: 100, + collapsible: true, + margins: '0 5 5 5', + activeTab: 0, + + initComponent: function() { + Deluge.details.DetailsPanel.superclass.initComponent.call(this); + this.add(new Deluge.details.StatusTab()); + this.add(new Deluge.details.DetailsTab()); + this.add(new Deluge.details.FilesTab()); + this.add(new Deluge.details.PeersTab()); + this.add(new Deluge.details.OptionsTab()); + }, + + clear: function() { + this.items.each(function(panel) { + if (panel.clear) { + panel.clear.defer(100, panel); + panel.disable(); + } + }); + }, + + + update: function(tab) { + var torrent = deluge.torrents.getSelected(); + if (!torrent) { + this.clear(); + return; + } + + this.items.each(function(tab) { + if (tab.disabled) tab.enable(); + }); + + tab = tab || this.getActiveTab(); + if (tab.update) tab.update(torrent.id); + }, + + /* Event Handlers */ + + // We need to add the events in onRender since Deluge.Torrents hasn't + // been created yet. + onRender: function(ct, position) { + Deluge.details.DetailsPanel.superclass.onRender.call(this, ct, position); + deluge.events.on('disconnect', this.clear, this); + deluge.torrents.on('rowclick', this.onTorrentsClick, this); + this.on('tabchange', this.onTabChange, this); + + deluge.torrents.getSelectionModel().on('selectionchange', function(selModel) { + if (!selModel.hasSelection()) this.clear(); + }, this); + }, + + onTabChange: function(panel, tab) { + this.update(tab); + }, + + onTorrentsClick: function(grid, rowIndex, e) { + this.update(); + } }); diff --git a/deluge/ui/web/js/deluge-all/details/DetailsTab.js b/deluge/ui/web/js/deluge-all/details/DetailsTab.js index 9a3fce56f..ca41478d4 100644 --- a/deluge/ui/web/js/deluge-all/details/DetailsTab.js +++ b/deluge/ui/web/js/deluge-all/details/DetailsTab.js @@ -1,115 +1,115 @@ /* Script: Deluge.Details.Details.js - The details tab displayed in the details panel. + The details tab displayed in the details panel. Copyright: - (C) Damien Churchill 2009-2010 - 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, or (at your option) - any later version. + (C) Damien Churchill 2009-2010 + 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, 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. + 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, write to: - The Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor - Boston, MA 02110-1301, USA. + You should have received a copy of the GNU General Public License + along with this program. If not, write to: + The Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor + Boston, MA 02110-1301, USA. - In addition, as a special exception, the copyright holders give - permission to link the code of portions of this program with the OpenSSL - library. - You must obey the GNU General Public License in all respects for all of - the code used other than OpenSSL. If you modify file(s) with this - exception, you may extend this exception to your version of the file(s), - but you are not obligated to do so. If you do not wish to do so, delete - this exception statement from your version. If you delete this exception - statement from all source files in the program, then also delete it here. + In addition, as a special exception, the copyright holders give + permission to link the code of portions of this program with the OpenSSL + library. + You must obey the GNU General Public License in all respects for all of + the code used other than OpenSSL. If you modify file(s) with this + exception, you may extend this exception to your version of the file(s), + but you are not obligated to do so. If you do not wish to do so, delete + this exception statement from your version. If you delete this exception + statement from all source files in the program, then also delete it here. */ Deluge.details.DetailsTab = Ext.extend(Ext.Panel, { - title: _('Details'), + title: _('Details'), - fields: {}, + fields: {}, - queuedItems: {}, + queuedItems: {}, - oldData: {}, + oldData: {}, - initComponent: function() { - Deluge.details.DetailsTab.superclass.initComponent.call(this); - this.addItem('torrent_name', _('Name')); - this.addItem('hash', _('Hash')); - this.addItem('path', _('Path')); - this.addItem('size', _('Total Size')); - this.addItem('files', _('# of files')); - this.addItem('comment', _('Comment')); - this.addItem('status', _('Status')); - this.addItem('tracker', _('Tracker')); - }, - - onRender: function(ct, position) { - Deluge.details.DetailsTab.superclass.onRender.call(this, ct, position); - this.body.setStyle('padding', '10px'); - this.dl = Ext.DomHelper.append(this.body, {tag: 'dl'}, true); + initComponent: function() { + Deluge.details.DetailsTab.superclass.initComponent.call(this); + this.addItem('torrent_name', _('Name')); + this.addItem('hash', _('Hash')); + this.addItem('path', _('Path')); + this.addItem('size', _('Total Size')); + this.addItem('files', _('# of files')); + this.addItem('comment', _('Comment')); + this.addItem('status', _('Status')); + this.addItem('tracker', _('Tracker')); + }, - for (var id in this.queuedItems) { - this.doAddItem(id, this.queuedItems[id]); - } - }, + onRender: function(ct, position) { + Deluge.details.DetailsTab.superclass.onRender.call(this, ct, position); + this.body.setStyle('padding', '10px'); + this.dl = Ext.DomHelper.append(this.body, {tag: 'dl'}, true); - addItem: function(id, label) { - if (!this.rendered) { - this.queuedItems[id] = label; - } else { - this.doAddItem(id, label); - } - }, + for (var id in this.queuedItems) { + this.doAddItem(id, this.queuedItems[id]); + } + }, - // private - doAddItem: function(id, label) { - Ext.DomHelper.append(this.dl, {tag: 'dt', cls: id, html: label + ':'}); - this.fields[id] = Ext.DomHelper.append(this.dl, {tag: 'dd', cls: id, html: ''}, true); - }, - - clear: function() { - if (!this.fields) return; - for (var k in this.fields) { - this.fields[k].dom.innerHTML = ''; - } - this.oldData = {} - }, - - update: function(torrentId) { - deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Details, { - success: this.onRequestComplete, - scope: this, - torrentId: torrentId - }); - }, - - onRequestComplete: function(torrent, request, response, options) { - var data = { - torrent_name: torrent.name, - hash: options.options.torrentId, - path: torrent.save_path, - size: fsize(torrent.total_size), - files: torrent.num_files, - status: torrent.tracker_status, - tracker: torrent.tracker, - comment: torrent.comment - }; - - for (var field in this.fields) { - if (!Ext.isDefined(data[field])) continue; // this is a field we aren't responsible for. - if (data[field] == this.oldData[field]) continue; - this.fields[field].dom.innerHTML = Ext.escapeHTML(data[field]); - } - this.oldData = data; - } + addItem: function(id, label) { + if (!this.rendered) { + this.queuedItems[id] = label; + } else { + this.doAddItem(id, label); + } + }, + + // private + doAddItem: function(id, label) { + Ext.DomHelper.append(this.dl, {tag: 'dt', cls: id, html: label + ':'}); + this.fields[id] = Ext.DomHelper.append(this.dl, {tag: 'dd', cls: id, html: ''}, true); + }, + + clear: function() { + if (!this.fields) return; + for (var k in this.fields) { + this.fields[k].dom.innerHTML = ''; + } + this.oldData = {} + }, + + update: function(torrentId) { + deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Details, { + success: this.onRequestComplete, + scope: this, + torrentId: torrentId + }); + }, + + onRequestComplete: function(torrent, request, response, options) { + var data = { + torrent_name: torrent.name, + hash: options.options.torrentId, + path: torrent.save_path, + size: fsize(torrent.total_size), + files: torrent.num_files, + status: torrent.message, + tracker: torrent.tracker, + comment: torrent.comment + }; + + for (var field in this.fields) { + if (!Ext.isDefined(data[field])) continue; // this is a field we aren't responsible for. + if (data[field] == this.oldData[field]) continue; + this.fields[field].dom.innerHTML = Ext.escapeHTML(data[field]); + } + this.oldData = data; + } }); diff --git a/deluge/ui/web/js/deluge-all/details/FilesTab.js b/deluge/ui/web/js/deluge-all/details/FilesTab.js index a57168d47..061ed7950 100644 --- a/deluge/ui/web/js/deluge-all/details/FilesTab.js +++ b/deluge/ui/web/js/deluge-all/details/FilesTab.js @@ -29,200 +29,200 @@ * this exception statement from your version. If you delete this exception * statement from all source files in the program, then also delete it here. */ - + Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, { - title: _('Files'), + title: _('Files'), - autoScroll: true, - rootVisible: false, + autoScroll: true, + rootVisible: false, - columns: [{ - header: _('Filename'), - width: 330, - dataIndex: 'filename' - }, { - header: _('Size'), - width: 150, - dataIndex: 'size', - tpl: new Ext.XTemplate('{size:this.fsize}', { - fsize: function(v) { return fsize(v); } - }) - }, { - xtype: 'tgrendercolumn', - header: _('Progress'), - width: 150, - dataIndex: 'progress', - renderer: function(v) { - var progress = v * 100; - return Deluge.progressBar(progress, this.col.width, progress.toFixed(2) + '%', 0); - } - }, { - header: _('Priority'), - width: 150, - dataIndex: 'priority', - tpl: new Ext.XTemplate('' + - '
' + - '{priority:this.getName}' + - '
', { - getClass: function(v) { - return FILE_PRIORITY_CSS[v]; - }, + columns: [{ + header: _('Filename'), + width: 330, + dataIndex: 'filename' + }, { + header: _('Size'), + width: 150, + dataIndex: 'size', + tpl: new Ext.XTemplate('{size:this.fsize}', { + fsize: function(v) { return fsize(v); } + }) + }, { + xtype: 'tgrendercolumn', + header: _('Progress'), + width: 150, + dataIndex: 'progress', + renderer: function(v) { + var progress = v * 100; + return Deluge.progressBar(progress, this.col.width, progress.toFixed(2) + '%', 0); + } + }, { + header: _('Priority'), + width: 150, + dataIndex: 'priority', + tpl: new Ext.XTemplate('' + + '
' + + '{priority:this.getName}' + + '
', { + getClass: function(v) { + return FILE_PRIORITY_CSS[v]; + }, - getName: function(v) { - return _(FILE_PRIORITY[v]); - } - }) - }], - - selModel: new Ext.tree.MultiSelectionModel(), + getName: function(v) { + return _(FILE_PRIORITY[v]); + } + }) + }], + + selModel: new Ext.tree.MultiSelectionModel(), - initComponent: function() { - Deluge.details.FilesTab.superclass.initComponent.call(this); - this.setRootNode(new Ext.tree.TreeNode({text: 'Files'})); - }, + initComponent: function() { + Deluge.details.FilesTab.superclass.initComponent.call(this); + this.setRootNode(new Ext.tree.TreeNode({text: 'Files'})); + }, - clear: function() { - var root = this.getRootNode(); - if (!root.hasChildNodes()) return; - root.cascade(function(node) { - var parentNode = node.parentNode; - if (!parentNode) return; - if (!parentNode.ownerTree) return; - parentNode.removeChild(node); - }); - }, + clear: function() { + var root = this.getRootNode(); + if (!root.hasChildNodes()) return; + root.cascade(function(node) { + var parentNode = node.parentNode; + if (!parentNode) return; + if (!parentNode.ownerTree) return; + parentNode.removeChild(node); + }); + }, - createFileTree: function(files) { - function walk(files, parentNode) { - for (var file in files.contents) { - var item = files.contents[file]; - if (item.type == 'dir') { - walk(item, parentNode.appendChild(new Ext.tree.TreeNode({ - text: file, - filename: file, - size: item.size, - progress: item.progress, - priority: item.priority - }))); - } else { - parentNode.appendChild(new Ext.tree.TreeNode({ - text: file, - filename: file, - fileIndex: item.index, - size: item.size, - progress: item.progress, - priority: item.priority, - leaf: true, - iconCls: 'x-deluge-file', - uiProvider: Ext.ux.tree.TreeGridNodeUI - })); - } - } - } - var root = this.getRootNode(); - walk(files, root); - root.firstChild.expand(); - }, + createFileTree: function(files) { + function walk(files, parentNode) { + for (var file in files.contents) { + var item = files.contents[file]; + if (item.type == 'dir') { + walk(item, parentNode.appendChild(new Ext.tree.TreeNode({ + text: file, + filename: file, + size: item.size, + progress: item.progress, + priority: item.priority + }))); + } else { + parentNode.appendChild(new Ext.tree.TreeNode({ + text: file, + filename: file, + fileIndex: item.index, + size: item.size, + progress: item.progress, + priority: item.priority, + leaf: true, + iconCls: 'x-deluge-file', + uiProvider: Ext.ux.tree.TreeGridNodeUI + })); + } + } + } + var root = this.getRootNode(); + walk(files, root); + root.firstChild.expand(); + }, - update: function(torrentId) { - if (this.torrentId != torrentId) { - this.clear(); - this.torrentId = torrentId; - } - - deluge.client.web.get_torrent_files(torrentId, { - success: this.onRequestComplete, - scope: this, - torrentId: torrentId - }); - }, + update: function(torrentId) { + if (this.torrentId != torrentId) { + this.clear(); + this.torrentId = torrentId; + } + + deluge.client.web.get_torrent_files(torrentId, { + success: this.onRequestComplete, + scope: this, + torrentId: torrentId + }); + }, - updateFileTree: function(files) { - function walk(files, parentNode) { - for (var file in files.contents) { - var item = files.contents[file]; - var node = parentNode.findChild('filename', file); - node.attributes.size = item.size; - node.attributes.progress = item.progress; - node.attributes.priority = item.priority; - node.ui.updateColumns(); - if (item.type == 'dir') { - walk(item, node); - } - } - } - walk(files, this.getRootNode()); - }, + updateFileTree: function(files) { + function walk(files, parentNode) { + for (var file in files.contents) { + var item = files.contents[file]; + var node = parentNode.findChild('filename', file); + node.attributes.size = item.size; + node.attributes.progress = item.progress; + node.attributes.priority = item.priority; + node.ui.updateColumns(); + if (item.type == 'dir') { + walk(item, node); + } + } + } + walk(files, this.getRootNode()); + }, - onRender: function(ct, position) { - Deluge.details.FilesTab.superclass.onRender.call(this, ct, position); - deluge.menus.filePriorities.on('itemclick', this.onItemClick, this); - this.on('contextmenu', this.onContextMenu, this); - this.sorter = new Ext.tree.TreeSorter(this, { - folderSort: true - }); - }, - - onContextMenu: function(node, e) { - e.stopEvent(); - var selModel = this.getSelectionModel(); - if (selModel.getSelectedNodes().length < 2) { - selModel.clearSelections(); - node.select(); - } - deluge.menus.filePriorities.showAt(e.getPoint()); - }, - - onItemClick: function(baseItem, e) { - switch (baseItem.id) { - case 'expandAll': - this.expandAll(); - break; - default: - var indexes = {}; - function walk(node) { - if (Ext.isEmpty(node.attributes.fileIndex)) return; - indexes[node.attributes.fileIndex] = node.attributes.priority; - } - this.getRootNode().cascade(walk); + onRender: function(ct, position) { + Deluge.details.FilesTab.superclass.onRender.call(this, ct, position); + deluge.menus.filePriorities.on('itemclick', this.onItemClick, this); + this.on('contextmenu', this.onContextMenu, this); + this.sorter = new Ext.tree.TreeSorter(this, { + folderSort: true + }); + }, + + onContextMenu: function(node, e) { + e.stopEvent(); + var selModel = this.getSelectionModel(); + if (selModel.getSelectedNodes().length < 2) { + selModel.clearSelections(); + node.select(); + } + deluge.menus.filePriorities.showAt(e.getPoint()); + }, + + onItemClick: function(baseItem, e) { + switch (baseItem.id) { + case 'expandAll': + this.expandAll(); + break; + default: + var indexes = {}; + function walk(node) { + if (Ext.isEmpty(node.attributes.fileIndex)) return; + indexes[node.attributes.fileIndex] = node.attributes.priority; + } + this.getRootNode().cascade(walk); - var nodes = this.getSelectionModel().getSelectedNodes(); - Ext.each(nodes, function(node) { - if (!node.isLeaf()) { - function setPriorities(node) { - if (Ext.isEmpty(node.attributes.fileIndex)) return; - indexes[node.attributes.fileIndex] = baseItem.filePriority; - } - node.cascade(setPriorities); - } else if (!Ext.isEmpty(node.attributes.fileIndex)) { - indexes[node.attributes.fileIndex] = baseItem.filePriority; - return; - } - }); - - var priorities = new Array(Ext.keys(indexes).length); - for (var index in indexes) { - priorities[index] = indexes[index]; - } + var nodes = this.getSelectionModel().getSelectedNodes(); + Ext.each(nodes, function(node) { + if (!node.isLeaf()) { + function setPriorities(node) { + if (Ext.isEmpty(node.attributes.fileIndex)) return; + indexes[node.attributes.fileIndex] = baseItem.filePriority; + } + node.cascade(setPriorities); + } else if (!Ext.isEmpty(node.attributes.fileIndex)) { + indexes[node.attributes.fileIndex] = baseItem.filePriority; + return; + } + }); + + var priorities = new Array(Ext.keys(indexes).length); + for (var index in indexes) { + priorities[index] = indexes[index]; + } - deluge.client.core.set_torrent_file_priorities(this.torrentId, priorities, { - success: function() { - Ext.each(nodes, function(node) { - node.setColumnValue(3, baseItem.filePriority); - }); - }, - scope: this - }); - break; - } - }, - - onRequestComplete: function(files, options) { - if (!this.getRootNode().hasChildNodes()) { - this.createFileTree(files); - } else { - this.updateFileTree(files); - } - } + deluge.client.core.set_torrent_file_priorities(this.torrentId, priorities, { + success: function() { + Ext.each(nodes, function(node) { + node.setColumnValue(3, baseItem.filePriority); + }); + }, + scope: this + }); + break; + } + }, + + onRequestComplete: function(files, options) { + if (!this.getRootNode().hasChildNodes()) { + this.createFileTree(files); + } else { + this.updateFileTree(files); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/details/OptionsTab.js b/deluge/ui/web/js/deluge-all/details/OptionsTab.js index 97b410553..4a6294f80 100644 --- a/deluge/ui/web/js/deluge-all/details/OptionsTab.js +++ b/deluge/ui/web/js/deluge-all/details/OptionsTab.js @@ -1,6 +1,6 @@ /*! * Deluge.details.OptionsTab.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -33,383 +33,404 @@ Deluge.details.OptionsTab = Ext.extend(Ext.form.FormPanel, { - constructor: function(config) { - config = Ext.apply({ - autoScroll: true, - bodyStyle: 'padding: 5px;', - border: false, - cls: 'x-deluge-options', - defaults: { - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }, - deferredRender: false, - layout: 'column', - title: _('Options') - }, config); - Deluge.details.OptionsTab.superclass.constructor.call(this, config); - }, + constructor: function(config) { + config = Ext.apply({ + autoScroll: true, + bodyStyle: 'padding: 5px;', + border: false, + cls: 'x-deluge-options', + defaults: { + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }, + deferredRender: false, + layout: 'column', + title: _('Options') + }, config); + Deluge.details.OptionsTab.superclass.constructor.call(this, config); + }, - initComponent: function() { - Deluge.details.OptionsTab.superclass.initComponent.call(this); - - this.fieldsets = {}, this.fields = {}; - this.optionsManager = new Deluge.MultiOptionsManager({ - options: { - 'max_download_speed': -1, - 'max_upload_speed': -1, - 'max_connections': -1, - 'max_upload_slots': -1, - 'auto_managed': false, - 'stop_at_ratio': false, - 'stop_ratio': 2.0, - 'remove_at_ratio': false, - 'move_completed': null, - 'private': false, - 'prioritize_first_last': false - } - }); - - /* - * Bandwidth Options - */ - this.fieldsets.bandwidth = this.add({ - xtype: 'fieldset', - defaultType: 'spinnerfield', - bodyStyle: 'padding: 5px', - - layout: 'table', - layoutConfig: {columns: 3}, - labelWidth: 150, - - style: 'margin-left: 10px; margin-right: 5px; padding: 5px', - title: _('Bandwidth'), - width: 250 - }); - - /* - * Max Download Speed - */ - this.fieldsets.bandwidth.add({ - xtype: 'label', - text: _('Max Download Speed'), - forId: 'max_download_speed', - cls: 'x-deluge-options-label' - }); - this.fields.max_download_speed = this.fieldsets.bandwidth.add({ - id: 'max_download_speed', - name: 'max_download_speed', - width: 70, - strategy: { - xtype: 'number', - decimalPrecision: 1, - minValue: -1, - maxValue: 99999 - } - }); - this.fieldsets.bandwidth.add({ - xtype: 'label', - text: _('KiB/s'), - style: 'margin-left: 10px' - }); - - /* - * Max Upload Speed - */ - this.fieldsets.bandwidth.add({ - xtype: 'label', - text: _('Max Upload Speed'), - forId: 'max_upload_speed', - cls: 'x-deluge-options-label' - }); - this.fields.max_upload_speed = this.fieldsets.bandwidth.add({ - id: 'max_upload_speed', - name: 'max_upload_speed', - width: 70, - value: -1, - strategy: { - xtype: 'number', - decimalPrecision: 1, - minValue: -1, - maxValue: 99999 - } - }); - this.fieldsets.bandwidth.add({ - xtype: 'label', - text: _('KiB/s'), - style: 'margin-left: 10px' - }); - - /* - * Max Connections - */ - this.fieldsets.bandwidth.add({ - xtype: 'label', - text: _('Max Connections'), - forId: 'max_connections', - cls: 'x-deluge-options-label' - }); - this.fields.max_connections = this.fieldsets.bandwidth.add({ - id: 'max_connections', - name: 'max_connections', - width: 70, - value: -1, - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - }, - colspan: 2 - }); - - /* - * Max Upload Slots - */ - this.fieldsets.bandwidth.add({ - xtype: 'label', - text: _('Max Upload Slots'), - forId: 'max_upload_slots', - cls: 'x-deluge-options-label' - }); - this.fields.max_upload_slots = this.fieldsets.bandwidth.add({ - id: 'max_upload_slots', - name: 'max_upload_slots', - width: 70, - value: -1, - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - }, - colspan: 2 - }); + initComponent: function() { + Deluge.details.OptionsTab.superclass.initComponent.call(this); - /* - * Queue Options - */ - this.fieldsets.queue = this.add({ - xtype: 'fieldset', - title: _('Queue'), - style: 'margin-left: 5px; margin-right: 5px; padding: 5px', - width: 210, - - layout: 'table', - layoutConfig: {columns: 2}, - labelWidth: 0, - - defaults: { - fieldLabel: '', - labelSeparator: '' - } - }); - - this.fields.auto_managed = this.fieldsets.queue.add({ - xtype: 'checkbox', - fieldLabel: '', - labelSeparator: '', - name: 'is_auto_managed', - boxLabel: _('Auto Managed'), - width: 200, - colspan: 2 - }); - - this.fields.stop_at_ratio = this.fieldsets.queue.add({ - fieldLabel: '', - labelSeparator: '', - id: 'stop_at_ratio', - width: 120, - boxLabel: _('Stop seed at ratio'), - handler: this.onStopRatioChecked, - scope: this - }); - - this.fields.stop_ratio = this.fieldsets.queue.add({ - xtype: 'spinnerfield', - id: 'stop_ratio', - name: 'stop_ratio', - disabled: true, - width: 50, - value: 2.0, - strategy: { - xtype: 'number', - minValue: -1, - maxValue: 99999, - incrementValue: 0.1, - alternateIncrementValue: 1, - decimalPrecision: 1 - } - }); - - this.fields.remove_at_ratio = this.fieldsets.queue.add({ - fieldLabel: '', - labelSeparator: '', - id: 'remove_at_ratio', - ctCls: 'x-deluge-indent-checkbox', - bodyStyle: 'padding-left: 10px', - boxLabel: _('Remove at ratio'), - disabled: true, - colspan: 2 - }); - - this.fields.move_completed = this.fieldsets.queue.add({ - fieldLabel: '', - labelSeparator: '', - id: 'move_completed', - boxLabel: _('Move Completed'), - colspan: 2 - }); - - - /* - * General Options - */ - this.rightColumn = this.add({ - border: false, - autoHeight: true, - style: 'margin-left: 5px', - width: 210 - }); - - this.fieldsets.general = this.rightColumn.add({ - xtype: 'fieldset', - autoHeight: true, - defaultType: 'checkbox', - title: _('General'), - layout: 'form' - }); - - this.fields['private'] = this.fieldsets.general.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Private'), - id: 'private', - disabled: true - }); - - this.fields.prioritize_first_last = this.fieldsets.general.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Prioritize First/Last'), - id: 'prioritize_first_last' - }); - - // Bind the fields so the options manager can manage them. - for (var id in this.fields) { - this.optionsManager.bind(id, this.fields[id]); - } - - /* - * Buttons - */ - this.buttonPanel = this.rightColumn.add({ - layout: 'hbox', - xtype: 'panel', - border: false - }); - - /* - * Edit Trackers button - */ - this.buttonPanel.add({ - id: 'edit_trackers', - xtype: 'button', - text: _('Edit Trackers'), - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-edit-trackers', - border: false, - width: 100, - handler: this.onEditTrackers, - scope: this - }); - - /* - * Apply button - */ - this.buttonPanel.add({ - id: 'apply', - xtype: 'button', - text: _('Apply'), - style: 'margin-left: 10px;', - border: false, - width: 100, - handler: this.onApply, - scope: this - }); - }, - - onRender: function(ct, position) { - Deluge.details.OptionsTab.superclass.onRender.call(this, ct, position); - - // This is another hack I think, so keep an eye out here when upgrading. - this.layout = new Ext.layout.ColumnLayout(); - this.layout.setContainer(this); - this.doLayout(); - }, - - clear: function() { - if (this.torrentId == null) return; - this.torrentId = null; - this.optionsManager.changeId(null); - }, - - reset: function() { - if (this.torrentId) this.optionsManager.reset(); - }, - - update: function(torrentId) { - if (this.torrentId && !torrentId) this.clear(); // we want to clear the pane if we get a null torrent torrentIds - - if (!torrentId) return; // we don't care about null torrentIds - - if (this.torrentId != torrentId) { - this.torrentId = torrentId; - this.optionsManager.changeId(torrentId); - } - deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Options, { - success: this.onRequestComplete, - scope: this - }); - }, - - onApply: function() { - var changed = this.optionsManager.getDirty(); - if (!Ext.isEmpty(changed['prioritize_first_last'])) { - var value = changed['prioritize_first_last']; - deluge.client.core.set_torrent_prioritize_first_last(this.torrentId, value, { - success: function() { - this.optionsManager.set('prioritize_first_last', value); - }, - scope: this - }); - } - deluge.client.core.set_torrent_options([this.torrentId], changed, { - success: function() { - this.optionsManager.commit(); - }, - scope: this - }); - }, - - onEditTrackers: function() { - deluge.editTrackers.show(); - }, - - onStopRatioChecked: function(checkbox, checked) { - this.fields.remove_at_ratio.setDisabled(!checked); - this.fields.stop_ratio.setDisabled(!checked); - }, - - onRequestComplete: function(torrent, options) { - this.fields['private'].setValue(torrent['private']); - this.fields['private'].setDisabled(true); - delete torrent['private']; - torrent['auto_managed'] = torrent['is_auto_managed']; - this.optionsManager.setDefault(torrent); - var stop_at_ratio = this.optionsManager.get('stop_at_ratio'); - this.fields.remove_at_ratio.setDisabled(!stop_at_ratio); - this.fields.stop_ratio.setDisabled(!stop_at_ratio); - } + this.fieldsets = {}, this.fields = {}; + this.optionsManager = new Deluge.MultiOptionsManager({ + options: { + 'max_download_speed': -1, + 'max_upload_speed': -1, + 'max_connections': -1, + 'max_upload_slots': -1, + 'auto_managed': false, + 'stop_at_ratio': false, + 'stop_ratio': 2.0, + 'remove_at_ratio': false, + 'move_completed': false, + 'move_completed_path': '', + 'private': false, + 'prioritize_first_last': false + } + }); + + /* + * Bandwidth Options + */ + this.fieldsets.bandwidth = this.add({ + xtype: 'fieldset', + defaultType: 'spinnerfield', + bodyStyle: 'padding: 5px', + + layout: 'table', + layoutConfig: {columns: 3}, + labelWidth: 150, + + style: 'margin-left: 10px; margin-right: 5px; padding: 5px', + title: _('Bandwidth'), + width: 250 + }); + + /* + * Max Download Speed + */ + this.fieldsets.bandwidth.add({ + xtype: 'label', + text: _('Max Download Speed'), + forId: 'max_download_speed', + cls: 'x-deluge-options-label' + }); + this.fields.max_download_speed = this.fieldsets.bandwidth.add({ + id: 'max_download_speed', + name: 'max_download_speed', + width: 70, + strategy: { + xtype: 'number', + decimalPrecision: 1, + minValue: -1, + maxValue: 99999 + } + }); + this.fieldsets.bandwidth.add({ + xtype: 'label', + text: _('KiB/s'), + style: 'margin-left: 10px' + }); + + /* + * Max Upload Speed + */ + this.fieldsets.bandwidth.add({ + xtype: 'label', + text: _('Max Upload Speed'), + forId: 'max_upload_speed', + cls: 'x-deluge-options-label' + }); + this.fields.max_upload_speed = this.fieldsets.bandwidth.add({ + id: 'max_upload_speed', + name: 'max_upload_speed', + width: 70, + value: -1, + strategy: { + xtype: 'number', + decimalPrecision: 1, + minValue: -1, + maxValue: 99999 + } + }); + this.fieldsets.bandwidth.add({ + xtype: 'label', + text: _('KiB/s'), + style: 'margin-left: 10px' + }); + + /* + * Max Connections + */ + this.fieldsets.bandwidth.add({ + xtype: 'label', + text: _('Max Connections'), + forId: 'max_connections', + cls: 'x-deluge-options-label' + }); + this.fields.max_connections = this.fieldsets.bandwidth.add({ + id: 'max_connections', + name: 'max_connections', + width: 70, + value: -1, + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + }, + colspan: 2 + }); + + /* + * Max Upload Slots + */ + this.fieldsets.bandwidth.add({ + xtype: 'label', + text: _('Max Upload Slots'), + forId: 'max_upload_slots', + cls: 'x-deluge-options-label' + }); + this.fields.max_upload_slots = this.fieldsets.bandwidth.add({ + id: 'max_upload_slots', + name: 'max_upload_slots', + width: 70, + value: -1, + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + }, + colspan: 2 + }); + + /* + * Queue Options + */ + this.fieldsets.queue = this.add({ + xtype: 'fieldset', + title: _('Queue'), + style: 'margin-left: 5px; margin-right: 5px; padding: 5px', + width: 210, + + layout: 'table', + layoutConfig: {columns: 2}, + labelWidth: 0, + + defaults: { + fieldLabel: '', + labelSeparator: '' + } + }); + + this.fields.auto_managed = this.fieldsets.queue.add({ + xtype: 'checkbox', + fieldLabel: '', + labelSeparator: '', + name: 'is_auto_managed', + boxLabel: _('Auto Managed'), + width: 200, + colspan: 2 + }); + + this.fields.stop_at_ratio = this.fieldsets.queue.add({ + fieldLabel: '', + labelSeparator: '', + id: 'stop_at_ratio', + width: 120, + boxLabel: _('Stop seed at ratio'), + handler: this.onStopRatioChecked, + scope: this + }); + + this.fields.stop_ratio = this.fieldsets.queue.add({ + xtype: 'spinnerfield', + id: 'stop_ratio', + name: 'stop_ratio', + disabled: true, + width: 50, + value: 2.0, + strategy: { + xtype: 'number', + minValue: -1, + maxValue: 99999, + incrementValue: 0.1, + alternateIncrementValue: 1, + decimalPrecision: 1 + } + }); + + this.fields.remove_at_ratio = this.fieldsets.queue.add({ + fieldLabel: '', + labelSeparator: '', + id: 'remove_at_ratio', + ctCls: 'x-deluge-indent-checkbox', + bodyStyle: 'padding-left: 10px', + boxLabel: _('Remove at ratio'), + disabled: true, + colspan: 2 + }); + + this.fields.move_completed = this.fieldsets.queue.add({ + fieldLabel: '', + labelSeparator: '', + id: 'move_completed', + boxLabel: _('Move Completed'), + colspan: 2, + handler: this.onMoveCompletedChecked, + scope: this + }); + + this.fields.move_completed_path = this.fieldsets.queue.add({ + xtype: 'textfield', + fieldLabel: '', + id: 'move_completed_path', + colspan: 3, + bodyStyle: 'margin-left: 20px', + width: 180, + disabled: true + }); + + + /* + * General Options + */ + this.rightColumn = this.add({ + border: false, + autoHeight: true, + style: 'margin-left: 5px', + width: 210 + }); + + this.fieldsets.general = this.rightColumn.add({ + xtype: 'fieldset', + autoHeight: true, + defaultType: 'checkbox', + title: _('General'), + layout: 'form' + }); + + this.fields['private'] = this.fieldsets.general.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Private'), + id: 'private', + disabled: true + }); + + this.fields.prioritize_first_last = this.fieldsets.general.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Prioritize First/Last'), + id: 'prioritize_first_last' + }); + + // Bind the fields so the options manager can manage them. + for (var id in this.fields) { + this.optionsManager.bind(id, this.fields[id]); + } + + /* + * Buttons + */ + this.buttonPanel = this.rightColumn.add({ + layout: 'hbox', + xtype: 'panel', + border: false + }); + + /* + * Edit Trackers button + */ + this.buttonPanel.add({ + id: 'edit_trackers', + xtype: 'button', + text: _('Edit Trackers'), + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-edit-trackers', + border: false, + width: 100, + handler: this.onEditTrackers, + scope: this + }); + + /* + * Apply button + */ + this.buttonPanel.add({ + id: 'apply', + xtype: 'button', + text: _('Apply'), + style: 'margin-left: 10px;', + border: false, + width: 100, + handler: this.onApply, + scope: this + }); + }, + + onRender: function(ct, position) { + Deluge.details.OptionsTab.superclass.onRender.call(this, ct, position); + + // This is another hack I think, so keep an eye out here when upgrading. + this.layout = new Ext.layout.ColumnLayout(); + this.layout.setContainer(this); + this.doLayout(); + }, + + clear: function() { + if (this.torrentId == null) return; + this.torrentId = null; + this.optionsManager.changeId(null); + }, + + reset: function() { + if (this.torrentId) this.optionsManager.reset(); + }, + + update: function(torrentId) { + if (this.torrentId && !torrentId) this.clear(); // we want to clear the pane if we get a null torrent torrentIds + + if (!torrentId) return; // we don't care about null torrentIds + + if (this.torrentId != torrentId) { + this.torrentId = torrentId; + this.optionsManager.changeId(torrentId); + } + deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Options, { + success: this.onRequestComplete, + scope: this + }); + }, + + onApply: function() { + var changed = this.optionsManager.getDirty(); + if (!Ext.isEmpty(changed['prioritize_first_last'])) { + var value = changed['prioritize_first_last']; + deluge.client.core.set_torrent_prioritize_first_last(this.torrentId, value, { + success: function() { + this.optionsManager.set('prioritize_first_last', value); + }, + scope: this + }); + } + deluge.client.core.set_torrent_options([this.torrentId], changed, { + success: function() { + this.optionsManager.commit(); + }, + scope: this + }); + }, + + onEditTrackers: function() { + deluge.editTrackers.show(); + }, + + onMoveCompletedChecked: function(checkbox, checked) { + this.fields.move_completed_path.setDisabled(!checked); + + if (!checked) return; + this.fields.move_completed_path.focus(); + }, + + onStopRatioChecked: function(checkbox, checked) { + this.fields.remove_at_ratio.setDisabled(!checked); + this.fields.stop_ratio.setDisabled(!checked); + }, + + onRequestComplete: function(torrent, options) { + this.fields['private'].setValue(torrent['private']); + this.fields['private'].setDisabled(true); + delete torrent['private']; + torrent['auto_managed'] = torrent['is_auto_managed']; + this.optionsManager.setDefault(torrent); + var stop_at_ratio = this.optionsManager.get('stop_at_ratio'); + this.fields.remove_at_ratio.setDisabled(!stop_at_ratio); + this.fields.stop_ratio.setDisabled(!stop_at_ratio); + this.fields.move_completed_path.setDisabled(!this.optionsManager.get('move_completed')); + } }); diff --git a/deluge/ui/web/js/deluge-all/details/PeersTab.js b/deluge/ui/web/js/deluge-all/details/PeersTab.js index 6ddc8d057..44a4daa35 100644 --- a/deluge/ui/web/js/deluge-all/details/PeersTab.js +++ b/deluge/ui/web/js/deluge-all/details/PeersTab.js @@ -1,6 +1,6 @@ /*! * Deluge.details.PeersTab.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -31,120 +31,130 @@ */ (function() { - function flagRenderer(value) { - if (!value) return ''; - return String.format('', value); - } - function peerAddressRenderer(value, p, record) { - var seed = (record.data['seed'] == 1024) ? 'x-deluge-seed' : 'x-deluge-peer' - return String.format('
{1}
', seed, value); - } - function peerProgressRenderer(value) { - var progress = (value * 100).toFixed(0); - return Deluge.progressBar(progress, this.width - 8, progress + '%'); - } + function flagRenderer(value) { + if (!value.replace(' ', '').replace(' ', '')){ + return ''; + } + return String.format('', value); + } + function peerAddressRenderer(value, p, record) { + var seed = (record.data['seed'] == 1024) ? 'x-deluge-seed' : 'x-deluge-peer' + return String.format('
{1}
', seed, value); + } + function peerProgressRenderer(value) { + var progress = (value * 100).toFixed(0); + return Deluge.progressBar(progress, this.width - 8, progress + '%'); + } - Deluge.details.PeersTab = Ext.extend(Ext.grid.GridPanel, { - - // fast way to figure out if we have a peer already. - peers: {}, - - constructor: function(config) { - config = Ext.apply({ - title: _('Peers'), - cls: 'x-deluge-peers', - store: new Ext.data.Store({ - reader: new Ext.data.JsonReader({ - idProperty: 'ip', - root: 'peers' - }, Deluge.data.Peer) - }), - columns: [{ - header: ' ', - width: 30, - sortable: true, - renderer: flagRenderer, - dataIndex: 'country' - }, { - header: 'Address', - width: 125, - sortable: true, - renderer: peerAddressRenderer, - dataIndex: 'ip' - }, { - header: 'Client', - width: 125, - sortable: true, - renderer: fplain, - dataIndex: 'client' - }, { - header: 'Progress', - width: 150, - sortable: true, - renderer: peerProgressRenderer, - dataIndex: 'progress' - }, { - header: 'Down Speed', - width: 100, - sortable: true, - renderer: fspeed, - dataIndex: 'down_speed' - }, { - header: 'Up Speed', - width: 100, - sortable: true, - renderer: fspeed, - dataIndex: 'up_speed' - }], - stripeRows: true, - deferredRender:false, - autoScroll:true - }, config); - Deluge.details.PeersTab.superclass.constructor.call(this, config); - }, - - clear: function() { - this.getStore().removeAll(); - this.peers = {}; - }, - - update: function(torrentId) { - deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Peers, { - success: this.onRequestComplete, - scope: this - }); - }, - - onRequestComplete: function(torrent, options) { - if (!torrent) return; + Deluge.details.PeersTab = Ext.extend(Ext.grid.GridPanel, { - var store = this.getStore(); - var newPeers = []; - var addresses = {}; + // fast way to figure out if we have a peer already. + peers: {}, - // Go through the peers updating and creating peer records - Ext.each(torrent.peers, function(peer) { - if (this.peers[peer.ip]) { - } else { - this.peers[peer.ip] = 1; - newPeers.push(new Deluge.data.Peer(peer, peer.ip)); - } - addresses[peer.ip] = 1; - }, this); - store.add(newPeers); + constructor: function(config) { + config = Ext.apply({ + title: _('Peers'), + cls: 'x-deluge-peers', + store: new Ext.data.Store({ + reader: new Ext.data.JsonReader({ + idProperty: 'ip', + root: 'peers' + }, Deluge.data.Peer) + }), + columns: [{ + header: ' ', + width: 30, + sortable: true, + renderer: flagRenderer, + dataIndex: 'country' + }, { + header: 'Address', + width: 125, + sortable: true, + renderer: peerAddressRenderer, + dataIndex: 'ip' + }, { + header: 'Client', + width: 125, + sortable: true, + renderer: fplain, + dataIndex: 'client' + }, { + header: 'Progress', + width: 150, + sortable: true, + renderer: peerProgressRenderer, + dataIndex: 'progress' + }, { + header: 'Down Speed', + width: 100, + sortable: true, + renderer: fspeed, + dataIndex: 'down_speed' + }, { + header: 'Up Speed', + width: 100, + sortable: true, + renderer: fspeed, + dataIndex: 'up_speed' + }], + stripeRows: true, + deferredRender:false, + autoScroll:true + }, config); + Deluge.details.PeersTab.superclass.constructor.call(this, config); + }, - // Remove any peers that shouldn't be left in the store - store.each(function(record) { - if (!addresses[record.id]) { - store.remove(record); - delete this.peers[record.id]; - } - }, this); - store.commitChanges(); + clear: function() { + this.getStore().removeAll(); + this.peers = {}; + }, - var sortState = store.getSortState(); - if (!sortState) return; - store.sort(sortState.field, sortState.direction); - } - }); + update: function(torrentId) { + deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Peers, { + success: this.onRequestComplete, + scope: this + }); + }, + + onRequestComplete: function(torrent, options) { + if (!torrent) return; + + var store = this.getStore(); + var newPeers = []; + var addresses = {}; + + // Go through the peers updating and creating peer records + Ext.each(torrent.peers, function(peer) { + if (this.peers[peer.ip]) { + var record = store.getById(peer.ip); + record.beginEdit(); + for (var k in peer) { + if (record.get(k) != peer[k]) { + record.set(k, peer[k]); + } + } + record.endEdit(); + } else { + this.peers[peer.ip] = 1; + newPeers.push(new Deluge.data.Peer(peer, peer.ip)); + } + addresses[peer.ip] = 1; + }, this); + store.add(newPeers); + + // Remove any peers that shouldn't be left in the store + store.each(function(record) { + if (!addresses[record.id]) { + store.remove(record); + delete this.peers[record.id]; + } + }, this); + store.commitChanges(); + + var sortState = store.getSortState(); + if (!sortState) return; + store.sort(sortState.field, sortState.direction); + } + }); })(); diff --git a/deluge/ui/web/js/deluge-all/details/StatusTab.js b/deluge/ui/web/js/deluge-all/details/StatusTab.js index ba99a2a5d..e3671bd0e 100644 --- a/deluge/ui/web/js/deluge-all/details/StatusTab.js +++ b/deluge/ui/web/js/deluge-all/details/StatusTab.js @@ -36,90 +36,90 @@ Ext.ns('Deluge.details'); * @extends Ext.Panel */ Deluge.details.StatusTab = Ext.extend(Ext.Panel, { - title: _('Status'), - autoScroll: true, - - onRender: function(ct, position) { - Deluge.details.StatusTab.superclass.onRender.call(this, ct, position); - - this.progressBar = this.add({ - xtype: 'progress', - cls: 'x-deluge-status-progressbar' - }); - - this.status = this.add({ - cls: 'x-deluge-status', - id: 'deluge-details-status', - - border: false, - width: 1000, - listeners: { - 'render': { - fn: function(panel) { - panel.load({ - url: deluge.config.base + 'render/tab_status.html', - text: _('Loading') + '...' - }); - panel.getUpdater().on('update', this.onPanelUpdate, this); - }, - scope: this - } - } - }); - }, - - clear: function() { - this.progressBar.updateProgress(0, ' '); - for (var k in this.fields) { - this.fields[k].innerHTML = ''; - } - }, - - update: function(torrentId) { - if (!this.fields) this.getFields(); - deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Status, { - success: this.onRequestComplete, - scope: this - }); - }, - - onPanelUpdate: function(el, response) { - this.fields = {}; - Ext.each(Ext.query('dd', this.status.body.dom), function(field) { - this.fields[field.className] = field; - }, this); - }, - - onRequestComplete: function(status) { - seeders = status.total_seeds > -1 ? status.num_seeds + ' (' + status.total_seeds + ')' : status.num_seeds - peers = status.total_peers > -1 ? status.num_peers + ' (' + status.total_peers + ')' : status.num_peers - var data = { - downloaded: fsize(status.total_done, true), - uploaded: fsize(status.total_uploaded, true), - share: (status.ratio == -1) ? '∞' : status.ratio.toFixed(3), - announce: ftime(status.next_announce), - tracker_status: status.tracker_status, - downspeed: (status.download_payload_rate) ? fspeed(status.download_payload_rate) : '0.0 KiB/s', - upspeed: (status.upload_payload_rate) ? fspeed(status.upload_payload_rate) : '0.0 KiB/s', - eta: ftime(status.eta), - pieces: status.num_pieces + ' (' + fsize(status.piece_length) + ')', - seeders: seeders, - peers: peers, - avail: status.distributed_copies.toFixed(3), - active_time: ftime(status.active_time), - seeding_time: ftime(status.seeding_time), - seed_rank: status.seed_rank, - time_added: fdate(status.time_added) - } - data.auto_managed = _((status.is_auto_managed) ? 'True' : 'False'); + title: _('Status'), + autoScroll: true, + + onRender: function(ct, position) { + Deluge.details.StatusTab.superclass.onRender.call(this, ct, position); + + this.progressBar = this.add({ + xtype: 'progress', + cls: 'x-deluge-status-progressbar' + }); + + this.status = this.add({ + cls: 'x-deluge-status', + id: 'deluge-details-status', + + border: false, + width: 1000, + listeners: { + 'render': { + fn: function(panel) { + panel.load({ + url: deluge.config.base + 'render/tab_status.html', + text: _('Loading') + '...' + }); + panel.getUpdater().on('update', this.onPanelUpdate, this); + }, + scope: this + } + } + }); + }, + + clear: function() { + this.progressBar.updateProgress(0, ' '); + for (var k in this.fields) { + this.fields[k].innerHTML = ''; + } + }, + + update: function(torrentId) { + if (!this.fields) this.getFields(); + deluge.client.web.get_torrent_status(torrentId, Deluge.Keys.Status, { + success: this.onRequestComplete, + scope: this + }); + }, + + onPanelUpdate: function(el, response) { + this.fields = {}; + Ext.each(Ext.query('dd', this.status.body.dom), function(field) { + this.fields[field.className] = field; + }, this); + }, + + onRequestComplete: function(status) { + seeders = status.total_seeds > -1 ? status.num_seeds + ' (' + status.total_seeds + ')' : status.num_seeds + peers = status.total_peers > -1 ? status.num_peers + ' (' + status.total_peers + ')' : status.num_peers + var data = { + downloaded: fsize(status.total_done, true), + uploaded: fsize(status.total_uploaded, true), + share: (status.ratio == -1) ? '∞' : status.ratio.toFixed(3), + announce: ftime(status.next_announce), + tracker_status: status.tracker_status, + downspeed: (status.download_payload_rate) ? fspeed(status.download_payload_rate) : '0.0 KiB/s', + upspeed: (status.upload_payload_rate) ? fspeed(status.upload_payload_rate) : '0.0 KiB/s', + eta: ftime(status.eta), + pieces: status.num_pieces + ' (' + fsize(status.piece_length) + ')', + seeders: seeders, + peers: peers, + avail: status.distributed_copies.toFixed(3), + active_time: ftime(status.active_time), + seeding_time: ftime(status.seeding_time), + seed_rank: status.seed_rank, + time_added: fdate(status.time_added) + } + data.auto_managed = _((status.is_auto_managed) ? 'True' : 'False'); - data.downloaded += ' (' + ((status.total_payload_download) ? fsize(status.total_payload_download) : '0.0 KiB') + ')'; - data.uploaded += ' (' + ((status.total_payload_download) ? fsize(status.total_payload_download): '0.0 KiB') + ')'; - - for (var field in this.fields) { - this.fields[field].innerHTML = data[field]; - } - var text = status.state + ' ' + status.progress.toFixed(2) + '%'; - this.progressBar.updateProgress(status.progress / 100.0, text); - } + data.downloaded += ' (' + ((status.total_payload_download) ? fsize(status.total_payload_download) : '0.0 KiB') + ')'; + data.uploaded += ' (' + ((status.total_payload_download) ? fsize(status.total_payload_download): '0.0 KiB') + ')'; + + for (var field in this.fields) { + this.fields[field].innerHTML = data[field]; + } + var text = status.state + ' ' + status.progress.toFixed(2) + '%'; + this.progressBar.updateProgress(status.progress / 100.0, text); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/BandwidthPage.js b/deluge/ui/web/js/deluge-all/preferences/BandwidthPage.js index 5b5c1b0f9..78d02dffc 100644 --- a/deluge/ui/web/js/deluge-all/preferences/BandwidthPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/BandwidthPage.js @@ -36,139 +36,139 @@ Ext.namespace('Deluge.preferences'); * @extends Ext.form.FormPanel */ Deluge.preferences.Bandwidth = Ext.extend(Ext.form.FormPanel, { - constructor: function(config) { - config = Ext.apply({ - border: false, - title: _('Bandwidth'), - layout: 'form', - labelWidth: 10 - }, config); - Deluge.preferences.Bandwidth.superclass.constructor.call(this, config); - }, - - initComponent: function() { - Deluge.preferences.Bandwidth.superclass.initComponent.call(this); - - var om = deluge.preferences.getOptionsManager(); - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Global Bandwidth Usage'), - labelWidth: 200, - defaultType: 'spinnerfield', - defaults: { - minValue: -1, - maxValue: 99999 - }, - style: 'margin-bottom: 0px; padding-bottom: 0px;', - autoHeight: true - }); - om.bind('max_connections_global', fieldset.add({ - name: 'max_connections_global', - fieldLabel: _('Maximum Connections'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - om.bind('max_upload_slots_global', fieldset.add({ - name: 'max_upload_slots_global', - fieldLabel: _('Maximum Upload Slots'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - om.bind('max_download_speed', fieldset.add({ - name: 'max_download_speed', - fieldLabel: _('Maximum Download Speed (KiB/s)'), - width: 80, - value: -1.0, - decimalPrecision: 1 - })); - om.bind('max_upload_speed', fieldset.add({ - name: 'max_upload_speed', - fieldLabel: _('Maximum Upload Speed (KiB/s)'), - width: 80, - value: -1.0, - decimalPrecision: 1 - })); - om.bind('max_half_open_connections', fieldset.add({ - name: 'max_half_open_connections', - fieldLabel: _('Maximum Half-Open Connections'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - om.bind('max_connections_per_second', fieldset.add({ - name: 'max_connections_per_second', - fieldLabel: _('Maximum Connection Attempts per Second'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: '', - defaultType: 'checkbox', - style: 'padding-top: 0px; padding-bottom: 5px; margin-top: 0px; margin-bottom: 0px;', - autoHeight: true - }); - om.bind('ignore_limits_on_local_network', fieldset.add({ - name: 'ignore_limits_on_local_network', - height: 22, - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Ignore limits on local network') - })); - om.bind('rate_limit_ip_overhead', fieldset.add({ - name: 'rate_limit_ip_overhead', - height: 22, - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Rate limit IP overhead') - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Per Torrent Bandwidth Usage'), - style: 'margin-bottom: 0px; padding-bottom: 0px;', - defaultType: 'spinnerfield', - labelWidth: 200, - defaults: { - minValue: -1, - maxValue: 99999 - }, - autoHeight: true - }); - om.bind('max_connections_per_torrent', fieldset.add({ - name: 'max_connections_per_torrent', - fieldLabel: _('Maximum Connections'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - om.bind('max_upload_slots_per_torrent', fieldset.add({ - name: 'max_upload_slots_per_torrent', - fieldLabel: _('Maximum Upload Slots'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - om.bind('max_download_speed_per_torrent', fieldset.add({ - name: 'max_download_speed_per_torrent', - fieldLabel: _('Maximum Download Speed (KiB/s)'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - om.bind('max_upload_speed_per_torrent', fieldset.add({ - name: 'max_upload_speed_per_torrent', - fieldLabel: _('Maximum Upload Speed (KiB/s)'), - width: 80, - value: -1, - decimalPrecision: 0 - })); - } + constructor: function(config) { + config = Ext.apply({ + border: false, + title: _('Bandwidth'), + layout: 'form', + labelWidth: 10 + }, config); + Deluge.preferences.Bandwidth.superclass.constructor.call(this, config); + }, + + initComponent: function() { + Deluge.preferences.Bandwidth.superclass.initComponent.call(this); + + var om = deluge.preferences.getOptionsManager(); + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Global Bandwidth Usage'), + labelWidth: 200, + defaultType: 'spinnerfield', + defaults: { + minValue: -1, + maxValue: 99999 + }, + style: 'margin-bottom: 0px; padding-bottom: 0px;', + autoHeight: true + }); + om.bind('max_connections_global', fieldset.add({ + name: 'max_connections_global', + fieldLabel: _('Maximum Connections'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + om.bind('max_upload_slots_global', fieldset.add({ + name: 'max_upload_slots_global', + fieldLabel: _('Maximum Upload Slots'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + om.bind('max_download_speed', fieldset.add({ + name: 'max_download_speed', + fieldLabel: _('Maximum Download Speed (KiB/s)'), + width: 80, + value: -1.0, + decimalPrecision: 1 + })); + om.bind('max_upload_speed', fieldset.add({ + name: 'max_upload_speed', + fieldLabel: _('Maximum Upload Speed (KiB/s)'), + width: 80, + value: -1.0, + decimalPrecision: 1 + })); + om.bind('max_half_open_connections', fieldset.add({ + name: 'max_half_open_connections', + fieldLabel: _('Maximum Half-Open Connections'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + om.bind('max_connections_per_second', fieldset.add({ + name: 'max_connections_per_second', + fieldLabel: _('Maximum Connection Attempts per Second'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: '', + defaultType: 'checkbox', + style: 'padding-top: 0px; padding-bottom: 5px; margin-top: 0px; margin-bottom: 0px;', + autoHeight: true + }); + om.bind('ignore_limits_on_local_network', fieldset.add({ + name: 'ignore_limits_on_local_network', + height: 22, + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Ignore limits on local network') + })); + om.bind('rate_limit_ip_overhead', fieldset.add({ + name: 'rate_limit_ip_overhead', + height: 22, + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Rate limit IP overhead') + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Per Torrent Bandwidth Usage'), + style: 'margin-bottom: 0px; padding-bottom: 0px;', + defaultType: 'spinnerfield', + labelWidth: 200, + defaults: { + minValue: -1, + maxValue: 99999 + }, + autoHeight: true + }); + om.bind('max_connections_per_torrent', fieldset.add({ + name: 'max_connections_per_torrent', + fieldLabel: _('Maximum Connections'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + om.bind('max_upload_slots_per_torrent', fieldset.add({ + name: 'max_upload_slots_per_torrent', + fieldLabel: _('Maximum Upload Slots'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + om.bind('max_download_speed_per_torrent', fieldset.add({ + name: 'max_download_speed_per_torrent', + fieldLabel: _('Maximum Download Speed (KiB/s)'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + om.bind('max_upload_speed_per_torrent', fieldset.add({ + name: 'max_upload_speed_per_torrent', + fieldLabel: _('Maximum Upload Speed (KiB/s)'), + width: 80, + value: -1, + decimalPrecision: 0 + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/CachePage.js b/deluge/ui/web/js/deluge-all/preferences/CachePage.js index c673ed7ff..4afec2e6a 100644 --- a/deluge/ui/web/js/deluge-all/preferences/CachePage.js +++ b/deluge/ui/web/js/deluge-all/preferences/CachePage.js @@ -37,39 +37,39 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.Cache = Ext.extend(Ext.form.FormPanel, { - border: false, - title: _('Cache'), - layout: 'form', - - initComponent: function() { - Deluge.preferences.Cache.superclass.initComponent.call(this); + border: false, + title: _('Cache'), + layout: 'form', + + initComponent: function() { + Deluge.preferences.Cache.superclass.initComponent.call(this); - var om = deluge.preferences.getOptionsManager(); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Settings'), - autoHeight: true, - labelWidth: 180, - defaultType: 'spinnerfield', - defaults: { - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - } - }); - om.bind('cache_size', fieldset.add({ - fieldLabel: _('Cache Size (16 KiB Blocks)'), - name: 'cache_size', - width: 60, - value: 512 - })); - om.bind('cache_expiry', fieldset.add({ - fieldLabel: _('Cache Expiry (seconds)'), - name: 'cache_expiry', - width: 60, - value: 60 - })); - } + var om = deluge.preferences.getOptionsManager(); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Settings'), + autoHeight: true, + labelWidth: 180, + defaultType: 'spinnerfield', + defaults: { + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + } + }); + om.bind('cache_size', fieldset.add({ + fieldLabel: _('Cache Size (16 KiB Blocks)'), + name: 'cache_size', + width: 60, + value: 512 + })); + om.bind('cache_expiry', fieldset.add({ + fieldLabel: _('Cache Expiry (seconds)'), + name: 'cache_expiry', + width: 60, + value: 60 + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/DaemonPage.js b/deluge/ui/web/js/deluge-all/preferences/DaemonPage.js index 621c28a0f..2a344d9c0 100644 --- a/deluge/ui/web/js/deluge-all/preferences/DaemonPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/DaemonPage.js @@ -37,61 +37,61 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.Daemon = Ext.extend(Ext.form.FormPanel, { - border: false, - title: _('Daemon'), - layout: 'form', - - initComponent: function() { - Deluge.preferences.Daemon.superclass.initComponent.call(this); + border: false, + title: _('Daemon'), + layout: 'form', + + initComponent: function() { + Deluge.preferences.Daemon.superclass.initComponent.call(this); - var om = deluge.preferences.getOptionsManager(); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Port'), - autoHeight: true, - defaultType: 'spinnerfield' - }); - om.bind('daemon_port', fieldset.add({ - fieldLabel: _('Daemon port'), - name: 'daemon_port', - value: 58846, - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Connections'), - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - om.bind('allow_remote', fieldset.add({ - fieldLabel: '', - height: 22, - labelSeparator: '', - boxLabel: _('Allow Remote Connections'), - name: 'allow_remote' - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Other'), - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - om.bind('new_release_check', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - height: 40, - boxLabel: _('Periodically check the website for new releases'), - id: 'new_release_check' - })); - } + var om = deluge.preferences.getOptionsManager(); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Port'), + autoHeight: true, + defaultType: 'spinnerfield' + }); + om.bind('daemon_port', fieldset.add({ + fieldLabel: _('Daemon port'), + name: 'daemon_port', + value: 58846, + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Connections'), + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + om.bind('allow_remote', fieldset.add({ + fieldLabel: '', + height: 22, + labelSeparator: '', + boxLabel: _('Allow Remote Connections'), + name: 'allow_remote' + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Other'), + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + om.bind('new_release_check', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + height: 40, + boxLabel: _('Periodically check the website for new releases'), + id: 'new_release_check' + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/DownloadsPage.js b/deluge/ui/web/js/deluge-all/preferences/DownloadsPage.js index 603b58d29..6984db040 100644 --- a/deluge/ui/web/js/deluge-all/preferences/DownloadsPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/DownloadsPage.js @@ -36,114 +36,114 @@ Ext.namespace('Deluge.preferences'); * @extends Ext.form.FormPanel */ Deluge.preferences.Downloads = Ext.extend(Ext.FormPanel, { - constructor: function(config) { - config = Ext.apply({ - border: false, - title: _('Downloads'), - layout: 'form', - autoHeight: true, - width: 320 - }, config); - Deluge.preferences.Downloads.superclass.constructor.call(this, config); - }, + constructor: function(config) { + config = Ext.apply({ + border: false, + title: _('Downloads'), + layout: 'form', + autoHeight: true, + width: 320 + }, config); + Deluge.preferences.Downloads.superclass.constructor.call(this, config); + }, - initComponent: function() { - Deluge.preferences.Downloads.superclass.initComponent.call(this); + initComponent: function() { + Deluge.preferences.Downloads.superclass.initComponent.call(this); - var optMan = deluge.preferences.getOptionsManager(); - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Folders'), - labelWidth: 150, - defaultType: 'togglefield', - autoHeight: true, - labelAlign: 'top', - width: 300, - style: 'margin-bottom: 5px; padding-bottom: 5px;' - }); + var optMan = deluge.preferences.getOptionsManager(); + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Folders'), + labelWidth: 150, + defaultType: 'togglefield', + autoHeight: true, + labelAlign: 'top', + width: 300, + style: 'margin-bottom: 5px; padding-bottom: 5px;' + }); - optMan.bind('download_location', fieldset.add({ - xtype: 'textfield', - name: 'download_location', - fieldLabel: _('Download to'), - width: 280 - })); + optMan.bind('download_location', fieldset.add({ + xtype: 'textfield', + name: 'download_location', + fieldLabel: _('Download to'), + width: 280 + })); - var field = fieldset.add({ - name: 'move_completed_path', - fieldLabel: _('Move completed to'), - width: 280 - }); - optMan.bind('move_completed', field.toggle); - optMan.bind('move_completed_path', field.input); + var field = fieldset.add({ + name: 'move_completed_path', + fieldLabel: _('Move completed to'), + width: 280 + }); + optMan.bind('move_completed', field.toggle); + optMan.bind('move_completed_path', field.input); - field = fieldset.add({ - name: 'torrentfiles_location', - fieldLabel: _('Copy of .torrent files to'), - width: 280 - }); - optMan.bind('copy_torrent_file', field.toggle); - optMan.bind('torrentfiles_location', field.input); + field = fieldset.add({ + name: 'torrentfiles_location', + fieldLabel: _('Copy of .torrent files to'), + width: 280 + }); + optMan.bind('copy_torrent_file', field.toggle); + optMan.bind('torrentfiles_location', field.input); - field = fieldset.add({ - name: 'autoadd_location', - fieldLabel: _('Autoadd .torrent files from'), - width: 280 - }); - optMan.bind('autoadd_enable', field.toggle); - optMan.bind('autoadd_location', field.input); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Allocation'), - autoHeight: true, - labelWidth: 1, - defaultType: 'radiogroup', - style: 'margin-bottom: 5px; margin-top: 0; padding-bottom: 5px; padding-top: 0;', - width: 240 - }); - optMan.bind('compact_allocation', fieldset.add({ - name: 'compact_allocation', - width: 200, - labelSeparator: '', - //disabled: true, - defaults: { - width: 80, - height: 22, - name: 'compact_allocation' - }, - items: [{ - boxLabel: _('Use Full'), - inputValue: false - }, { - boxLabel: _('Use Compact'), - inputValue: true - }] - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Options'), - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox', - style: 'margin-bottom: 0; padding-bottom: 0;', - width: 280 - }); - optMan.bind('prioritize_first_last_pieces', fieldset.add({ - name: 'prioritize_first_last_pieces', - labelSeparator: '', - height: 22, - boxLabel: _('Prioritize first and last pieces of torrent') - })); - optMan.bind('add_paused', fieldset.add({ - name: 'add_paused', - labelSeparator: '', - height: 22, - boxLabel: _('Add torrents in Paused state') - })); - } + field = fieldset.add({ + name: 'autoadd_location', + fieldLabel: _('Autoadd .torrent files from'), + width: 280 + }); + optMan.bind('autoadd_enable', field.toggle); + optMan.bind('autoadd_location', field.input); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Allocation'), + autoHeight: true, + labelWidth: 1, + defaultType: 'radiogroup', + style: 'margin-bottom: 5px; margin-top: 0; padding-bottom: 5px; padding-top: 0;', + width: 240 + }); + optMan.bind('compact_allocation', fieldset.add({ + name: 'compact_allocation', + width: 200, + labelSeparator: '', + //disabled: true, + defaults: { + width: 80, + height: 22, + name: 'compact_allocation' + }, + items: [{ + boxLabel: _('Use Full'), + inputValue: false + }, { + boxLabel: _('Use Compact'), + inputValue: true + }] + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Options'), + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox', + style: 'margin-bottom: 0; padding-bottom: 0;', + width: 280 + }); + optMan.bind('prioritize_first_last_pieces', fieldset.add({ + name: 'prioritize_first_last_pieces', + labelSeparator: '', + height: 22, + boxLabel: _('Prioritize first and last pieces of torrent') + })); + optMan.bind('add_paused', fieldset.add({ + name: 'add_paused', + labelSeparator: '', + height: 22, + boxLabel: _('Add torrents in Paused state') + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/EncryptionPage.js b/deluge/ui/web/js/deluge-all/preferences/EncryptionPage.js index af945c5c3..a259529d3 100644 --- a/deluge/ui/web/js/deluge-all/preferences/EncryptionPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/EncryptionPage.js @@ -37,79 +37,79 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.Encryption = Ext.extend(Ext.form.FormPanel, { - border: false, - title: _('Encryption'), - - initComponent: function() { - Deluge.preferences.Encryption.superclass.initComponent.call(this); + border: false, + title: _('Encryption'), + + initComponent: function() { + Deluge.preferences.Encryption.superclass.initComponent.call(this); - var optMan = deluge.preferences.getOptionsManager(); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Settings'), - autoHeight: true, - defaultType: 'combo', - width: 300 - }); - optMan.bind('enc_in_policy', fieldset.add({ - fieldLabel: _('Inbound'), - mode: 'local', - width: 150, - store: new Ext.data.ArrayStore({ - fields: ['id', 'text'], - data: [ - [0, _('Forced')], - [1, _('Enabled')], - [2, _('Disabled')] - ] - }), - editable: false, - triggerAction: 'all', - valueField: 'id', - displayField: 'text' - })); - optMan.bind('enc_out_policy', fieldset.add({ - fieldLabel: _('Outbound'), - mode: 'local', - width: 150, - store: new Ext.data.SimpleStore({ - fields: ['id', 'text'], - data: [ - [0, _('Forced')], - [1, _('Enabled')], - [2, _('Disabled')] - ] - }), - editable: false, - triggerAction: 'all', - valueField: 'id', - displayField: 'text' - })); - optMan.bind('enc_level', fieldset.add({ - fieldLabel: _('Level'), - mode: 'local', - width: 150, - store: new Ext.data.SimpleStore({ - fields: ['id', 'text'], - data: [ - [0, _('Handshake')], - [1, _('Full Stream')], - [2, _('Either')] - ] - }), - editable: false, - triggerAction: 'all', - valueField: 'id', - displayField: 'text' - })); - optMan.bind('enc_prefer_rc4', fieldset.add({ - xtype: 'checkbox', - name: 'enc_prefer_rc4', - height: 40, - hideLabel: true, - boxLabel: _('Encrypt entire stream') - })); - } + var optMan = deluge.preferences.getOptionsManager(); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Settings'), + autoHeight: true, + defaultType: 'combo', + width: 300 + }); + optMan.bind('enc_in_policy', fieldset.add({ + fieldLabel: _('Inbound'), + mode: 'local', + width: 150, + store: new Ext.data.ArrayStore({ + fields: ['id', 'text'], + data: [ + [0, _('Forced')], + [1, _('Enabled')], + [2, _('Disabled')] + ] + }), + editable: false, + triggerAction: 'all', + valueField: 'id', + displayField: 'text' + })); + optMan.bind('enc_out_policy', fieldset.add({ + fieldLabel: _('Outbound'), + mode: 'local', + width: 150, + store: new Ext.data.SimpleStore({ + fields: ['id', 'text'], + data: [ + [0, _('Forced')], + [1, _('Enabled')], + [2, _('Disabled')] + ] + }), + editable: false, + triggerAction: 'all', + valueField: 'id', + displayField: 'text' + })); + optMan.bind('enc_level', fieldset.add({ + fieldLabel: _('Level'), + mode: 'local', + width: 150, + store: new Ext.data.SimpleStore({ + fields: ['id', 'text'], + data: [ + [0, _('Handshake')], + [1, _('Full Stream')], + [2, _('Either')] + ] + }), + editable: false, + triggerAction: 'all', + valueField: 'id', + displayField: 'text' + })); + optMan.bind('enc_prefer_rc4', fieldset.add({ + xtype: 'checkbox', + name: 'enc_prefer_rc4', + height: 40, + hideLabel: true, + boxLabel: _('Encrypt entire stream') + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/InstallPluginWindow.js b/deluge/ui/web/js/deluge-all/preferences/InstallPluginWindow.js index abc161437..c1d9e2b14 100644 --- a/deluge/ui/web/js/deluge-all/preferences/InstallPluginWindow.js +++ b/deluge/ui/web/js/deluge-all/preferences/InstallPluginWindow.js @@ -37,66 +37,66 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.InstallPluginWindow = Ext.extend(Ext.Window, { - title: _('Install Plugin'), - layout: 'fit', - height: 115, - width: 350, - - bodyStyle: 'padding: 10px 5px;', - buttonAlign: 'center', - closeAction: 'hide', - iconCls: 'x-deluge-install-plugin', - modal: true, - plain: true, + title: _('Install Plugin'), + layout: 'fit', + height: 115, + width: 350, + + bodyStyle: 'padding: 10px 5px;', + buttonAlign: 'center', + closeAction: 'hide', + iconCls: 'x-deluge-install-plugin', + modal: true, + plain: true, - initComponent: function() { - Deluge.add.FileWindow.superclass.initComponent.call(this); - this.addButton(_('Install'), this.onInstall, this); - - this.form = this.add({ - xtype: 'form', - baseCls: 'x-plain', - labelWidth: 70, - autoHeight: true, - fileUpload: true, - items: [{ - xtype: 'fileuploadfield', - width: 240, - emptyText: _('Select an egg'), - fieldLabel: _('Plugin Egg'), - name: 'file', - buttonCfg: { - text: _('Browse') + '...' - } - }] - }); - }, + initComponent: function() { + Deluge.add.FileWindow.superclass.initComponent.call(this); + this.addButton(_('Install'), this.onInstall, this); + + this.form = this.add({ + xtype: 'form', + baseCls: 'x-plain', + labelWidth: 70, + autoHeight: true, + fileUpload: true, + items: [{ + xtype: 'fileuploadfield', + width: 240, + emptyText: _('Select an egg'), + fieldLabel: _('Plugin Egg'), + name: 'file', + buttonCfg: { + text: _('Browse') + '...' + } + }] + }); + }, - onInstall: function(field, e) { - this.form.getForm().submit({ - url: '/upload', - waitMsg: _('Uploading your plugin...'), - success: this.onUploadSuccess, - scope: this - }); - }, + onInstall: function(field, e) { + this.form.getForm().submit({ + url: '/upload', + waitMsg: _('Uploading your plugin...'), + success: this.onUploadSuccess, + scope: this + }); + }, - onUploadPlugin: function(info, obj, response, request) { - this.fireEvent('pluginadded'); - }, + onUploadPlugin: function(info, obj, response, request) { + this.fireEvent('pluginadded'); + }, - onUploadSuccess: function(fp, upload) { - this.hide(); - if (upload.result.success) { - var filename = this.form.getForm().getFieldValues().file; - filename = filename.split('\\').slice(-1)[0] - var path = upload.result.files[0]; - this.form.getForm().setValues({file: ''}); - deluge.client.web.upload_plugin(filename, path, { - success: this.onUploadPlugin, - scope: this, - filename: filename - }); - } - } + onUploadSuccess: function(fp, upload) { + this.hide(); + if (upload.result.success) { + var filename = this.form.getForm().getFieldValues().file; + filename = filename.split('\\').slice(-1)[0] + var path = upload.result.files[0]; + this.form.getForm().setValues({file: ''}); + deluge.client.web.upload_plugin(filename, path, { + success: this.onUploadPlugin, + scope: this, + filename: filename + }); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js b/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js index bc8e87898..e8e6b749d 100644 --- a/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js +++ b/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js @@ -37,219 +37,219 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.Interface = Ext.extend(Ext.form.FormPanel, { - border: false, - title: _('Interface'), - layout: 'form', - - initComponent: function() { - Deluge.preferences.Interface.superclass.initComponent.call(this); - - var om = this.optionsManager = new Deluge.OptionsManager(); - this.on('show', this.onPageShow, this); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Interface'), - style: 'margin-bottom: 0px; padding-bottom: 5px; padding-top: 5px', - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - om.bind('show_session_speed', fieldset.add({ - name: 'show_session_speed', - height: 22, - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Show session speed in titlebar') - })); - om.bind('sidebar_show_zero', fieldset.add({ - name: 'sidebar_show_zero', - height: 22, - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Show filters with zero torrents') - })); - om.bind('sidebar_multiple_filters', fieldset.add({ - name: 'sidebar_multiple_filters', - height: 22, - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Allow the use of multiple filters at once') - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Password'), - style: 'margin-bottom: 0px; padding-bottom: 0px; padding-top: 5px', - autoHeight: true, - labelWidth: 110, - defaultType: 'textfield', - defaults: { - width: 180, - inputType: 'password' - } - }); - - this.oldPassword = fieldset.add({ - name: 'old_password', - fieldLabel: _('Old Password') - }); - this.newPassword = fieldset.add({ - name: 'new_password', - fieldLabel: _('New Password') - }); - this.confirmPassword = fieldset.add({ - name: 'confirm_password', - fieldLabel: _('Confirm Password') - }); - - var panel = fieldset.add({ - xtype: 'panel', - autoHeight: true, - border: false, - width: 320, - bodyStyle: 'padding-left: 230px' - }) - panel.add({ - xtype: 'button', - text: _('Change'), - listeners: { - 'click': { - fn: this.onPasswordChange, - scope: this - } - } - }); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Server'), - style: 'margin-top: 0px; padding-top: 0px; margin-bottom: 0px; padding-bottom: 0px', - autoHeight: true, - labelWidth: 110, - defaultType: 'spinnerfield', - defaults: { - width: 80 - } - }); - om.bind('session_timeout', fieldset.add({ - name: 'session_timeout', - fieldLabel: _('Session Timeout'), - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - om.bind('port', fieldset.add({ - name: 'port', - fieldLabel: _('Port'), - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - this.httpsField = om.bind('https', fieldset.add({ - xtype: 'checkbox', - name: 'https', - hideLabel: true, - width: 280, - height: 22, - boxLabel: _('Use SSL (paths relative to Deluge config folder)') - })); - this.httpsField.on('check', this.onSSLCheck, this); - this.pkeyField = om.bind('pkey', fieldset.add({ - xtype: 'textfield', - disabled: true, - name: 'pkey', - width: 180, - fieldLabel: _('Private Key') - })); - this.certField = om.bind('cert', fieldset.add({ - xtype: 'textfield', - disabled: true, - name: 'cert', - width: 180, - fieldLabel: _('Certificate') - })); - }, - - onApply: function() { - var changed = this.optionsManager.getDirty(); - if (!Ext.isObjectEmpty(changed)) { - deluge.client.web.set_config(changed, { - success: this.onSetConfig, - scope: this - }); + border: false, + title: _('Interface'), + layout: 'form', + + initComponent: function() { + Deluge.preferences.Interface.superclass.initComponent.call(this); + + var om = this.optionsManager = new Deluge.OptionsManager(); + this.on('show', this.onPageShow, this); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Interface'), + style: 'margin-bottom: 0px; padding-bottom: 5px; padding-top: 5px', + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + om.bind('show_session_speed', fieldset.add({ + name: 'show_session_speed', + height: 22, + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Show session speed in titlebar') + })); + om.bind('sidebar_show_zero', fieldset.add({ + name: 'sidebar_show_zero', + height: 22, + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Show filters with zero torrents') + })); + om.bind('sidebar_multiple_filters', fieldset.add({ + name: 'sidebar_multiple_filters', + height: 22, + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Allow the use of multiple filters at once') + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Password'), + style: 'margin-bottom: 0px; padding-bottom: 0px; padding-top: 5px', + autoHeight: true, + labelWidth: 110, + defaultType: 'textfield', + defaults: { + width: 180, + inputType: 'password' + } + }); + + this.oldPassword = fieldset.add({ + name: 'old_password', + fieldLabel: _('Old Password') + }); + this.newPassword = fieldset.add({ + name: 'new_password', + fieldLabel: _('New Password') + }); + this.confirmPassword = fieldset.add({ + name: 'confirm_password', + fieldLabel: _('Confirm Password') + }); + + var panel = fieldset.add({ + xtype: 'panel', + autoHeight: true, + border: false, + width: 320, + bodyStyle: 'padding-left: 230px' + }) + panel.add({ + xtype: 'button', + text: _('Change'), + listeners: { + 'click': { + fn: this.onPasswordChange, + scope: this + } + } + }); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Server'), + style: 'margin-top: 0px; padding-top: 0px; margin-bottom: 0px; padding-bottom: 0px', + autoHeight: true, + labelWidth: 110, + defaultType: 'spinnerfield', + defaults: { + width: 80 + } + }); + om.bind('session_timeout', fieldset.add({ + name: 'session_timeout', + fieldLabel: _('Session Timeout'), + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + om.bind('port', fieldset.add({ + name: 'port', + fieldLabel: _('Port'), + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + this.httpsField = om.bind('https', fieldset.add({ + xtype: 'checkbox', + name: 'https', + hideLabel: true, + width: 280, + height: 22, + boxLabel: _('Use SSL (paths relative to Deluge config folder)') + })); + this.httpsField.on('check', this.onSSLCheck, this); + this.pkeyField = om.bind('pkey', fieldset.add({ + xtype: 'textfield', + disabled: true, + name: 'pkey', + width: 180, + fieldLabel: _('Private Key') + })); + this.certField = om.bind('cert', fieldset.add({ + xtype: 'textfield', + disabled: true, + name: 'cert', + width: 180, + fieldLabel: _('Certificate') + })); + }, + + onApply: function() { + var changed = this.optionsManager.getDirty(); + if (!Ext.isObjectEmpty(changed)) { + deluge.client.web.set_config(changed, { + success: this.onSetConfig, + scope: this + }); - for (var key in deluge.config) { - deluge.config[key] = this.optionsManager.get(key); - } - } - }, - - onGotConfig: function(config) { - this.optionsManager.set(config); - }, - - onPasswordChange: function() { - var newPassword = this.newPassword.getValue(); - if (newPassword != this.confirmPassword.getValue()) { - Ext.MessageBox.show({ - title: _('Invalid Password'), - msg: _('Your passwords don\'t match!'), - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.ERROR, - iconCls: 'x-deluge-icon-error' - }); - return; - } - - var oldPassword = this.oldPassword.getValue(); - deluge.client.auth.change_password(oldPassword, newPassword, { - success: function(result) { - if (!result) { - Ext.MessageBox.show({ - title: _('Password'), - msg: _('Your old password was incorrect!'), - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.ERROR, - iconCls: 'x-deluge-icon-error' - }); - this.oldPassword.setValue(''); - } else { - Ext.MessageBox.show({ - title: _('Change Successful'), - msg: _('Your password was successfully changed!'), - buttons: Ext.MessageBox.OK, - modal: false, - icon: Ext.MessageBox.INFO, - iconCls: 'x-deluge-icon-info' - }); - this.oldPassword.setValue(''); - this.newPassword.setValue(''); - this.confirmPassword.setValue(''); - } - }, - scope: this - }); - }, - - onSetConfig: function() { - this.optionsManager.commit(); - }, - - onPageShow: function() { - deluge.client.web.get_config({ - success: this.onGotConfig, - scope: this - }) - }, - - onSSLCheck: function(e, checked) { - this.pkeyField.setDisabled(!checked); - this.certField.setDisabled(!checked); - } + for (var key in deluge.config) { + deluge.config[key] = this.optionsManager.get(key); + } + } + }, + + onGotConfig: function(config) { + this.optionsManager.set(config); + }, + + onPasswordChange: function() { + var newPassword = this.newPassword.getValue(); + if (newPassword != this.confirmPassword.getValue()) { + Ext.MessageBox.show({ + title: _('Invalid Password'), + msg: _('Your passwords don\'t match!'), + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + return; + } + + var oldPassword = this.oldPassword.getValue(); + deluge.client.auth.change_password(oldPassword, newPassword, { + success: function(result) { + if (!result) { + Ext.MessageBox.show({ + title: _('Password'), + msg: _('Your old password was incorrect!'), + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.ERROR, + iconCls: 'x-deluge-icon-error' + }); + this.oldPassword.setValue(''); + } else { + Ext.MessageBox.show({ + title: _('Change Successful'), + msg: _('Your password was successfully changed!'), + buttons: Ext.MessageBox.OK, + modal: false, + icon: Ext.MessageBox.INFO, + iconCls: 'x-deluge-icon-info' + }); + this.oldPassword.setValue(''); + this.newPassword.setValue(''); + this.confirmPassword.setValue(''); + } + }, + scope: this + }); + }, + + onSetConfig: function() { + this.optionsManager.commit(); + }, + + onPageShow: function() { + deluge.client.web.get_config({ + success: this.onGotConfig, + scope: this + }) + }, + + onSSLCheck: function(e, checked) { + this.pkeyField.setDisabled(!checked); + this.certField.setDisabled(!checked); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/NetworkPage.js b/deluge/ui/web/js/deluge-all/preferences/NetworkPage.js index 6d7d83929..13b1448fb 100644 --- a/deluge/ui/web/js/deluge-all/preferences/NetworkPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/NetworkPage.js @@ -36,195 +36,195 @@ Ext.namespace('Deluge.preferences'); * @extends Ext.form.FormPanel */ Deluge.preferences.Network = Ext.extend(Ext.form.FormPanel, { - - border: false, - layout: 'form', - title: _('Network'), + + border: false, + layout: 'form', + title: _('Network'), - initComponent: function() { - Deluge.preferences.Network.superclass.initComponent.call(this); - var optMan = deluge.preferences.getOptionsManager(); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Incoming Ports'), - style: 'margin-bottom: 5px; padding-bottom: 0px;', - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - optMan.bind('random_port', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Use Random Ports'), - name: 'random_port', - height: 22, - listeners: { - 'check': { - fn: function(e, checked) { - this.listenPorts.setDisabled(checked); - }, - scope: this - } - } - })); - this.listenPorts = fieldset.add({ - xtype: 'spinnergroup', - name: 'listen_ports', - fieldLabel: '', - labelSeparator: '', - colCfg: { - labelWidth: 40, - style: 'margin-right: 10px;' - }, - items: [{ - fieldLabel: 'From', - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - } - }, { - fieldLabel: 'To', - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - } - }] - }); - optMan.bind('listen_ports', this.listenPorts); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Outgoing Ports'), - style: 'margin-bottom: 5px; padding-bottom: 0px;', - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - optMan.bind('random_outgoing_ports', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Use Random Ports'), - name: 'random_outgoing_ports', - height: 22, - listeners: { - 'check': { - fn: function(e, checked) { - this.outgoingPorts.setDisabled(checked); - }, - scope: this - } - } - })); - this.outgoingPorts = fieldset.add({ - xtype: 'spinnergroup', - name: 'outgoing_ports', - fieldLabel: '', - labelSeparator: '', - colCfg: { - labelWidth: 40, - style: 'margin-right: 10px;' - }, - items: [{ - fieldLabel: 'From', - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - } - }, { - fieldLabel: 'To', - strategy: { - xtype: 'number', - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - } - }] - }); - optMan.bind('outgoing_ports', this.outgoingPorts); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Network Interface'), - style: 'margin-bottom: 5px; padding-bottom: 0px;', - autoHeight: true, - labelWidth: 1, - defaultType: 'textfield' - }); - optMan.bind('listen_interface', fieldset.add({ - name: 'listen_interface', - fieldLabel: '', - labelSeparator: '', - width: 200 - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('TOS'), - style: 'margin-bottom: 5px; padding-bottom: 0px;', - bodyStyle: 'margin: 0px; padding: 0px', - autoHeight: true, - defaultType: 'textfield' - }); - optMan.bind('peer_tos', fieldset.add({ - name: 'peer_tos', - fieldLabel: _('Peer TOS Byte'), - width: 80 - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Network Extras'), - autoHeight: true, - layout: 'table', - layoutConfig: { - columns: 3 - }, - defaultType: 'checkbox' - }); - optMan.bind('upnp', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('UPnP'), - name: 'upnp' - })); - optMan.bind('natpmp', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('NAT-PMP'), - ctCls: 'x-deluge-indent-checkbox', - name: 'natpmp' - })); - optMan.bind('utpex', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('Peer Exchange'), - ctCls: 'x-deluge-indent-checkbox', - name: 'utpex' - })); - optMan.bind('lsd', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('LSD'), - name: 'lsd' - })); - optMan.bind('dht', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - boxLabel: _('DHT'), - ctCls: 'x-deluge-indent-checkbox', - name: 'dht' - })); - } + initComponent: function() { + Deluge.preferences.Network.superclass.initComponent.call(this); + var optMan = deluge.preferences.getOptionsManager(); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Incoming Ports'), + style: 'margin-bottom: 5px; padding-bottom: 0px;', + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + optMan.bind('random_port', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Use Random Ports'), + name: 'random_port', + height: 22, + listeners: { + 'check': { + fn: function(e, checked) { + this.listenPorts.setDisabled(checked); + }, + scope: this + } + } + })); + this.listenPorts = fieldset.add({ + xtype: 'spinnergroup', + name: 'listen_ports', + fieldLabel: '', + labelSeparator: '', + colCfg: { + labelWidth: 40, + style: 'margin-right: 10px;' + }, + items: [{ + fieldLabel: 'From', + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + } + }, { + fieldLabel: 'To', + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + } + }] + }); + optMan.bind('listen_ports', this.listenPorts); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Outgoing Ports'), + style: 'margin-bottom: 5px; padding-bottom: 0px;', + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + optMan.bind('random_outgoing_ports', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Use Random Ports'), + name: 'random_outgoing_ports', + height: 22, + listeners: { + 'check': { + fn: function(e, checked) { + this.outgoingPorts.setDisabled(checked); + }, + scope: this + } + } + })); + this.outgoingPorts = fieldset.add({ + xtype: 'spinnergroup', + name: 'outgoing_ports', + fieldLabel: '', + labelSeparator: '', + colCfg: { + labelWidth: 40, + style: 'margin-right: 10px;' + }, + items: [{ + fieldLabel: 'From', + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + } + }, { + fieldLabel: 'To', + strategy: { + xtype: 'number', + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + } + }] + }); + optMan.bind('outgoing_ports', this.outgoingPorts); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Network Interface'), + style: 'margin-bottom: 5px; padding-bottom: 0px;', + autoHeight: true, + labelWidth: 1, + defaultType: 'textfield' + }); + optMan.bind('listen_interface', fieldset.add({ + name: 'listen_interface', + fieldLabel: '', + labelSeparator: '', + width: 200 + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('TOS'), + style: 'margin-bottom: 5px; padding-bottom: 0px;', + bodyStyle: 'margin: 0px; padding: 0px', + autoHeight: true, + defaultType: 'textfield' + }); + optMan.bind('peer_tos', fieldset.add({ + name: 'peer_tos', + fieldLabel: _('Peer TOS Byte'), + width: 80 + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Network Extras'), + autoHeight: true, + layout: 'table', + layoutConfig: { + columns: 3 + }, + defaultType: 'checkbox' + }); + optMan.bind('upnp', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('UPnP'), + name: 'upnp' + })); + optMan.bind('natpmp', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('NAT-PMP'), + ctCls: 'x-deluge-indent-checkbox', + name: 'natpmp' + })); + optMan.bind('utpex', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('Peer Exchange'), + ctCls: 'x-deluge-indent-checkbox', + name: 'utpex' + })); + optMan.bind('lsd', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('LSD'), + name: 'lsd' + })); + optMan.bind('dht', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + boxLabel: _('DHT'), + ctCls: 'x-deluge-indent-checkbox', + name: 'dht' + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/OtherPage.js b/deluge/ui/web/js/deluge-all/preferences/OtherPage.js index 951d8c2b4..105500eb3 100644 --- a/deluge/ui/web/js/deluge-all/preferences/OtherPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/OtherPage.js @@ -36,73 +36,73 @@ Ext.namespace('Deluge.preferences'); * @extends Ext.form.FormPanel */ Deluge.preferences.Other = Ext.extend(Ext.form.FormPanel, { - constructor: function(config) { - config = Ext.apply({ - border: false, - title: _('Other'), - layout: 'form' - }, config); - Deluge.preferences.Other.superclass.constructor.call(this, config); - }, - - initComponent: function() { - Deluge.preferences.Other.superclass.initComponent.call(this); - - var optMan = deluge.preferences.getOptionsManager(); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Updates'), - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - optMan.bind('new_release_check', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - height: 22, - name: 'new_release_check', - boxLabel: _('Be alerted about new releases') - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('System Information'), - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - fieldset.add({ - xtype: 'panel', - border: false, - bodyCfg: { - html: _('Help us improve Deluge by sending us your ' - + 'Python version, PyGTK version, OS and processor ' - + 'types. Absolutely no other information is sent.') - } - }); - optMan.bind('send_info', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - height: 22, - boxLabel: _('Yes, please send anonymous statistics'), - name: 'send_info' - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('GeoIP Database'), - autoHeight: true, - labelWidth: 80, - defaultType: 'textfield' - }); - optMan.bind('geoip_db_location', fieldset.add({ - name: 'geoip_db_location', - fieldLabel: _('Location'), - width: 200 - })); - } + constructor: function(config) { + config = Ext.apply({ + border: false, + title: _('Other'), + layout: 'form' + }, config); + Deluge.preferences.Other.superclass.constructor.call(this, config); + }, + + initComponent: function() { + Deluge.preferences.Other.superclass.initComponent.call(this); + + var optMan = deluge.preferences.getOptionsManager(); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Updates'), + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + optMan.bind('new_release_check', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + height: 22, + name: 'new_release_check', + boxLabel: _('Be alerted about new releases') + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('System Information'), + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + fieldset.add({ + xtype: 'panel', + border: false, + bodyCfg: { + html: _('Help us improve Deluge by sending us your ' + + 'Python version, PyGTK version, OS and processor ' + + 'types. Absolutely no other information is sent.') + } + }); + optMan.bind('send_info', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + height: 22, + boxLabel: _('Yes, please send anonymous statistics'), + name: 'send_info' + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('GeoIP Database'), + autoHeight: true, + labelWidth: 80, + defaultType: 'textfield' + }); + optMan.bind('geoip_db_location', fieldset.add({ + name: 'geoip_db_location', + fieldLabel: _('Location'), + width: 200 + })); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/PluginsPage.js b/deluge/ui/web/js/deluge-all/preferences/PluginsPage.js index d963d1fde..1d7499fe1 100644 --- a/deluge/ui/web/js/deluge-all/preferences/PluginsPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/PluginsPage.js @@ -1,6 +1,6 @@ /*! * Deluge.preferences.PluginsPage.js - * + * * Copyright (c) Damien Churchill 2009-2010 * * This program is free software; you can redistribute it and/or modify @@ -37,233 +37,234 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.Plugins = Ext.extend(Ext.Panel, { - layout: 'border', - title: _('Plugins'), - border: false, - height: 400, - cls: 'x-deluge-plugins', + layout: 'border', + title: _('Plugins'), + border: false, + height: 400, + cls: 'x-deluge-plugins', - pluginTemplate: new Ext.Template( - '
' + - '
Author:
{author}
' + - '
Version:
{version}
' + - '
Author Email:
{email}
' + - '
Homepage:
{homepage}
' + - '
Details:
{details}
' + - '
' - ), + pluginTemplate: new Ext.Template( + '
' + + '
Author:
{author}
' + + '
Version:
{version}
' + + '
Author Email:
{email}
' + + '
Homepage:
{homepage}
' + + '
Details:
{details}
' + + '
' + ), - initComponent: function() { - Deluge.preferences.Plugins.superclass.initComponent.call(this); - this.defaultValues = { - 'version': '', - 'email': '', - 'homepage': '', - 'details': '' - }; - this.pluginTemplate.compile(); + initComponent: function() { + Deluge.preferences.Plugins.superclass.initComponent.call(this); + this.defaultValues = { + 'version': '', + 'email': '', + 'homepage': '', + 'details': '' + }; + this.pluginTemplate.compile(); - var checkboxRenderer = function(v, p, record){ - p.css += ' x-grid3-check-col-td'; - return '
'; - } + var checkboxRenderer = function(v, p, record){ + p.css += ' x-grid3-check-col-td'; + return '
'; + } - this.list = this.add({ - xtype: 'listview', - store: new Ext.data.ArrayStore({ - fields: [ - {name: 'enabled', mapping: 0}, - {name: 'plugin', mapping: 1} - ] - }), - columns: [{ - id: 'enabled', - header: _('Enabled'), - width: .2, - sortable: true, - tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', { - getCheckbox: function(v) { - return '
'; - } - }), - dataIndex: 'enabled' - }, { - id: 'plugin', - header: _('Plugin'), - width: .8, - sortable: true, - dataIndex: 'plugin' - }], - singleSelect: true, - autoExpandColumn: 'plugin', - listeners: { - selectionchange: {fn: this.onPluginSelect, scope: this} - } - }); + this.list = this.add({ + xtype: 'listview', + store: new Ext.data.ArrayStore({ + fields: [ + {name: 'enabled', mapping: 0}, + {name: 'plugin', mapping: 1} + ] + }), + columns: [{ + id: 'enabled', + header: _('Enabled'), + width: .2, + sortable: true, + tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', { + getCheckbox: function(v) { + return '
'; + } + }), + dataIndex: 'enabled' + }, { + id: 'plugin', + header: _('Plugin'), + width: .8, + sortable: true, + dataIndex: 'plugin' + }], + singleSelect: true, + autoExpandColumn: 'plugin', + listeners: { + selectionchange: {fn: this.onPluginSelect, scope: this} + } + }); - this.panel = this.add({ - region: 'center', - autoScroll: true, - margins: '5 5 5 5', - items: [this.list], - bbar: new Ext.Toolbar({ - items: [{ - cls: 'x-btn-text-icon', - iconCls: 'x-deluge-install-plugin', - text: _('Install'), - handler: this.onInstallPluginWindow, - scope: this - }, '->', { - cls: 'x-btn-text-icon', - text: _('Find More'), - iconCls: 'x-deluge-find-more', - handler: this.onFindMorePlugins, - scope: this - }] - }) - }); - - var pp = this.pluginInfo = this.add({ - xtype: 'panel', - border: true, - height: 160, - region: 'south', - margins: '0 5 5 5' - }); - var fieldset = pp.add({ - xtype: 'fieldset', - title: _('Info'), - border: false, - autoHeight: true, - labelWidth: 1, - style: 'margin-top: 5px;' - }); - this.pluginInfo = fieldset.add({ - xtype: 'panel', - border: false, - bodyCfg: { - style: 'margin-left: 10px' - } - }); - - this.pluginInfo.on('render', this.onPluginInfoRender, this); - this.list.on('click', this.onNodeClick, this); - deluge.preferences.on('show', this.onPreferencesShow, this); - deluge.events.on('PluginDisabledEvent', this.onPluginDisabled, this); - deluge.events.on('PluginEnabledEvent', this.onPluginEnabled, this); - }, + this.panel = this.add({ + region: 'center', + autoScroll: true, + margins: '5 5 5 5', + items: [this.list], + bbar: new Ext.Toolbar({ + items: [{ + cls: 'x-btn-text-icon', + iconCls: 'x-deluge-install-plugin', + text: _('Install'), + handler: this.onInstallPluginWindow, + scope: this + }, '->', { + cls: 'x-btn-text-icon', + text: _('Find More'), + iconCls: 'x-deluge-find-more', + handler: this.onFindMorePlugins, + scope: this + }] + }) + }); - disablePlugin: function(plugin) { - deluge.client.core.disable_plugin(plugin); - }, + var pp = this.pluginInfo = this.add({ + xtype: 'panel', + border: true, + height: 160, + region: 'south', + margins: '0 5 5 5' + }); + var fieldset = pp.add({ + xtype: 'fieldset', + title: _('Info'), + border: false, + autoHeight: true, + labelWidth: 1, + style: 'margin-top: 5px;' + }); + this.pluginInfo = fieldset.add({ + xtype: 'panel', + border: false, + bodyCfg: { + style: 'margin-left: 10px' + } + }); - enablePlugin: function(plugin) { - deluge.client.core.enable_plugin(plugin); - }, + this.pluginInfo.on('render', this.onPluginInfoRender, this); + this.list.on('click', this.onNodeClick, this); + deluge.preferences.on('show', this.onPreferencesShow, this); + deluge.events.on('PluginDisabledEvent', this.onPluginDisabled, this); + deluge.events.on('PluginEnabledEvent', this.onPluginEnabled, this); + }, - setInfo: function(plugin) { - if (!this.pluginInfo.rendered) return; - var values = plugin || this.defaultValues; - this.pluginInfo.body.dom.innerHTML = this.pluginTemplate.apply(values); - }, - - updatePlugins: function() { - deluge.client.web.get_plugins({ - success: this.onGotPlugins, - scope: this - }); - }, + disablePlugin: function(plugin) { + deluge.client.core.disable_plugin(plugin); + }, - updatePluginsGrid: function() { - var plugins = []; - Ext.each(this.availablePlugins, function(plugin) { - if (this.enabledPlugins.indexOf(plugin) > -1) { - plugins.push([true, plugin]); - } else { - plugins.push([false, plugin]); - } - }, this); - this.list.getStore().loadData(plugins); - }, + enablePlugin: function(plugin) { + deluge.client.core.enable_plugin(plugin); + }, - onNodeClick: function(dv, index, node, e) { - var el = new Ext.Element(e.target); - if (el.getAttribute('rel') != 'chkbox') return; + setInfo: function(plugin) { + if (!this.pluginInfo.rendered) return; + var values = plugin || this.defaultValues; + this.pluginInfo.body.dom.innerHTML = this.pluginTemplate.apply(values); + }, - var r = dv.getStore().getAt(index); - r.set('enabled', !r.get('enabled')); - r.commit(); - if (r.get('enabled')) { - this.enablePlugin(r.get('plugin')); - } else { - this.disablePlugin(r.get('plugin')); - } - }, + updatePlugins: function() { + deluge.client.web.get_plugins({ + success: this.onGotPlugins, + scope: this + }); + }, - onFindMorePlugins: function() { - window.open('http://dev.deluge-torrent.org/wiki/Plugins'); - }, + updatePluginsGrid: function() { + var plugins = []; + Ext.each(this.availablePlugins, function(plugin) { + if (this.enabledPlugins.indexOf(plugin) > -1) { + plugins.push([true, plugin]); + } else { + plugins.push([false, plugin]); + } + }, this); + this.list.getStore().loadData(plugins); + }, - onGotPlugins: function(plugins) { - this.enabledPlugins = plugins.enabled_plugins; - this.availablePlugins = plugins.available_plugins; - this.setInfo(); - this.updatePluginsGrid(); - }, + onNodeClick: function(dv, index, node, e) { + var el = new Ext.Element(e.target); + if (el.getAttribute('rel') != 'chkbox') return; - onGotPluginInfo: function(info) { - var values = { - author: info['Author'], - version: info['Version'], - email: info['Author-email'], - homepage: info['Home-page'], - details: info['Description'] - } - this.setInfo(values); - delete info; - }, + var r = dv.getStore().getAt(index); + r.set('enabled', !r.get('enabled')); + r.commit(); + if (r.get('enabled')) { + this.enablePlugin(r.get('plugin')); + } else { + this.disablePlugin(r.get('plugin')); + } + }, - onInstallPluginWindow: function() { - if (!this.installWindow) { - this.installWindow = new Deluge.preferences.InstallPluginWindow(); - this.installWindow.on('pluginadded', this.onPluginInstall, this); - } - this.installWindow.show(); - }, + onFindMorePlugins: function() { + window.open('http://dev.deluge-torrent.org/wiki/Plugins'); + }, - onPluginEnabled: function(pluginName) { - var index = this.list.getStore().find('plugin', pluginName); - if (index == -1) return; - var plugin = this.list.getStore().getAt(index); - plugin.set('enabled', true); - plugin.commit(); - }, - - onPluginDisabled: function(pluginName) { - var index = this.list.getStore().find('plugin', pluginName); - if (index == -1) return; - var plugin = this.list.getStore().getAt(index); - plugin.set('enabled', false); - plugin.commit(); - }, + onGotPlugins: function(plugins) { + this.enabledPlugins = plugins.enabled_plugins; + this.availablePlugins = plugins.available_plugins; + this.setInfo(); + this.updatePluginsGrid(); + }, - onPluginInstall: function() { - this.updatePlugins(); - }, + onGotPluginInfo: function(info) { + var values = { + author: info['Author'], + version: info['Version'], + email: info['Author-email'], + homepage: info['Home-page'], + details: info['Description'] + } + this.setInfo(values); + delete info; + }, - onPluginSelect: function(dv, selections) { - var r = dv.getRecords(selections)[0]; - deluge.client.web.get_plugin_info(r.get('plugin'), { - success: this.onGotPluginInfo, - scope: this - }); - }, + onInstallPluginWindow: function() { + if (!this.installWindow) { + this.installWindow = new Deluge.preferences.InstallPluginWindow(); + this.installWindow.on('pluginadded', this.onPluginInstall, this); + } + this.installWindow.show(); + }, - onPreferencesShow: function() { - this.updatePlugins(); - }, + onPluginEnabled: function(pluginName) { + var index = this.list.getStore().find('plugin', pluginName); + if (index == -1) return; + var plugin = this.list.getStore().getAt(index); + plugin.set('enabled', true); + plugin.commit(); + }, - onPluginInfoRender: function(ct, position) { - this.setInfo(); - } + onPluginDisabled: function(pluginName) { + var index = this.list.getStore().find('plugin', pluginName); + if (index == -1) return; + var plugin = this.list.getStore().getAt(index); + plugin.set('enabled', false); + plugin.commit(); + }, + + onPluginInstall: function() { + this.updatePlugins(); + }, + + onPluginSelect: function(dv, selections) { + if (selections.length == 0) return; + var r = dv.getRecords(selections)[0]; + deluge.client.web.get_plugin_info(r.get('plugin'), { + success: this.onGotPluginInfo, + scope: this + }); + }, + + onPreferencesShow: function() { + this.updatePlugins(); + }, + + onPluginInfoRender: function(ct, position) { + this.setInfo(); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js b/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js index f71c463f6..67f11fe83 100644 --- a/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js +++ b/deluge/ui/web/js/deluge-all/preferences/PreferencesWindow.js @@ -39,207 +39,207 @@ PreferencesRecord = Ext.data.Record.create([{name:'name', type:'string'}]); */ Deluge.preferences.PreferencesWindow = Ext.extend(Ext.Window, { - /** - * @property {String} currentPage The currently selected page. - */ - currentPage: null, + /** + * @property {String} currentPage The currently selected page. + */ + currentPage: null, - title: _('Preferences'), - layout: 'border', - width: 485, - height: 500, + title: _('Preferences'), + layout: 'border', + width: 485, + height: 500, - buttonAlign: 'right', - closeAction: 'hide', - closable: true, - iconCls: 'x-deluge-preferences', - plain: true, - resizable: false, + buttonAlign: 'right', + closeAction: 'hide', + closable: true, + iconCls: 'x-deluge-preferences', + plain: true, + resizable: false, - pages: {}, + pages: {}, - initComponent: function() { - Deluge.preferences.PreferencesWindow.superclass.initComponent.call(this); + initComponent: function() { + Deluge.preferences.PreferencesWindow.superclass.initComponent.call(this); - this.list = new Ext.list.ListView({ - store: new Ext.data.Store(), - columns: [{ - id: 'name', - renderer: fplain, - dataIndex: 'name' - }], - singleSelect: true, - listeners: { - 'selectionchange': { - fn: this.onPageSelect, scope: this - } - }, - hideHeaders: true, - autoExpandColumn: 'name', - deferredRender: false, - autoScroll: true, - collapsible: true - }); - this.add({ - region: 'west', - title: _('Categories'), - items: [this.list], - width: 120, - margins: '5 0 5 5', - cmargins: '5 0 5 5' - }); + this.list = new Ext.list.ListView({ + store: new Ext.data.Store(), + columns: [{ + id: 'name', + renderer: fplain, + dataIndex: 'name' + }], + singleSelect: true, + listeners: { + 'selectionchange': { + fn: this.onPageSelect, scope: this + } + }, + hideHeaders: true, + autoExpandColumn: 'name', + deferredRender: false, + autoScroll: true, + collapsible: true + }); + this.add({ + region: 'west', + title: _('Categories'), + items: [this.list], + width: 120, + margins: '5 0 5 5', + cmargins: '5 0 5 5' + }); - this.configPanel = this.add({ - type: 'container', - autoDestroy: false, - region: 'center', - layout: 'card', - layoutConfig: { - deferredRender: true - }, - autoScroll: true, - width: 300, - margins: '5 5 5 5', - cmargins: '5 5 5 5' - }); + this.configPanel = this.add({ + type: 'container', + autoDestroy: false, + region: 'center', + layout: 'card', + layoutConfig: { + deferredRender: true + }, + autoScroll: true, + width: 300, + margins: '5 5 5 5', + cmargins: '5 5 5 5' + }); - this.addButton(_('Close'), this.onClose, this); - this.addButton(_('Apply'), this.onApply, this); - this.addButton(_('Ok'), this.onOk, this); - - this.optionsManager = new Deluge.OptionsManager(); - this.on('afterrender', this.onAfterRender, this); - this.on('show', this.onShow, this); + this.addButton(_('Close'), this.onClose, this); + this.addButton(_('Apply'), this.onApply, this); + this.addButton(_('Ok'), this.onOk, this); + + this.optionsManager = new Deluge.OptionsManager(); + this.on('afterrender', this.onAfterRender, this); + this.on('show', this.onShow, this); - this.initPages(); - }, + this.initPages(); + }, - initPages: function() { - deluge.preferences = this; - this.addPage(new Deluge.preferences.Downloads()); - this.addPage(new Deluge.preferences.Network()); - this.addPage(new Deluge.preferences.Encryption()); - this.addPage(new Deluge.preferences.Bandwidth()); - this.addPage(new Deluge.preferences.Interface()); - this.addPage(new Deluge.preferences.Other()); - this.addPage(new Deluge.preferences.Daemon()); - this.addPage(new Deluge.preferences.Queue()); - this.addPage(new Deluge.preferences.Proxy()); - this.addPage(new Deluge.preferences.Cache()); - this.addPage(new Deluge.preferences.Plugins()); - }, - - onApply: function(e) { - var changed = this.optionsManager.getDirty(); - if (!Ext.isObjectEmpty(changed)) { - deluge.client.core.set_config(changed, { - success: this.onSetConfig, - scope: this - }); - } - - for (var page in this.pages) { - if (this.pages[page].onApply) this.pages[page].onApply(); - } - }, - - - /** - * Return the options manager for the preferences window. - * @returns {Deluge.OptionsManager} the options manager - */ - getOptionsManager: function() { - return this.optionsManager; - }, - - /** - * Adds a page to the preferences window. - * @param {Mixed} page - */ - addPage: function(page) { - var store = this.list.getStore(); - var name = page.title; - store.add([new PreferencesRecord({name: name})]); - page['bodyStyle'] = 'padding: 5px'; - page.preferences = this; - this.pages[name] = this.configPanel.add(page); - this.pages[name].index = -1; - return this.pages[name]; - }, - - /** - * Removes a preferences page from the window. - * @param {mixed} name - */ - removePage: function(page) { - var name = page.title; - var store = this.list.getStore(); - store.removeAt(store.find('name', name)); - this.configPanel.remove(page); - delete this.pages[page.title]; - }, + initPages: function() { + deluge.preferences = this; + this.addPage(new Deluge.preferences.Downloads()); + this.addPage(new Deluge.preferences.Network()); + this.addPage(new Deluge.preferences.Encryption()); + this.addPage(new Deluge.preferences.Bandwidth()); + this.addPage(new Deluge.preferences.Interface()); + this.addPage(new Deluge.preferences.Other()); + this.addPage(new Deluge.preferences.Daemon()); + this.addPage(new Deluge.preferences.Queue()); + this.addPage(new Deluge.preferences.Proxy()); + this.addPage(new Deluge.preferences.Cache()); + this.addPage(new Deluge.preferences.Plugins()); + }, + + onApply: function(e) { + var changed = this.optionsManager.getDirty(); + if (!Ext.isObjectEmpty(changed)) { + deluge.client.core.set_config(changed, { + success: this.onSetConfig, + scope: this + }); + } + + for (var page in this.pages) { + if (this.pages[page].onApply) this.pages[page].onApply(); + } + }, + + + /** + * Return the options manager for the preferences window. + * @returns {Deluge.OptionsManager} the options manager + */ + getOptionsManager: function() { + return this.optionsManager; + }, + + /** + * Adds a page to the preferences window. + * @param {Mixed} page + */ + addPage: function(page) { + var store = this.list.getStore(); + var name = page.title; + store.add([new PreferencesRecord({name: name})]); + page['bodyStyle'] = 'padding: 5px'; + page.preferences = this; + this.pages[name] = this.configPanel.add(page); + this.pages[name].index = -1; + return this.pages[name]; + }, + + /** + * Removes a preferences page from the window. + * @param {mixed} name + */ + removePage: function(page) { + var name = page.title; + var store = this.list.getStore(); + store.removeAt(store.find('name', name)); + this.configPanel.remove(page); + delete this.pages[page.title]; + }, - /** - * Select which preferences page is displayed. - * @param {String} page The page name to change to - */ - selectPage: function(page) { - if (this.pages[page].index < 0) { - this.pages[page].index = this.configPanel.items.indexOf(this.pages[page]); - } - this.list.select(this.pages[page].index); - }, + /** + * Select which preferences page is displayed. + * @param {String} page The page name to change to + */ + selectPage: function(page) { + if (this.pages[page].index < 0) { + this.pages[page].index = this.configPanel.items.indexOf(this.pages[page]); + } + this.list.select(this.pages[page].index); + }, - // private - doSelectPage: function(page) { - if (this.pages[page].index < 0) { - this.pages[page].index = this.configPanel.items.indexOf(this.pages[page]); - } - this.configPanel.getLayout().setActiveItem(this.pages[page].index); - this.currentPage = page; - }, - - // private - onGotConfig: function(config) { - this.getOptionsManager().set(config); - }, - - // private - onPageSelect: function(list, selections) { - var r = list.getRecord(selections[0]); - this.doSelectPage(r.get('name')); - }, - - // private - onSetConfig: function() { - this.getOptionsManager().commit(); - }, + // private + doSelectPage: function(page) { + if (this.pages[page].index < 0) { + this.pages[page].index = this.configPanel.items.indexOf(this.pages[page]); + } + this.configPanel.getLayout().setActiveItem(this.pages[page].index); + this.currentPage = page; + }, + + // private + onGotConfig: function(config) { + this.getOptionsManager().set(config); + }, + + // private + onPageSelect: function(list, selections) { + var r = list.getRecord(selections[0]); + this.doSelectPage(r.get('name')); + }, + + // private + onSetConfig: function() { + this.getOptionsManager().commit(); + }, - // private - onAfterRender: function() { - if (!this.list.getSelectionCount()) { - this.list.select(0); - } - this.configPanel.getLayout().setActiveItem(0); - }, - - // private - onShow: function() { - if (!deluge.client.core) return; - deluge.client.core.get_config({ - success: this.onGotConfig, - scope: this - }) - }, + // private + onAfterRender: function() { + if (!this.list.getSelectionCount()) { + this.list.select(0); + } + this.configPanel.getLayout().setActiveItem(0); + }, + + // private + onShow: function() { + if (!deluge.client.core) return; + deluge.client.core.get_config({ + success: this.onGotConfig, + scope: this + }) + }, - // private - onClose: function() { - this.hide(); - }, + // private + onClose: function() { + this.hide(); + }, - // private - onOk: function() { - deluge.client.core.set_config(this.optionsManager.getDirty()); - this.hide(); - } + // private + onOk: function() { + deluge.client.core.set_config(this.optionsManager.getDirty()); + this.hide(); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/ProxyField.js b/deluge/ui/web/js/deluge-all/preferences/ProxyField.js index 228ae10d7..811372128 100644 --- a/deluge/ui/web/js/deluge-all/preferences/ProxyField.js +++ b/deluge/ui/web/js/deluge-all/preferences/ProxyField.js @@ -15,9 +15,9 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, write to: - * The Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor - * Boston, MA 02110-1301, USA. + * The Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give * permission to link the code of portions of this program with the OpenSSL @@ -37,125 +37,125 @@ Ext.ns('Deluge.preferences'); */ Deluge.preferences.ProxyField = Ext.extend(Ext.form.FieldSet, { - border: false, - autoHeight: true, - labelWidth: 70, + border: false, + autoHeight: true, + labelWidth: 70, - initComponent: function() { - Deluge.preferences.ProxyField.superclass.initComponent.call(this); - this.proxyType = this.add({ - xtype: 'combo', - fieldLabel: _('Type'), - name: 'proxytype', - mode: 'local', - width: 150, - store: new Ext.data.ArrayStore({ - fields: ['id', 'text'], - data: [ - [0, _('None')], - [1, _('Socksv4')], - [2, _('Socksv5')], - [3, _('Socksv5 with Auth')], - [4, _('HTTP')], - [5, _('HTTP with Auth')] - ] - }), - editable: false, - triggerAction: 'all', - valueField: 'id', - displayField: 'text' - }); - this.hostname = this.add({ - xtype: 'textfield', - name: 'hostname', - fieldLabel: _('Host'), - width: 220 - }); + initComponent: function() { + Deluge.preferences.ProxyField.superclass.initComponent.call(this); + this.proxyType = this.add({ + xtype: 'combo', + fieldLabel: _('Type'), + name: 'proxytype', + mode: 'local', + width: 150, + store: new Ext.data.ArrayStore({ + fields: ['id', 'text'], + data: [ + [0, _('None')], + [1, _('Socksv4')], + [2, _('Socksv5')], + [3, _('Socksv5 with Auth')], + [4, _('HTTP')], + [5, _('HTTP with Auth')] + ] + }), + editable: false, + triggerAction: 'all', + valueField: 'id', + displayField: 'text' + }); + this.hostname = this.add({ + xtype: 'textfield', + name: 'hostname', + fieldLabel: _('Host'), + width: 220 + }); - this.port = this.add({ - xtype: 'spinnerfield', - name: 'port', - fieldLabel: _('Port'), - width: 80, - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - }); + this.port = this.add({ + xtype: 'spinnerfield', + name: 'port', + fieldLabel: _('Port'), + width: 80, + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + }); - this.username = this.add({ - xtype: 'textfield', - name: 'username', - fieldLabel: _('Username'), - width: 220 - }); + this.username = this.add({ + xtype: 'textfield', + name: 'username', + fieldLabel: _('Username'), + width: 220 + }); - this.password = this.add({ - xtype: 'textfield', - name: 'password', - fieldLabel: _('Password'), - inputType: 'password', - width: 220 - }); + this.password = this.add({ + xtype: 'textfield', + name: 'password', + fieldLabel: _('Password'), + inputType: 'password', + width: 220 + }); - this.proxyType.on('change', this.onFieldChange, this); - this.proxyType.on('select', this.onTypeSelect, this); - this.setting = false; - }, + this.proxyType.on('change', this.onFieldChange, this); + this.proxyType.on('select', this.onTypeSelect, this); + this.setting = false; + }, - getName: function() { - return this.initialConfig.name; - }, + getName: function() { + return this.initialConfig.name; + }, - getValue: function() { - return { - 'type': this.proxyType.getValue(), - 'hostname': this.hostname.getValue(), - 'port': Number(this.port.getValue()), - 'username': this.username.getValue(), - 'password': this.password.getValue() - } - }, + getValue: function() { + return { + 'type': this.proxyType.getValue(), + 'hostname': this.hostname.getValue(), + 'port': Number(this.port.getValue()), + 'username': this.username.getValue(), + 'password': this.password.getValue() + } + }, - // Set the values of the proxies - setValue: function(value) { - this.setting = true; - this.proxyType.setValue(value['type']); - var index = this.proxyType.getStore().find('id', value['type']); - var record = this.proxyType.getStore().getAt(index); + // Set the values of the proxies + setValue: function(value) { + this.setting = true; + this.proxyType.setValue(value['type']); + var index = this.proxyType.getStore().find('id', value['type']); + var record = this.proxyType.getStore().getAt(index); - this.hostname.setValue(value['hostname']); - this.port.setValue(value['port']); - this.username.setValue(value['username']); - this.password.setValue(value['password']); - this.onTypeSelect(this.type, record, index); - this.setting = false; - }, + this.hostname.setValue(value['hostname']); + this.port.setValue(value['port']); + this.username.setValue(value['username']); + this.password.setValue(value['password']); + this.onTypeSelect(this.type, record, index); + this.setting = false; + }, - onFieldChange: function(field, newValue, oldValue) { - if (this.setting) return; - var newValues = this.getValue(); - var oldValues = Ext.apply({}, newValues); - oldValues[field.getName()] = oldValue; + onFieldChange: function(field, newValue, oldValue) { + if (this.setting) return; + var newValues = this.getValue(); + var oldValues = Ext.apply({}, newValues); + oldValues[field.getName()] = oldValue; - this.fireEvent('change', this, newValues, oldValues); - }, + this.fireEvent('change', this, newValues, oldValues); + }, - onTypeSelect: function(combo, record, index) { - var typeId = record.get('id'); - if (typeId > 0) { - this.hostname.show(); - this.port.show(); - } else { - this.hostname.hide(); - this.port.hide(); - } + onTypeSelect: function(combo, record, index) { + var typeId = record.get('id'); + if (typeId > 0) { + this.hostname.show(); + this.port.show(); + } else { + this.hostname.hide(); + this.port.hide(); + } - if (typeId == 3 || typeId == 5) { - this.username.show(); - this.password.show(); - } else { - this.username.hide(); - this.password.hide(); - } - } + if (typeId == 3 || typeId == 5) { + this.username.show(); + this.password.show(); + } else { + this.username.hide(); + this.password.hide(); + } + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/ProxyPage.js b/deluge/ui/web/js/deluge-all/preferences/ProxyPage.js index 24cefd9c4..db4740fb6 100644 --- a/deluge/ui/web/js/deluge-all/preferences/ProxyPage.js +++ b/deluge/ui/web/js/deluge-all/preferences/ProxyPage.js @@ -36,64 +36,64 @@ Ext.namespace('Deluge.preferences'); * @extends Ext.form.FormPanel */ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, { - constructor: function(config) { - config = Ext.apply({ - border: false, - title: _('Proxy'), - layout: 'form' - }, config); - Deluge.preferences.Proxy.superclass.constructor.call(this, config); - }, - - initComponent: function() { - Deluge.preferences.Proxy.superclass.initComponent.call(this); - this.peer = this.add(new Deluge.preferences.ProxyField({ - title: _('Peer'), - name: 'peer' - })); - this.peer.on('change', this.onProxyChange, this); - - this.web_seed = this.add(new Deluge.preferences.ProxyField({ - title: _('Web Seed'), - name: 'web_seed' - })); - this.web_seed.on('change', this.onProxyChange, this); - - this.tracker = this.add(new Deluge.preferences.ProxyField({ - title: _('Tracker'), - name: 'tracker' - })); - this.tracker.on('change', this.onProxyChange, this); - - this.dht = this.add(new Deluge.preferences.ProxyField({ - title: _('DHT'), - name: 'dht' - })); - this.dht.on('change', this.onProxyChange, this); - - deluge.preferences.getOptionsManager().bind('proxies', this); - }, - - getValue: function() { - return { - 'dht': this.dht.getValue(), - 'peer': this.peer.getValue(), - 'tracker': this.tracker.getValue(), - 'web_seed': this.web_seed.getValue() - } - }, - - setValue: function(value) { - for (var proxy in value) { - this[proxy].setValue(value[proxy]); - } - }, - - onProxyChange: function(field, newValue, oldValue) { - var newValues = this.getValue(); - var oldValues = Ext.apply({}, newValues); - oldValues[field.getName()] = oldValue; - - this.fireEvent('change', this, newValues, oldValues); - } + constructor: function(config) { + config = Ext.apply({ + border: false, + title: _('Proxy'), + layout: 'form' + }, config); + Deluge.preferences.Proxy.superclass.constructor.call(this, config); + }, + + initComponent: function() { + Deluge.preferences.Proxy.superclass.initComponent.call(this); + this.peer = this.add(new Deluge.preferences.ProxyField({ + title: _('Peer'), + name: 'peer' + })); + this.peer.on('change', this.onProxyChange, this); + + this.web_seed = this.add(new Deluge.preferences.ProxyField({ + title: _('Web Seed'), + name: 'web_seed' + })); + this.web_seed.on('change', this.onProxyChange, this); + + this.tracker = this.add(new Deluge.preferences.ProxyField({ + title: _('Tracker'), + name: 'tracker' + })); + this.tracker.on('change', this.onProxyChange, this); + + this.dht = this.add(new Deluge.preferences.ProxyField({ + title: _('DHT'), + name: 'dht' + })); + this.dht.on('change', this.onProxyChange, this); + + deluge.preferences.getOptionsManager().bind('proxies', this); + }, + + getValue: function() { + return { + 'dht': this.dht.getValue(), + 'peer': this.peer.getValue(), + 'tracker': this.tracker.getValue(), + 'web_seed': this.web_seed.getValue() + } + }, + + setValue: function(value) { + for (var proxy in value) { + this[proxy].setValue(value[proxy]); + } + }, + + onProxyChange: function(field, newValue, oldValue) { + var newValues = this.getValue(); + var oldValues = Ext.apply({}, newValues); + oldValues[field.getName()] = oldValue; + + this.fireEvent('change', this, newValues, oldValues); + } }); diff --git a/deluge/ui/web/js/deluge-all/preferences/QueuePage.js b/deluge/ui/web/js/deluge-all/preferences/QueuePage.js index 15ca0eb60..cee5a1557 100644 --- a/deluge/ui/web/js/deluge-all/preferences/QueuePage.js +++ b/deluge/ui/web/js/deluge-all/preferences/QueuePage.js @@ -37,166 +37,166 @@ Ext.namespace('Deluge.preferences'); */ Deluge.preferences.Queue = Ext.extend(Ext.form.FormPanel, { - border: false, - title: _('Queue'), - layout: 'form', - - initComponent: function() { - Deluge.preferences.Queue.superclass.initComponent.call(this); - - var om = deluge.preferences.getOptionsManager(); - - var fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('General'), - style: 'padding-top: 5px;', - autoHeight: true, - labelWidth: 1, - defaultType: 'checkbox' - }); - om.bind('queue_new_to_top', fieldset.add({ - fieldLabel: '', - labelSeparator: '', - height: 22, - boxLabel: _('Queue new torrents to top'), - name: 'queue_new_to_top' - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Active Torrents'), - autoHeight: true, - labelWidth: 150, - defaultType: 'spinnerfield', - style: 'margin-bottom: 0px; padding-bottom: 0px;' - }); - om.bind('max_active_limit', fieldset.add({ - fieldLabel: _('Total Active'), - name: 'max_active_limit', - value: 8, - width: 80, - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - om.bind('max_active_downloading', fieldset.add({ - fieldLabel: _('Total Active Downloading'), - name: 'max_active_downloading', - value: 3, - width: 80, - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - om.bind('max_active_seeding', fieldset.add({ - fieldLabel: _('Total Active Seeding'), - name: 'max_active_seeding', - value: 5, - width: 80, - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - om.bind('dont_count_slow_torrents', fieldset.add({ - xtype: 'checkbox', - name: 'dont_count_slow_torrents', - height: 40, - hideLabel: true, - boxLabel: _('Do not count slow torrents') - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - title: _('Seeding'), - autoHeight: true, - labelWidth: 150, - defaultType: 'spinnerfield', - style: 'margin-bottom: 0px; padding-bottom: 0px; margin-top: 0; padding-top: 0;' - }); - om.bind('share_ratio_limit', fieldset.add({ - fieldLabel: _('Share Ratio Limit'), - name: 'share_ratio_limit', - value: 8, - width: 80, - incrementValue: 0.1, - minValue: -1, - maxValue: 99999, - alternateIncrementValue: 1, - decimalPrecision: 2 - })); - om.bind('seed_time_ratio_limit', fieldset.add({ - fieldLabel: _('Share Time Ratio'), - name: 'seed_time_ratio_limit', - value: 3, - width: 80, - incrementValue: 0.1, - minValue: -1, - maxValue: 99999, - alternateIncrementValue: 1, - decimalPrecision: 2 - })); - om.bind('seed_time_limit', fieldset.add({ - fieldLabel: _('Seed Time (m)'), - name: 'seed_time_limit', - value: 5, - width: 80, - decimalPrecision: 0, - minValue: -1, - maxValue: 99999 - })); - - fieldset = this.add({ - xtype: 'fieldset', - border: false, - autoHeight: true, - - layout: 'table', - layoutConfig: {columns: 2}, - labelWidth: 0, - defaultType: 'checkbox', - - defaults: { - fieldLabel: '', - labelSeparator: '' - } - }); - this.stopAtRatio = fieldset.add({ - name: 'stop_seed_at_ratio', - boxLabel: _('Stop seeding when share ratio reaches:') - }); - this.stopAtRatio.on('check', this.onStopRatioCheck, this); - om.bind('stop_seed_at_ratio', this.stopAtRatio); - - this.stopRatio = fieldset.add({ - xtype: 'spinnerfield', - name: 'stop_seed_ratio', - ctCls: 'x-deluge-indent-checkbox', - disabled: true, - value: '2.0', - width: 60, - incrementValue: 0.1, - minValue: -1, - maxValue: 99999, - alternateIncrementValue: 1, - decimalPrecision: 2 - }); - om.bind('stop_seed_ratio', this.stopRatio); - - this.removeAtRatio = fieldset.add({ - name: 'remove_seed_at_ratio', - ctCls: 'x-deluge-indent-checkbox', - boxLabel: _('Remove torrent when share ratio is reached'), - disabled: true, - colspan: 2 - }); - om.bind('remove_seed_at_ratio', this.removeAtRatio); - }, - - onStopRatioCheck: function(e, checked) { - this.stopRatio.setDisabled(!checked); - this.removeAtRatio.setDisabled(!checked); - } + border: false, + title: _('Queue'), + layout: 'form', + + initComponent: function() { + Deluge.preferences.Queue.superclass.initComponent.call(this); + + var om = deluge.preferences.getOptionsManager(); + + var fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('General'), + style: 'padding-top: 5px;', + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + om.bind('queue_new_to_top', fieldset.add({ + fieldLabel: '', + labelSeparator: '', + height: 22, + boxLabel: _('Queue new torrents to top'), + name: 'queue_new_to_top' + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Active Torrents'), + autoHeight: true, + labelWidth: 150, + defaultType: 'spinnerfield', + style: 'margin-bottom: 0px; padding-bottom: 0px;' + }); + om.bind('max_active_limit', fieldset.add({ + fieldLabel: _('Total Active'), + name: 'max_active_limit', + value: 8, + width: 80, + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + om.bind('max_active_downloading', fieldset.add({ + fieldLabel: _('Total Active Downloading'), + name: 'max_active_downloading', + value: 3, + width: 80, + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + om.bind('max_active_seeding', fieldset.add({ + fieldLabel: _('Total Active Seeding'), + name: 'max_active_seeding', + value: 5, + width: 80, + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + om.bind('dont_count_slow_torrents', fieldset.add({ + xtype: 'checkbox', + name: 'dont_count_slow_torrents', + height: 40, + hideLabel: true, + boxLabel: _('Do not count slow torrents') + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + title: _('Seeding'), + autoHeight: true, + labelWidth: 150, + defaultType: 'spinnerfield', + style: 'margin-bottom: 0px; padding-bottom: 0px; margin-top: 0; padding-top: 0;' + }); + om.bind('share_ratio_limit', fieldset.add({ + fieldLabel: _('Share Ratio Limit'), + name: 'share_ratio_limit', + value: 8, + width: 80, + incrementValue: 0.1, + minValue: -1, + maxValue: 99999, + alternateIncrementValue: 1, + decimalPrecision: 2 + })); + om.bind('seed_time_ratio_limit', fieldset.add({ + fieldLabel: _('Share Time Ratio'), + name: 'seed_time_ratio_limit', + value: 3, + width: 80, + incrementValue: 0.1, + minValue: -1, + maxValue: 99999, + alternateIncrementValue: 1, + decimalPrecision: 2 + })); + om.bind('seed_time_limit', fieldset.add({ + fieldLabel: _('Seed Time (m)'), + name: 'seed_time_limit', + value: 5, + width: 80, + decimalPrecision: 0, + minValue: -1, + maxValue: 99999 + })); + + fieldset = this.add({ + xtype: 'fieldset', + border: false, + autoHeight: true, + + layout: 'table', + layoutConfig: {columns: 2}, + labelWidth: 0, + defaultType: 'checkbox', + + defaults: { + fieldLabel: '', + labelSeparator: '' + } + }); + this.stopAtRatio = fieldset.add({ + name: 'stop_seed_at_ratio', + boxLabel: _('Stop seeding when share ratio reaches:') + }); + this.stopAtRatio.on('check', this.onStopRatioCheck, this); + om.bind('stop_seed_at_ratio', this.stopAtRatio); + + this.stopRatio = fieldset.add({ + xtype: 'spinnerfield', + name: 'stop_seed_ratio', + ctCls: 'x-deluge-indent-checkbox', + disabled: true, + value: '2.0', + width: 60, + incrementValue: 0.1, + minValue: -1, + maxValue: 99999, + alternateIncrementValue: 1, + decimalPrecision: 2 + }); + om.bind('stop_seed_ratio', this.stopRatio); + + this.removeAtRatio = fieldset.add({ + name: 'remove_seed_at_ratio', + ctCls: 'x-deluge-indent-checkbox', + boxLabel: _('Remove torrent when share ratio is reached'), + disabled: true, + colspan: 2 + }); + om.bind('remove_seed_at_ratio', this.removeAtRatio); + }, + + onStopRatioCheck: function(e, checked) { + this.stopRatio.setDisabled(!checked); + this.removeAtRatio.setDisabled(!checked); + } }); diff --git a/deluge/ui/web/js/ext-extensions/JSLoader.js b/deluge/ui/web/js/ext-extensions/JSLoader.js index e3b01515b..7d397244d 100644 --- a/deluge/ui/web/js/ext-extensions/JSLoader.js +++ b/deluge/ui/web/js/ext-extensions/JSLoader.js @@ -1,38 +1,38 @@ Ext.ux.JSLoader = function(options) { - Ext.ux.JSLoader.scripts[++Ext.ux.JSLoader.index] = { - url: options.url, - success: true, - jsLoadObj: null, - options: options, - onLoad: options.onLoad || Ext.emptyFn, - onError: options.onError || Ext.ux.JSLoader.stdError, - scope: options.scope || this - }; + Ext.ux.JSLoader.scripts[++Ext.ux.JSLoader.index] = { + url: options.url, + success: true, + jsLoadObj: null, + options: options, + onLoad: options.onLoad || Ext.emptyFn, + onError: options.onError || Ext.ux.JSLoader.stdError, + scope: options.scope || this + }; - Ext.Ajax.request({ - url: options.url, - scriptIndex: Ext.ux.JSLoader.index, - success: function(response, options) { - var script = Ext.ux.JSLoader.scripts[options.scriptIndex]; - try { - eval(response.responseText); - } catch(e) { - script.success = false; - script.onError(script.options, e); - } - if (script.success) { - script.onLoad.call(script.scope, script.options); - } - }, - failure: function(response, options) { - var script = Ext.ux.JSLoader.scripts[options.scriptIndex]; - script.success = false; - script.onError(script.options, response.status); - } - }); + Ext.Ajax.request({ + url: options.url, + scriptIndex: Ext.ux.JSLoader.index, + success: function(response, options) { + var script = Ext.ux.JSLoader.scripts[options.scriptIndex]; + try { + eval(response.responseText); + } catch(e) { + script.success = false; + script.onError(script.options, e); + } + if (script.success) { + script.onLoad.call(script.scope, script.options); + } + }, + failure: function(response, options) { + var script = Ext.ux.JSLoader.scripts[options.scriptIndex]; + script.success = false; + script.onError(script.options, response.status); + } + }); } Ext.ux.JSLoader.index = 0; Ext.ux.JSLoader.scripts = []; Ext.ux.JSLoader.stdError = function(options, e) { - window.alert('Error loading script:\n\n' + options.url + '\n\nstatus: ' + e); + window.alert('Error loading script:\n\n' + options.url + '\n\nstatus: ' + e); } diff --git a/deluge/ui/web/js/ext-extensions/Spinner.js b/deluge/ui/web/js/ext-extensions/Spinner.js index 3ce55bf2c..292fbbb3e 100644 --- a/deluge/ui/web/js/ext-extensions/Spinner.js +++ b/deluge/ui/web/js/ext-extensions/Spinner.js @@ -10,428 +10,428 @@ * Creates a Spinner control utilized by Ext.ux.form.SpinnerField */ Ext.ux.Spinner = Ext.extend(Ext.util.Observable, { - incrementValue: 1, - alternateIncrementValue: 5, - triggerClass: 'x-form-spinner-trigger', - splitterClass: 'x-form-spinner-splitter', - alternateKey: Ext.EventObject.shiftKey, - defaultValue: 0, - accelerate: false, + incrementValue: 1, + alternateIncrementValue: 5, + triggerClass: 'x-form-spinner-trigger', + splitterClass: 'x-form-spinner-splitter', + alternateKey: Ext.EventObject.shiftKey, + defaultValue: 0, + accelerate: false, - constructor: function(config){ - Ext.ux.Spinner.superclass.constructor.call(this, config); - Ext.apply(this, config); - this.mimicing = false; - }, + constructor: function(config){ + Ext.ux.Spinner.superclass.constructor.call(this, config); + Ext.apply(this, config); + this.mimicing = false; + }, - init: function(field){ - this.field = field; + init: function(field){ + this.field = field; - field.afterMethod('onRender', this.doRender, this); - field.afterMethod('onEnable', this.doEnable, this); - field.afterMethod('onDisable', this.doDisable, this); - field.afterMethod('afterRender', this.doAfterRender, this); - field.afterMethod('onResize', this.doResize, this); - field.afterMethod('onFocus', this.doFocus, this); - field.beforeMethod('onDestroy', this.doDestroy, this); - }, + field.afterMethod('onRender', this.doRender, this); + field.afterMethod('onEnable', this.doEnable, this); + field.afterMethod('onDisable', this.doDisable, this); + field.afterMethod('afterRender', this.doAfterRender, this); + field.afterMethod('onResize', this.doResize, this); + field.afterMethod('onFocus', this.doFocus, this); + field.beforeMethod('onDestroy', this.doDestroy, this); + }, - doRender: function(ct, position){ - var el = this.el = this.field.getEl(); - var f = this.field; + doRender: function(ct, position){ + var el = this.el = this.field.getEl(); + var f = this.field; - if (!f.wrap) { - f.wrap = this.wrap = el.wrap({ - cls: "x-form-field-wrap" - }); - } - else { - this.wrap = f.wrap.addClass('x-form-field-wrap'); - } + if (!f.wrap) { + f.wrap = this.wrap = el.wrap({ + cls: "x-form-field-wrap" + }); + } + else { + this.wrap = f.wrap.addClass('x-form-field-wrap'); + } - this.trigger = this.wrap.createChild({ - tag: "img", - src: Ext.BLANK_IMAGE_URL, - cls: "x-form-trigger " + this.triggerClass - }); + this.trigger = this.wrap.createChild({ + tag: "img", + src: Ext.BLANK_IMAGE_URL, + cls: "x-form-trigger " + this.triggerClass + }); - if (!f.width) { - this.wrap.setWidth(el.getWidth() + this.trigger.getWidth()); - } + if (!f.width) { + this.wrap.setWidth(el.getWidth() + this.trigger.getWidth()); + } - this.splitter = this.wrap.createChild({ - tag: 'div', - cls: this.splitterClass, - style: 'width:13px; height:2px;' - }); - this.splitter.setRight((Ext.isIE) ? 1 : 2).setTop(10).show(); + this.splitter = this.wrap.createChild({ + tag: 'div', + cls: this.splitterClass, + style: 'width:13px; height:2px;' + }); + this.splitter.setRight((Ext.isIE) ? 1 : 2).setTop(10).show(); - this.proxy = this.trigger.createProxy('', this.splitter, true); - this.proxy.addClass("x-form-spinner-proxy"); - this.proxy.setStyle('left', '0px'); - this.proxy.setSize(14, 1); - this.proxy.hide(); - this.dd = new Ext.dd.DDProxy(this.splitter.dom.id, "SpinnerDrag", { - dragElId: this.proxy.id - }); + this.proxy = this.trigger.createProxy('', this.splitter, true); + this.proxy.addClass("x-form-spinner-proxy"); + this.proxy.setStyle('left', '0px'); + this.proxy.setSize(14, 1); + this.proxy.hide(); + this.dd = new Ext.dd.DDProxy(this.splitter.dom.id, "SpinnerDrag", { + dragElId: this.proxy.id + }); - this.initTrigger(); - this.initSpinner(); - }, + this.initTrigger(); + this.initSpinner(); + }, - doAfterRender: function(){ - var y; - if (Ext.isIE && this.el.getY() != (y = this.trigger.getY())) { - this.el.position(); - this.el.setY(y); - } - }, + doAfterRender: function(){ + var y; + if (Ext.isIE && this.el.getY() != (y = this.trigger.getY())) { + this.el.position(); + this.el.setY(y); + } + }, - doEnable: function(){ - if (this.wrap) { - this.wrap.removeClass(this.field.disabledClass); - } - }, + doEnable: function(){ + if (this.wrap) { + this.wrap.removeClass(this.field.disabledClass); + } + }, - doDisable: function(){ - if (this.wrap) { - this.wrap.addClass(this.field.disabledClass); - this.el.removeClass(this.field.disabledClass); - } - }, + doDisable: function(){ + if (this.wrap) { + this.wrap.addClass(this.field.disabledClass); + this.el.removeClass(this.field.disabledClass); + } + }, - doResize: function(w, h){ - if (typeof w == 'number') { - this.el.setWidth(w - this.trigger.getWidth()); - } - this.wrap.setWidth(this.el.getWidth() + this.trigger.getWidth()); - }, + doResize: function(w, h){ + if (typeof w == 'number') { + this.el.setWidth(w - this.trigger.getWidth()); + } + this.wrap.setWidth(this.el.getWidth() + this.trigger.getWidth()); + }, - doFocus: function(){ - if (!this.mimicing) { - this.wrap.addClass('x-trigger-wrap-focus'); - this.mimicing = true; - Ext.get(Ext.isIE ? document.body : document).on("mousedown", this.mimicBlur, this, { - delay: 10 - }); - this.el.on('keydown', this.checkTab, this); - } - }, + doFocus: function(){ + if (!this.mimicing) { + this.wrap.addClass('x-trigger-wrap-focus'); + this.mimicing = true; + Ext.get(Ext.isIE ? document.body : document).on("mousedown", this.mimicBlur, this, { + delay: 10 + }); + this.el.on('keydown', this.checkTab, this); + } + }, - // private - checkTab: function(e){ - if (e.getKey() == e.TAB) { - this.triggerBlur(); - } - }, + // private + checkTab: function(e){ + if (e.getKey() == e.TAB) { + this.triggerBlur(); + } + }, - // private - mimicBlur: function(e){ - if (!this.wrap.contains(e.target) && this.field.validateBlur(e)) { - this.triggerBlur(); - } - }, + // private + mimicBlur: function(e){ + if (!this.wrap.contains(e.target) && this.field.validateBlur(e)) { + this.triggerBlur(); + } + }, - // private - triggerBlur: function(){ - this.mimicing = false; - Ext.get(Ext.isIE ? document.body : document).un("mousedown", this.mimicBlur, this); - this.el.un("keydown", this.checkTab, this); - this.field.beforeBlur(); - this.wrap.removeClass('x-trigger-wrap-focus'); - this.field.onBlur.call(this.field); - }, + // private + triggerBlur: function(){ + this.mimicing = false; + Ext.get(Ext.isIE ? document.body : document).un("mousedown", this.mimicBlur, this); + this.el.un("keydown", this.checkTab, this); + this.field.beforeBlur(); + this.wrap.removeClass('x-trigger-wrap-focus'); + this.field.onBlur.call(this.field); + }, - initTrigger: function(){ - this.trigger.addClassOnOver('x-form-trigger-over'); - this.trigger.addClassOnClick('x-form-trigger-click'); - }, + initTrigger: function(){ + this.trigger.addClassOnOver('x-form-trigger-over'); + this.trigger.addClassOnClick('x-form-trigger-click'); + }, - initSpinner: function(){ - this.field.addEvents({ - 'spin': true, - 'spinup': true, - 'spindown': true - }); + initSpinner: function(){ + this.field.addEvents({ + 'spin': true, + 'spinup': true, + 'spindown': true + }); - this.keyNav = new Ext.KeyNav(this.el, { - "up": function(e){ - e.preventDefault(); - this.onSpinUp(); - }, + this.keyNav = new Ext.KeyNav(this.el, { + "up": function(e){ + e.preventDefault(); + this.onSpinUp(); + }, - "down": function(e){ - e.preventDefault(); - this.onSpinDown(); - }, + "down": function(e){ + e.preventDefault(); + this.onSpinDown(); + }, - "pageUp": function(e){ - e.preventDefault(); - this.onSpinUpAlternate(); - }, + "pageUp": function(e){ + e.preventDefault(); + this.onSpinUpAlternate(); + }, - "pageDown": function(e){ - e.preventDefault(); - this.onSpinDownAlternate(); - }, + "pageDown": function(e){ + e.preventDefault(); + this.onSpinDownAlternate(); + }, - scope: this - }); + scope: this + }); - this.repeater = new Ext.util.ClickRepeater(this.trigger, { - accelerate: this.accelerate - }); - this.field.mon(this.repeater, "click", this.onTriggerClick, this, { - preventDefault: true - }); + this.repeater = new Ext.util.ClickRepeater(this.trigger, { + accelerate: this.accelerate + }); + this.field.mon(this.repeater, "click", this.onTriggerClick, this, { + preventDefault: true + }); - this.field.mon(this.trigger, { - mouseover: this.onMouseOver, - mouseout: this.onMouseOut, - mousemove: this.onMouseMove, - mousedown: this.onMouseDown, - mouseup: this.onMouseUp, - scope: this, - preventDefault: true - }); + this.field.mon(this.trigger, { + mouseover: this.onMouseOver, + mouseout: this.onMouseOut, + mousemove: this.onMouseMove, + mousedown: this.onMouseDown, + mouseup: this.onMouseUp, + scope: this, + preventDefault: true + }); - this.field.mon(this.wrap, "mousewheel", this.handleMouseWheel, this); + this.field.mon(this.wrap, "mousewheel", this.handleMouseWheel, this); - this.dd.setXConstraint(0, 0, 10) - this.dd.setYConstraint(1500, 1500, 10); - this.dd.endDrag = this.endDrag.createDelegate(this); - this.dd.startDrag = this.startDrag.createDelegate(this); - this.dd.onDrag = this.onDrag.createDelegate(this); - }, + this.dd.setXConstraint(0, 0, 10) + this.dd.setYConstraint(1500, 1500, 10); + this.dd.endDrag = this.endDrag.createDelegate(this); + this.dd.startDrag = this.startDrag.createDelegate(this); + this.dd.onDrag = this.onDrag.createDelegate(this); + }, - onMouseOver: function(){ - if (this.disabled) { - return; - } - var middle = this.getMiddle(); - this.tmpHoverClass = (Ext.EventObject.getPageY() < middle) ? 'x-form-spinner-overup' : 'x-form-spinner-overdown'; - this.trigger.addClass(this.tmpHoverClass); - }, + onMouseOver: function(){ + if (this.disabled) { + return; + } + var middle = this.getMiddle(); + this.tmpHoverClass = (Ext.EventObject.getPageY() < middle) ? 'x-form-spinner-overup' : 'x-form-spinner-overdown'; + this.trigger.addClass(this.tmpHoverClass); + }, - //private - onMouseOut: function(){ - this.trigger.removeClass(this.tmpHoverClass); - }, + //private + onMouseOut: function(){ + this.trigger.removeClass(this.tmpHoverClass); + }, - //private - onMouseMove: function(){ - if (this.disabled) { - return; - } - var middle = this.getMiddle(); - if (((Ext.EventObject.getPageY() > middle) && this.tmpHoverClass == "x-form-spinner-overup") || - ((Ext.EventObject.getPageY() < middle) && this.tmpHoverClass == "x-form-spinner-overdown")) { - } - }, + //private + onMouseMove: function(){ + if (this.disabled) { + return; + } + var middle = this.getMiddle(); + if (((Ext.EventObject.getPageY() > middle) && this.tmpHoverClass == "x-form-spinner-overup") || + ((Ext.EventObject.getPageY() < middle) && this.tmpHoverClass == "x-form-spinner-overdown")) { + } + }, - //private - onMouseDown: function(){ - if (this.disabled) { - return; - } - var middle = this.getMiddle(); - this.tmpClickClass = (Ext.EventObject.getPageY() < middle) ? 'x-form-spinner-clickup' : 'x-form-spinner-clickdown'; - this.trigger.addClass(this.tmpClickClass); - }, + //private + onMouseDown: function(){ + if (this.disabled) { + return; + } + var middle = this.getMiddle(); + this.tmpClickClass = (Ext.EventObject.getPageY() < middle) ? 'x-form-spinner-clickup' : 'x-form-spinner-clickdown'; + this.trigger.addClass(this.tmpClickClass); + }, - //private - onMouseUp: function(){ - this.trigger.removeClass(this.tmpClickClass); - }, + //private + onMouseUp: function(){ + this.trigger.removeClass(this.tmpClickClass); + }, - //private - onTriggerClick: function(){ - if (this.disabled || this.el.dom.readOnly) { - return; - } - var middle = this.getMiddle(); - var ud = (Ext.EventObject.getPageY() < middle) ? 'Up' : 'Down'; - this['onSpin' + ud](); - }, + //private + onTriggerClick: function(){ + if (this.disabled || this.el.dom.readOnly) { + return; + } + var middle = this.getMiddle(); + var ud = (Ext.EventObject.getPageY() < middle) ? 'Up' : 'Down'; + this['onSpin' + ud](); + }, - //private - getMiddle: function(){ - var t = this.trigger.getTop(); - var h = this.trigger.getHeight(); - var middle = t + (h / 2); - return middle; - }, + //private + getMiddle: function(){ + var t = this.trigger.getTop(); + var h = this.trigger.getHeight(); + var middle = t + (h / 2); + return middle; + }, - //private - //checks if control is allowed to spin - isSpinnable: function(){ - if (this.disabled || this.el.dom.readOnly) { - Ext.EventObject.preventDefault(); //prevent scrolling when disabled/readonly - return false; - } - return true; - }, + //private + //checks if control is allowed to spin + isSpinnable: function(){ + if (this.disabled || this.el.dom.readOnly) { + Ext.EventObject.preventDefault(); //prevent scrolling when disabled/readonly + return false; + } + return true; + }, - handleMouseWheel: function(e){ - //disable scrolling when not focused - if (this.wrap.hasClass('x-trigger-wrap-focus') == false) { - return; - } + handleMouseWheel: function(e){ + //disable scrolling when not focused + if (this.wrap.hasClass('x-trigger-wrap-focus') == false) { + return; + } - var delta = e.getWheelDelta(); - if (delta > 0) { - this.onSpinUp(); - e.stopEvent(); - } - else - if (delta < 0) { - this.onSpinDown(); - e.stopEvent(); - } - }, + var delta = e.getWheelDelta(); + if (delta > 0) { + this.onSpinUp(); + e.stopEvent(); + } + else + if (delta < 0) { + this.onSpinDown(); + e.stopEvent(); + } + }, - //private - startDrag: function(){ - this.proxy.show(); - this._previousY = Ext.fly(this.dd.getDragEl()).getTop(); - }, + //private + startDrag: function(){ + this.proxy.show(); + this._previousY = Ext.fly(this.dd.getDragEl()).getTop(); + }, - //private - endDrag: function(){ - this.proxy.hide(); - }, + //private + endDrag: function(){ + this.proxy.hide(); + }, - //private - onDrag: function(){ - if (this.disabled) { - return; - } - var y = Ext.fly(this.dd.getDragEl()).getTop(); - var ud = ''; + //private + onDrag: function(){ + if (this.disabled) { + return; + } + var y = Ext.fly(this.dd.getDragEl()).getTop(); + var ud = ''; - if (this._previousY > y) { - ud = 'Up'; - } //up - if (this._previousY < y) { - ud = 'Down'; - } //down - if (ud != '') { - this['onSpin' + ud](); - } + if (this._previousY > y) { + ud = 'Up'; + } //up + if (this._previousY < y) { + ud = 'Down'; + } //down + if (ud != '') { + this['onSpin' + ud](); + } - this._previousY = y; - }, + this._previousY = y; + }, - //private - onSpinUp: function(){ - if (this.isSpinnable() == false) { - return; - } - if (Ext.EventObject.shiftKey == true) { - this.onSpinUpAlternate(); - return; - } - else { - this.spin(false, false); - } - this.field.fireEvent("spin", this); - this.field.fireEvent("spinup", this); - }, + //private + onSpinUp: function(){ + if (this.isSpinnable() == false) { + return; + } + if (Ext.EventObject.shiftKey == true) { + this.onSpinUpAlternate(); + return; + } + else { + this.spin(false, false); + } + this.field.fireEvent("spin", this); + this.field.fireEvent("spinup", this); + }, - //private - onSpinDown: function(){ - if (this.isSpinnable() == false) { - return; - } - if (Ext.EventObject.shiftKey == true) { - this.onSpinDownAlternate(); - return; - } - else { - this.spin(true, false); - } - this.field.fireEvent("spin", this); - this.field.fireEvent("spindown", this); - }, + //private + onSpinDown: function(){ + if (this.isSpinnable() == false) { + return; + } + if (Ext.EventObject.shiftKey == true) { + this.onSpinDownAlternate(); + return; + } + else { + this.spin(true, false); + } + this.field.fireEvent("spin", this); + this.field.fireEvent("spindown", this); + }, - //private - onSpinUpAlternate: function(){ - if (this.isSpinnable() == false) { - return; - } - this.spin(false, true); - this.field.fireEvent("spin", this); - this.field.fireEvent("spinup", this); - }, + //private + onSpinUpAlternate: function(){ + if (this.isSpinnable() == false) { + return; + } + this.spin(false, true); + this.field.fireEvent("spin", this); + this.field.fireEvent("spinup", this); + }, - //private - onSpinDownAlternate: function(){ - if (this.isSpinnable() == false) { - return; - } - this.spin(true, true); - this.field.fireEvent("spin", this); - this.field.fireEvent("spindown", this); - }, + //private + onSpinDownAlternate: function(){ + if (this.isSpinnable() == false) { + return; + } + this.spin(true, true); + this.field.fireEvent("spin", this); + this.field.fireEvent("spindown", this); + }, - spin: function(down, alternate){ - var v = parseFloat(this.field.getValue()); - var incr = (alternate == true) ? this.alternateIncrementValue : this.incrementValue; - (down == true) ? v -= incr : v += incr; + spin: function(down, alternate){ + var v = parseFloat(this.field.getValue()); + var incr = (alternate == true) ? this.alternateIncrementValue : this.incrementValue; + (down == true) ? v -= incr : v += incr; - v = (isNaN(v)) ? this.defaultValue : v; - v = this.fixBoundries(v); - this.field.setRawValue(v); - }, + v = (isNaN(v)) ? this.defaultValue : v; + v = this.fixBoundries(v); + this.field.setRawValue(v); + }, - fixBoundries: function(value){ - var v = value; + fixBoundries: function(value){ + var v = value; - if (this.field.minValue != undefined && v < this.field.minValue) { - v = this.field.minValue; - } - if (this.field.maxValue != undefined && v > this.field.maxValue) { - v = this.field.maxValue; - } + if (this.field.minValue != undefined && v < this.field.minValue) { + v = this.field.minValue; + } + if (this.field.maxValue != undefined && v > this.field.maxValue) { + v = this.field.maxValue; + } - return this.fixPrecision(v); - }, + return this.fixPrecision(v); + }, - // private - fixPrecision: function(value){ - var nan = isNaN(value); - if (!this.field.allowDecimals || this.field.decimalPrecision == -1 || nan || !value) { - return nan ? '' : value; - } - return parseFloat(parseFloat(value).toFixed(this.field.decimalPrecision)); - }, + // private + fixPrecision: function(value){ + var nan = isNaN(value); + if (!this.field.allowDecimals || this.field.decimalPrecision == -1 || nan || !value) { + return nan ? '' : value; + } + return parseFloat(parseFloat(value).toFixed(this.field.decimalPrecision)); + }, - doDestroy: function(){ - if (this.trigger) { - this.trigger.remove(); - } - if (this.wrap) { - this.wrap.remove(); - delete this.field.wrap; - } + doDestroy: function(){ + if (this.trigger) { + this.trigger.remove(); + } + if (this.wrap) { + this.wrap.remove(); + delete this.field.wrap; + } - if (this.splitter) { - this.splitter.remove(); - } + if (this.splitter) { + this.splitter.remove(); + } - if (this.dd) { - this.dd.unreg(); - this.dd = null; - } + if (this.dd) { + this.dd.unreg(); + this.dd = null; + } - if (this.proxy) { - this.proxy.remove(); - } + if (this.proxy) { + this.proxy.remove(); + } - if (this.repeater) { - this.repeater.purgeListeners(); - } - } + if (this.repeater) { + this.repeater.purgeListeners(); + } + } }); //backwards compat diff --git a/deluge/ui/web/js/ext-extensions/StatusBar.js b/deluge/ui/web/js/ext-extensions/StatusBar.js index 8d4eb18b9..07b85c6c5 100644 --- a/deluge/ui/web/js/ext-extensions/StatusBar.js +++ b/deluge/ui/web/js/ext-extensions/StatusBar.js @@ -339,9 +339,9 @@ statusBar.setStatus({ scope: this, callback: function(){ this.setStatus({ - text: text, - iconCls: iconCls - }); + text: text, + iconCls: iconCls + }); this.statusEl.el.show(); } @@ -349,10 +349,10 @@ statusBar.setStatus({ }else{ // hide/show the el to avoid jumpy text or icon this.statusEl.hide(); - this.setStatus({ - text: text, - iconCls: iconCls - }); + this.setStatus({ + text: text, + iconCls: iconCls + }); this.statusEl.show(); } return this; @@ -391,14 +391,14 @@ statusBar.setStatus({ cls = cls || ''; if(this.rendered){ - if(this.currIconCls){ - this.statusEl.removeClass(this.currIconCls); - this.currIconCls = null; - } - if(cls.length > 0){ - this.statusEl.addClass(cls); - this.currIconCls = cls; - } + if(this.currIconCls){ + this.statusEl.removeClass(this.currIconCls); + this.currIconCls = null; + } + if(cls.length > 0){ + this.statusEl.addClass(cls); + this.currIconCls = cls; + } }else{ this.currIconCls = cls; } diff --git a/deluge/ui/web/js/ext-extensions/form/RadioGroupFix.js b/deluge/ui/web/js/ext-extensions/form/RadioGroupFix.js index 058cc012f..f5a325829 100644 --- a/deluge/ui/web/js/ext-extensions/form/RadioGroupFix.js +++ b/deluge/ui/web/js/ext-extensions/form/RadioGroupFix.js @@ -34,38 +34,38 @@ Ext.override(Ext.form.RadioGroup, { afterRender: function() { - this.items.each(function(i) { - this.relayEvents(i, ['check']); - }, this); - if (this.lazyValue) { - this.setValue(this.value); - delete this.value; - delete this.lazyValue; - } - Ext.form.RadioGroup.superclass.afterRender.call(this) + this.items.each(function(i) { + this.relayEvents(i, ['check']); + }, this); + if (this.lazyValue) { + this.setValue(this.value); + delete this.value; + delete this.lazyValue; + } + Ext.form.RadioGroup.superclass.afterRender.call(this) }, getName: function() { - return this.items.first().getName(); + return this.items.first().getName(); }, getValue: function() { - return this.items.first().getGroupValue(); + return this.items.first().getGroupValue(); }, setValue: function(v) { - if (!this.items.each) { - this.value = v; - this.lazyValue = true; - return; - } - this.items.each(function(item) { - if (item.rendered) { - var checked = (item.el.getValue() == String(v)); - item.el.dom.checked = checked; - item.el.dom.defaultChecked = checked; - item.wrap[checked ? 'addClass' : 'removeClass'](item.checkedCls); - } - }); + if (!this.items.each) { + this.value = v; + this.lazyValue = true; + return; + } + this.items.each(function(item) { + if (item.rendered) { + var checked = (item.el.getValue() == String(v)); + item.el.dom.checked = checked; + item.el.dom.defaultChecked = checked; + item.wrap[checked ? 'addClass' : 'removeClass'](item.checkedCls); + } + }); } }); diff --git a/deluge/ui/web/js/ext-extensions/form/SpinnerField.js b/deluge/ui/web/js/ext-extensions/form/SpinnerField.js index 96b2a16ca..951c36985 100644 --- a/deluge/ui/web/js/ext-extensions/form/SpinnerField.js +++ b/deluge/ui/web/js/ext-extensions/form/SpinnerField.js @@ -13,46 +13,46 @@ Ext.ns('Ext.ux.form'); * @xtype spinnerfield */ Ext.ux.form.SpinnerField = Ext.extend(Ext.form.NumberField, { - actionMode: 'wrap', - deferHeight: true, - autoSize: Ext.emptyFn, - onBlur: Ext.emptyFn, - adjustSize: Ext.BoxComponent.prototype.adjustSize, + actionMode: 'wrap', + deferHeight: true, + autoSize: Ext.emptyFn, + onBlur: Ext.emptyFn, + adjustSize: Ext.BoxComponent.prototype.adjustSize, - constructor: function(config) { - var spinnerConfig = Ext.copyTo({}, config, 'incrementValue,alternateIncrementValue,accelerate,defaultValue,triggerClass,splitterClass'); + constructor: function(config) { + var spinnerConfig = Ext.copyTo({}, config, 'incrementValue,alternateIncrementValue,accelerate,defaultValue,triggerClass,splitterClass'); - var spl = this.spinner = new Ext.ux.Spinner(spinnerConfig); + var spl = this.spinner = new Ext.ux.Spinner(spinnerConfig); - var plugins = config.plugins - ? (Ext.isArray(config.plugins) - ? config.plugins.push(spl) - : [config.plugins, spl]) - : spl; + var plugins = config.plugins + ? (Ext.isArray(config.plugins) + ? config.plugins.push(spl) + : [config.plugins, spl]) + : spl; - Ext.ux.form.SpinnerField.superclass.constructor.call(this, Ext.apply(config, {plugins: plugins})); - }, + Ext.ux.form.SpinnerField.superclass.constructor.call(this, Ext.apply(config, {plugins: plugins})); + }, - // private - getResizeEl: function(){ - return this.wrap; - }, + // private + getResizeEl: function(){ + return this.wrap; + }, - // private - getPositionEl: function(){ - return this.wrap; - }, + // private + getPositionEl: function(){ + return this.wrap; + }, - // private - alignErrorIcon: function(){ - if (this.wrap) { - this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); - } - }, + // private + alignErrorIcon: function(){ + if (this.wrap) { + this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); + } + }, - validateBlur: function(){ - return true; - } + validateBlur: function(){ + return true; + } }); Ext.reg('spinnerfield', Ext.ux.form.SpinnerField); diff --git a/deluge/ui/web/js/ext-extensions/form/SpinnerFieldFix.js b/deluge/ui/web/js/ext-extensions/form/SpinnerFieldFix.js index b12d2039d..f34462975 100644 --- a/deluge/ui/web/js/ext-extensions/form/SpinnerFieldFix.js +++ b/deluge/ui/web/js/ext-extensions/form/SpinnerFieldFix.js @@ -31,5 +31,5 @@ */ Ext.override(Ext.ux.form.SpinnerField, { - onBlur: Ext.form.Field.prototype.onBlur + onBlur: Ext.form.Field.prototype.onBlur }); diff --git a/deluge/ui/web/js/ext-extensions/form/SpinnerGroup.js b/deluge/ui/web/js/ext-extensions/form/SpinnerGroup.js index 207ef695c..7e042fb5a 100644 --- a/deluge/ui/web/js/ext-extensions/form/SpinnerGroup.js +++ b/deluge/ui/web/js/ext-extensions/form/SpinnerGroup.js @@ -99,7 +99,7 @@ Ext.ux.form.SpinnerGroup = Ext.extend(Ext.form.CheckboxGroup, { // Generate the column configs with the correct width setting for(var i=0; i' - ); - ts.rowHolder.disableFormats = true; - ts.rowHolder.compile(); + initTemplates : function(){ + Ext.ux.grid.BufferView.superclass.initTemplates.call(this); + var ts = this.templates; + // empty div to act as a place holder for a row + ts.rowHolder = new Ext.Template( + '
' + ); + ts.rowHolder.disableFormats = true; + ts.rowHolder.compile(); - ts.rowBody = new Ext.Template( - '', - '{cells}', - (this.enableRowBody ? '' : ''), - '
{body}
' - ); - ts.rowBody.disableFormats = true; - ts.rowBody.compile(); - }, + ts.rowBody = new Ext.Template( + '', + '{cells}', + (this.enableRowBody ? '' : ''), + '
{body}
' + ); + ts.rowBody.disableFormats = true; + ts.rowBody.compile(); + }, - getStyleRowHeight : function(){ - return Ext.isBorderBox ? (this.rowHeight + this.borderHeight) : this.rowHeight; - }, + getStyleRowHeight : function(){ + return Ext.isBorderBox ? (this.rowHeight + this.borderHeight) : this.rowHeight; + }, - getCalculatedRowHeight : function(){ - return this.rowHeight + this.borderHeight; - }, + getCalculatedRowHeight : function(){ + return this.rowHeight + this.borderHeight; + }, - getVisibleRowCount : function(){ - var rh = this.getCalculatedRowHeight(); - var visibleHeight = this.scroller.dom.clientHeight; - return (visibleHeight < 1) ? 0 : Math.ceil(visibleHeight / rh); - }, + getVisibleRowCount : function(){ + var rh = this.getCalculatedRowHeight(); + var visibleHeight = this.scroller.dom.clientHeight; + return (visibleHeight < 1) ? 0 : Math.ceil(visibleHeight / rh); + }, - getVisibleRows: function(){ - var count = this.getVisibleRowCount(); - var sc = this.scroller.dom.scrollTop; - var start = (sc == 0 ? 0 : Math.floor(sc/this.getCalculatedRowHeight())-1); - return { - first: Math.max(start, 0), - last: Math.min(start + count + 2, this.ds.getCount()-1) - }; - }, + getVisibleRows: function(){ + var count = this.getVisibleRowCount(); + var sc = this.scroller.dom.scrollTop; + var start = (sc == 0 ? 0 : Math.floor(sc/this.getCalculatedRowHeight())-1); + return { + first: Math.max(start, 0), + last: Math.min(start + count + 2, this.ds.getCount()-1) + }; + }, - doRender : function(cs, rs, ds, startRow, colCount, stripe, onlyBody){ - var ts = this.templates, ct = ts.cell, rt = ts.row, rb = ts.rowBody, last = colCount-1; - var rh = this.getStyleRowHeight(); - var vr = this.getVisibleRows(); - var tstyle = 'width:'+this.getTotalWidth()+';height:'+rh+'px;'; - // buffers - var buf = [], cb, c, p = {}, rp = {tstyle: tstyle}, r; - for (var j = 0, len = rs.length; j < len; j++) { - r = rs[j]; cb = []; - var rowIndex = (j+startRow); - var visible = rowIndex >= vr.first && rowIndex <= vr.last; - if (visible) { - for (var i = 0; i < colCount; i++) { - c = cs[i]; - p.id = c.id; - p.css = i == 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : ''); - p.attr = p.cellAttr = ""; - p.value = c.renderer(r.data[c.name], p, r, rowIndex, i, ds); - p.style = c.style; - if (p.value == undefined || p.value === "") { - p.value = " "; - } - if (r.dirty && typeof r.modified[c.name] !== 'undefined') { - p.css += ' x-grid3-dirty-cell'; - } - cb[cb.length] = ct.apply(p); - } - } - var alt = []; - if(stripe && ((rowIndex+1) % 2 == 0)){ - alt[0] = "x-grid3-row-alt"; - } - if(r.dirty){ - alt[1] = " x-grid3-dirty-row"; - } - rp.cols = colCount; - if(this.getRowClass){ - alt[2] = this.getRowClass(r, rowIndex, rp, ds); - } - rp.alt = alt.join(" "); - rp.cells = cb.join(""); - buf[buf.length] = !visible ? ts.rowHolder.apply(rp) : (onlyBody ? rb.apply(rp) : rt.apply(rp)); - } - return buf.join(""); - }, + doRender : function(cs, rs, ds, startRow, colCount, stripe, onlyBody){ + var ts = this.templates, ct = ts.cell, rt = ts.row, rb = ts.rowBody, last = colCount-1; + var rh = this.getStyleRowHeight(); + var vr = this.getVisibleRows(); + var tstyle = 'width:'+this.getTotalWidth()+';height:'+rh+'px;'; + // buffers + var buf = [], cb, c, p = {}, rp = {tstyle: tstyle}, r; + for (var j = 0, len = rs.length; j < len; j++) { + r = rs[j]; cb = []; + var rowIndex = (j+startRow); + var visible = rowIndex >= vr.first && rowIndex <= vr.last; + if (visible) { + for (var i = 0; i < colCount; i++) { + c = cs[i]; + p.id = c.id; + p.css = i == 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : ''); + p.attr = p.cellAttr = ""; + p.value = c.renderer(r.data[c.name], p, r, rowIndex, i, ds); + p.style = c.style; + if (p.value == undefined || p.value === "") { + p.value = " "; + } + if (r.dirty && typeof r.modified[c.name] !== 'undefined') { + p.css += ' x-grid3-dirty-cell'; + } + cb[cb.length] = ct.apply(p); + } + } + var alt = []; + if(stripe && ((rowIndex+1) % 2 == 0)){ + alt[0] = "x-grid3-row-alt"; + } + if(r.dirty){ + alt[1] = " x-grid3-dirty-row"; + } + rp.cols = colCount; + if(this.getRowClass){ + alt[2] = this.getRowClass(r, rowIndex, rp, ds); + } + rp.alt = alt.join(" "); + rp.cells = cb.join(""); + buf[buf.length] = !visible ? ts.rowHolder.apply(rp) : (onlyBody ? rb.apply(rp) : rt.apply(rp)); + } + return buf.join(""); + }, - isRowRendered: function(index){ - var row = this.getRow(index); - return row && row.childNodes.length > 0; - }, + isRowRendered: function(index){ + var row = this.getRow(index); + return row && row.childNodes.length > 0; + }, - syncScroll: function(){ - Ext.ux.grid.BufferView.superclass.syncScroll.apply(this, arguments); - this.update(); - }, + syncScroll: function(){ + Ext.ux.grid.BufferView.superclass.syncScroll.apply(this, arguments); + this.update(); + }, - // a (optionally) buffered method to update contents of gridview - update: function(){ - if (this.scrollDelay) { - if (!this.renderTask) { - this.renderTask = new Ext.util.DelayedTask(this.doUpdate, this); - } - this.renderTask.delay(this.scrollDelay); - }else{ - this.doUpdate(); - } - }, - - onRemove : function(ds, record, index, isUpdate){ - Ext.ux.grid.BufferView.superclass.onRemove.apply(this, arguments); - if(isUpdate !== true){ - this.update(); - } - }, + // a (optionally) buffered method to update contents of gridview + update: function(){ + if (this.scrollDelay) { + if (!this.renderTask) { + this.renderTask = new Ext.util.DelayedTask(this.doUpdate, this); + } + this.renderTask.delay(this.scrollDelay); + }else{ + this.doUpdate(); + } + }, + + onRemove : function(ds, record, index, isUpdate){ + Ext.ux.grid.BufferView.superclass.onRemove.apply(this, arguments); + if(isUpdate !== true){ + this.update(); + } + }, - doUpdate: function(){ - if (this.getVisibleRowCount() > 0) { - var g = this.grid, cm = g.colModel, ds = g.store; - var cs = this.getColumnData(); + doUpdate: function(){ + if (this.getVisibleRowCount() > 0) { + var g = this.grid, cm = g.colModel, ds = g.store; + var cs = this.getColumnData(); - var vr = this.getVisibleRows(); - for (var i = vr.first; i <= vr.last; i++) { - // if row is NOT rendered and is visible, render it - if(!this.isRowRendered(i)){ - var html = this.doRender(cs, [ds.getAt(i)], ds, i, cm.getColumnCount(), g.stripeRows, true); - this.getRow(i).innerHTML = html; - } - } - this.clean(); - } - }, + var vr = this.getVisibleRows(); + for (var i = vr.first; i <= vr.last; i++) { + // if row is NOT rendered and is visible, render it + if(!this.isRowRendered(i)){ + var html = this.doRender(cs, [ds.getAt(i)], ds, i, cm.getColumnCount(), g.stripeRows, true); + this.getRow(i).innerHTML = html; + } + } + this.clean(); + } + }, - // a buffered method to clean rows - clean : function(){ - if(!this.cleanTask){ - this.cleanTask = new Ext.util.DelayedTask(this.doClean, this); - } - this.cleanTask.delay(this.cleanDelay); - }, + // a buffered method to clean rows + clean : function(){ + if(!this.cleanTask){ + this.cleanTask = new Ext.util.DelayedTask(this.doClean, this); + } + this.cleanTask.delay(this.cleanDelay); + }, - doClean: function(){ - if (this.getVisibleRowCount() > 0) { - var vr = this.getVisibleRows(); - vr.first -= this.cacheSize; - vr.last += this.cacheSize; + doClean: function(){ + if (this.getVisibleRowCount() > 0) { + var vr = this.getVisibleRows(); + vr.first -= this.cacheSize; + vr.last += this.cacheSize; - var i = 0, rows = this.getRows(); - // if first is less than 0, all rows have been rendered - // so lets clean the end... - if(vr.first <= 0){ - i = vr.last + 1; - } - for(var len = this.ds.getCount(); i < len; i++){ - // if current row is outside of first and last and - // has content, update the innerHTML to nothing - if ((i < vr.first || i > vr.last) && rows[i].innerHTML) { - rows[i].innerHTML = ''; - } - } - } - }, + var i = 0, rows = this.getRows(); + // if first is less than 0, all rows have been rendered + // so lets clean the end... + if(vr.first <= 0){ + i = vr.last + 1; + } + for(var len = this.ds.getCount(); i < len; i++){ + // if current row is outside of first and last and + // has content, update the innerHTML to nothing + if ((i < vr.first || i > vr.last) && rows[i].innerHTML) { + rows[i].innerHTML = ''; + } + } + } + }, - layout: function(){ - Ext.ux.grid.BufferView.superclass.layout.call(this); - this.update(); - } + layout: function(){ + Ext.ux.grid.BufferView.superclass.layout.call(this); + this.update(); + } }); diff --git a/deluge/ui/web/js/ext-extensions/layout/FormLayoutFix.js b/deluge/ui/web/js/ext-extensions/layout/FormLayoutFix.js index dfa27e141..5f4121574 100644 --- a/deluge/ui/web/js/ext-extensions/layout/FormLayoutFix.js +++ b/deluge/ui/web/js/ext-extensions/layout/FormLayoutFix.js @@ -34,23 +34,23 @@ // remove spaces for hidden elements and make show(), hide(), enable() and disable() act on // the label. don't use hideLabel with this. Ext.override(Ext.layout.FormLayout, { - renderItem : function(c, position, target){ - if(c && !c.rendered && (c.isFormField || c.fieldLabel) && c.inputType != 'hidden'){ - var args = this.getTemplateArgs(c); - if(typeof position == 'number'){ - position = target.dom.childNodes[position] || null; - } - if(position){ - c.formItem = this.fieldTpl.insertBefore(position, args, true); - }else{ - c.formItem = this.fieldTpl.append(target, args, true); - } - c.actionMode = 'formItem'; - c.render('x-form-el-'+c.id); - c.container = c.formItem; - c.actionMode = 'container'; - }else { - Ext.layout.FormLayout.superclass.renderItem.apply(this, arguments); - } - } + renderItem : function(c, position, target){ + if(c && !c.rendered && (c.isFormField || c.fieldLabel) && c.inputType != 'hidden'){ + var args = this.getTemplateArgs(c); + if(typeof position == 'number'){ + position = target.dom.childNodes[position] || null; + } + if(position){ + c.formItem = this.fieldTpl.insertBefore(position, args, true); + }else{ + c.formItem = this.fieldTpl.append(target, args, true); + } + c.actionMode = 'formItem'; + c.render('x-form-el-'+c.id); + c.container = c.formItem; + c.actionMode = 'container'; + }else { + Ext.layout.FormLayout.superclass.renderItem.apply(this, arguments); + } + } }); diff --git a/deluge/ui/web/js/ext-extensions/tree/MultiSelectionModelFix.js b/deluge/ui/web/js/ext-extensions/tree/MultiSelectionModelFix.js index 943cd9cec..da2513c41 100644 --- a/deluge/ui/web/js/ext-extensions/tree/MultiSelectionModelFix.js +++ b/deluge/ui/web/js/ext-extensions/tree/MultiSelectionModelFix.js @@ -35,58 +35,58 @@ * @author Damien Churchill */ Ext.override(Ext.tree.MultiSelectionModel, { - - onNodeClick: function (node, e) { - if (e.ctrlKey && this.isSelected(node)) { - this.unselect(node); - } else if (e.shiftKey && !this.isSelected(node)) { - var parentNode = node.parentNode; - // We can only shift select files in the same node - if (this.lastSelNode.parentNode.id != parentNode.id) return; + + onNodeClick: function (node, e) { + if (e.ctrlKey && this.isSelected(node)) { + this.unselect(node); + } else if (e.shiftKey && !this.isSelected(node)) { + var parentNode = node.parentNode; + // We can only shift select files in the same node + if (this.lastSelNode.parentNode.id != parentNode.id) return; - // Get the node indexes - var fi = parentNode.indexOf(node), - li = parentNode.indexOf(this.lastSelNode); + // Get the node indexes + var fi = parentNode.indexOf(node), + li = parentNode.indexOf(this.lastSelNode); - // Select the last clicked node and wipe old selections - this.select(this.lastSelNode, e, false, true); + // Select the last clicked node and wipe old selections + this.select(this.lastSelNode, e, false, true); - // Swap the values if required - if (fi > li) { - fi = fi + li, li = fi - li, fi = fi - li; - } + // Swap the values if required + if (fi > li) { + fi = fi + li, li = fi - li, fi = fi - li; + } - // Select all the nodes - parentNode.eachChild(function(n) { - var i = parentNode.indexOf(n); - if (fi < i && i < li) { - this.select(n, e, true, true); - } - }, this); + // Select all the nodes + parentNode.eachChild(function(n) { + var i = parentNode.indexOf(n); + if (fi < i && i < li) { + this.select(n, e, true, true); + } + }, this); - // Select the clicked node - this.select(node, e, true); - } else { - this.select(node, e, e.ctrlKey); - } - }, + // Select the clicked node + this.select(node, e, true); + } else { + this.select(node, e, e.ctrlKey); + } + }, - select: function(node, e, keepExisting, suppressEvent) { - if(keepExisting !== true){ - this.clearSelections(true); - } - if(this.isSelected(node)){ - this.lastSelNode = node; - return node; - } - this.selNodes.push(node); - this.selMap[node.id] = node; - this.lastSelNode = node; - node.ui.onSelectedChange(true); - if (suppressEvent !== true) { - this.fireEvent('selectionchange', this, this.selNodes); - } + select: function(node, e, keepExisting, suppressEvent) { + if(keepExisting !== true){ + this.clearSelections(true); + } + if(this.isSelected(node)){ + this.lastSelNode = node; + return node; + } + this.selNodes.push(node); + this.selMap[node.id] = node; + this.lastSelNode = node; + node.ui.onSelectedChange(true); + if (suppressEvent !== true) { + this.fireEvent('selectionchange', this, this.selNodes); + } return node; - } + } }) diff --git a/deluge/ui/web/js/ext-extensions/tree/TreeGridColumnResizer.js b/deluge/ui/web/js/ext-extensions/tree/TreeGridColumnResizer.js index d5101c666..06c183231 100644 --- a/deluge/ui/web/js/ext-extensions/tree/TreeGridColumnResizer.js +++ b/deluge/ui/web/js/ext-extensions/tree/TreeGridColumnResizer.js @@ -56,7 +56,7 @@ Ext.tree.ColumnResizer = Ext.extend(Ext.util.Observable, { } if(ps) { this.activeHd = Ext.get(ps); - ss.cursor = Ext.isWebKit ? 'e-resize' : 'col-resize'; + ss.cursor = Ext.isWebKit ? 'e-resize' : 'col-resize'; } } else if(r.right - x <= hw) { var ns = hd.dom; @@ -65,7 +65,7 @@ Ext.tree.ColumnResizer = Ext.extend(Ext.util.Observable, { } if(ns) { this.activeHd = Ext.get(ns); - ss.cursor = Ext.isWebKit ? 'w-resize' : 'col-resize'; + ss.cursor = Ext.isWebKit ? 'w-resize' : 'col-resize'; } } else{ delete this.activeHd; diff --git a/deluge/ui/web/js/ext-extensions/tree/TreeGridNodeUIFix.js b/deluge/ui/web/js/ext-extensions/tree/TreeGridNodeUIFix.js index 55a6cd4d8..3d81c54f4 100644 --- a/deluge/ui/web/js/ext-extensions/tree/TreeGridNodeUIFix.js +++ b/deluge/ui/web/js/ext-extensions/tree/TreeGridNodeUIFix.js @@ -32,22 +32,22 @@ Ext.override(Ext.ux.tree.TreeGridNodeUI, { - updateColumns: function() { - if (!this.rendered) return; - - var a = this.node.attributes, - t = this.node.getOwnerTree(), - cols = t.columns, - c = cols[0]; + updateColumns: function() { + if (!this.rendered) return; + + var a = this.node.attributes, + t = this.node.getOwnerTree(), + cols = t.columns, + c = cols[0]; - // Update the first column - this.anchor.firstChild.innerHTML = (c.tpl ? c.tpl.apply(a) : a[c.dataIndex] || c.text); + // Update the first column + this.anchor.firstChild.innerHTML = (c.tpl ? c.tpl.apply(a) : a[c.dataIndex] || c.text); - // Update the remaining columns - for(i = 1, len = cols.length; i < len; i++) { - c = cols[i]; - this.elNode.childNodes[i].firstChild.innerHTML = (c.tpl ? c.tpl.apply(a) : a[c.dataIndex] || c.text); - } - } + // Update the remaining columns + for(i = 1, len = cols.length; i < len; i++) { + c = cols[i]; + this.elNode.childNodes[i].firstChild.innerHTML = (c.tpl ? c.tpl.apply(a) : a[c.dataIndex] || c.text); + } + } }); diff --git a/deluge/ui/web/js/ext-extensions/tree/TreeGridRenderColumn.js b/deluge/ui/web/js/ext-extensions/tree/TreeGridRenderColumn.js index d104f26c5..b50737f9f 100644 --- a/deluge/ui/web/js/ext-extensions/tree/TreeGridRenderColumn.js +++ b/deluge/ui/web/js/ext-extensions/tree/TreeGridRenderColumn.js @@ -1,10 +1,10 @@ Ext.tree.RenderColumn = Ext.extend(Ext.tree.Column, { - - constructor: function(c) { - c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':this.format}'); - c.tpl.format = c.renderer; - c.tpl.col = this; - Ext.tree.RenderColumn.superclass.constructor.call(this, c); - } + + constructor: function(c) { + c.tpl = c.tpl || new Ext.XTemplate('{' + c.dataIndex + ':this.format}'); + c.tpl.format = c.renderer; + c.tpl.col = this; + Ext.tree.RenderColumn.superclass.constructor.call(this, c); + } }); Ext.reg('tgrendercolumn', Ext.tree.RenderColumn); diff --git a/deluge/ui/web/js/ext-extensions/tree/TreeGridSorter.js b/deluge/ui/web/js/ext-extensions/tree/TreeGridSorter.js index 6d2f62fe4..bbaa31898 100644 --- a/deluge/ui/web/js/ext-extensions/tree/TreeGridSorter.js +++ b/deluge/ui/web/js/ext-extensions/tree/TreeGridSorter.js @@ -56,14 +56,14 @@ Ext.ux.tree.TreeGridSorter = Ext.extend(Ext.tree.TreeSorter, { return -1; } } - var v1 = sortType ? sortType(n1.attributes[p]) : (cs ? n1.attributes[p] : n1.attributes[p].toUpperCase()); - var v2 = sortType ? sortType(n2.attributes[p]) : (cs ? n2.attributes[p] : n2.attributes[p].toUpperCase()); - if(v1 < v2){ - return dsc ? +1 : -1; - }else if(v1 > v2){ - return dsc ? -1 : +1; + var v1 = sortType ? sortType(n1.attributes[p]) : (cs ? n1.attributes[p] : n1.attributes[p].toUpperCase()); + var v2 = sortType ? sortType(n2.attributes[p]) : (cs ? n2.attributes[p] : n2.attributes[p].toUpperCase()); + if(v1 < v2){ + return dsc ? +1 : -1; + }else if(v1 > v2){ + return dsc ? -1 : +1; }else{ - return 0; + return 0; } }; diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py index c190f2804..2cf90b70d 100644 --- a/deluge/ui/web/json_api.py +++ b/deluge/ui/web/json_api.py @@ -40,11 +40,14 @@ import shutil import logging import hashlib import tempfile +from urlparse import urljoin from types import FunctionType from twisted.internet import reactor from twisted.internet.defer import Deferred, DeferredList from twisted.web import http, resource, server +import twisted.web.client +import twisted.web.error from deluge import common, component, httpdownloader from deluge.configmanager import ConfigManager, get_config_dir @@ -639,13 +642,33 @@ class WebApi(JSONComponent): :rtype: string """ + def on_download_success(result): + log.debug("Successfully downloaded %s to %s", url, result) + return result + + def on_download_fail(result): + if result.check(twisted.web.error.PageRedirect): + new_url = urljoin(url, result.getErrorMessage().split(" to ")[1]) + result = httpdownloader.download_file(new_url, tmp_file, headers=headers) + result.addCallbacks(on_download_success, on_download_fail) + elif result.check(twisted.web.client.PartialDownloadError): + result = httpdownloader.download_file(url, tmp_file, headers=headers, + allow_compression=False) + result.addCallbacks(on_download_success, on_download_fail) + else: + log.error("Error occured downloading torrent from %s", url) + log.error("Reason: %s", result.getErrorMessage()) + return result + tmp_file = os.path.join(tempfile.gettempdir(), url.split("/")[-1]) log.debug("filename: %s", tmp_file) headers = {} if cookie: headers["Cookie"] = cookie log.debug("cookie: %s", cookie) - return httpdownloader.download_file(url, tmp_file, headers=headers) + d = httpdownloader.download_file(url, tmp_file, headers=headers) + d.addCallbacks(on_download_success, on_download_fail) + return d @export def get_torrent_info(self, filename): @@ -717,46 +740,46 @@ class WebApi(JSONComponent): :param host_id: the hash id of the host :type host_id: string """ - main_deferred = Deferred() - (host_id, host, port, user, password) = self.get_host(host_id) + def response(status, info=None): + return host_id, host, port, status, info - def callback(status, info=None): - main_deferred.callback((host_id, host, port, status, info)) + try: + host_id, host, port, user, password = self.get_host(host_id) + except TypeError, e: + return response(_("Offline")) def on_connect(connected, c, host_id): def on_info(info, c): c.disconnect() - callback(_("Online"), info) + return response(_("Online"), info) def on_info_fail(reason, c): c.disconnect() - callback(_("Offline")) + return response(_("Offline")) if not connected: - callback(_("Offline")) - return + return response(_("Offline")) - d = c.daemon.info() - d.addCallback(on_info, c) - d.addErrback(on_info_fail, c) + return c.daemon.info( + ).addCallback(on_info, c + ).addErrback(on_info_fail, c) def on_connect_failed(reason, host_id): - callback(_("Offline")) + return response(_("Offline")) - if client.connected() and (host, port, "localclient" if not \ - user and host in ("127.0.0.1", "localhost") else \ + if client.connected() and (host, port, "localclient" if not + user and host in ("127.0.0.1", "localhost") else user) == client.connection_info(): def on_info(info): - callback(_("Connected"), info) + return response(_("Connected"), info) - client.daemon.info().addCallback(on_info) + return client.daemon.info().addCallback(on_info) else: c = Client() - d = c.connect(host, port, user, password) - d.addCallback(on_connect, c, host_id) - d.addErrback(on_connect_failed, host_id) - return main_deferred + return c.connect(host, port, user, password + ).addCallback(on_connect, c, host_id + ).addErrback(on_connect_failed, host_id) @export def start_daemon(self, port): diff --git a/deluge/ui/web/pluginmanager.py b/deluge/ui/web/pluginmanager.py index ec4c61c58..c2ed75efd 100644 --- a/deluge/ui/web/pluginmanager.py +++ b/deluge/ui/web/pluginmanager.py @@ -72,11 +72,11 @@ class PluginManager(PluginManagerBase, component.Component): for plugin in plugins: self.enable_plugin(plugin) - def _on_plugin_enabled_event(self, event): - self.enable_plugin(event.plugin_name) + def _on_plugin_enabled_event(self, name): + self.enable_plugin(name) - def _on_plugin_disabled_event(self, event): - self.disable_plugin(event.plugin_name) + def _on_plugin_disabled_event(self, name): + self.disable_plugin(name) def disable_plugin(self, name): # Get the plugin instance @@ -146,7 +146,7 @@ class PluginManager(PluginManagerBase, component.Component): return info = gather_info(plugin) info["name"] = name - info["scripts"] = ["/js/%s/%s" % (name.lower(), os.path.basename(s)) for s in info["scripts"]] - info["debug_scripts"] = ["/js/%s/%s" % (name.lower(), os.path.basename(s)) for s in info["debug_scripts"]] + info["scripts"] = ["js/%s/%s" % (name.lower(), os.path.basename(s)) for s in info["scripts"]] + info["debug_scripts"] = ["js/%s/%s" % (name.lower(), os.path.basename(s)) for s in info["debug_scripts"]] del info["script_directories"] return info diff --git a/deluge/ui/web/server.py b/deluge/ui/web/server.py index bb7b12da8..ee03e85e0 100644 --- a/deluge/ui/web/server.py +++ b/deluge/ui/web/server.py @@ -518,13 +518,31 @@ class TopLevel(resource.Resource): self.__scripts.remove(script) self.__debug_scripts.remove(script) - def getChild(self, path, request): if path == "": return self else: return resource.Resource.getChild(self, path, request) + def getChildWithDefault(self, path, request): + # Calculate the request base + header = request.getHeader('x-deluge-base') + base = header if header else component.get("DelugeWeb").base + + # validate the base parameter + if not base: + base = '/' + + if base[0] != '/': + base = '/' + base + + if base[-1] != '/': + base += '/' + + request.base = base.encode('idna') + + return resource.Resource.getChildWithDefault(self, path, request) + def render(self, request): debug = False if 'debug' in request.args: @@ -555,25 +573,12 @@ class TopLevel(resource.Resource): template = Template(filename=rpath("index.html")) request.setHeader("content-type", "text/html; charset=utf-8") - header = request.getHeader('x-deluge-base') - base = header if header else component.get("DelugeWeb").base - - # validate the base parameter - if not base: - base = '/' - - if base[0] != '/': - base = '/' + base - - if base[-1] != '/': - base += '/' - web_config = component.get("Web").get_config() - web_config["base"] = base + web_config["base"] = request.base config = dict([(key, web_config[key]) for key in UI_CONFIG_KEYS]) js_config = common.json.dumps(config) return template.render(scripts=scripts, stylesheets=self.stylesheets, - debug=debug, base=base, js_config=js_config) + debug=debug, base=request.base, js_config=js_config) class ServerContextFactory: diff --git a/deluge/ui/web/web.py b/deluge/ui/web/web.py index a2b57655a..254468235 100644 --- a/deluge/ui/web/web.py +++ b/deluge/ui/web/web.py @@ -35,6 +35,7 @@ import os +import deluge.common from deluge.ui.ui import _UI, UI from optparse import OptionGroup @@ -56,9 +57,20 @@ class Web(_UI): group.add_option("-b", "--base", dest="base", help="Set the base path that the ui is running on (proxying)", action="store", default=None) - group.add_option("-f", "--fork", dest="fork", - help="Fork the web interface process into the background", - action="store_true", default=False) + if not (deluge.common.windows_check() or deluge.common.osx_check()): + group.add_option("-f", "--fork", dest="fork", + help="Fork the web interface process into the background", + action="store_true", default=False) + group.add_option("-P", "--pidfile", dest="pidfile", type="str", + help="Use pidfile to store process id", + action="store", default=None) + if not deluge.common.windows_check(): + group.add_option("-U", "--user", dest="user", type="str", + help="User to switch to. Only use it when starting as root", + action="store", default=None) + group.add_option("-g", "--group", dest="group", type="str", + help="Group to switch to. Only use it when starting as root", + action="store", default=None) group.add_option("-p", "--port", dest="port", type="int", help="Sets the port to be used for the webserver", action="store", default=None) @@ -83,10 +95,9 @@ class Web(_UI): def start(self): super(Web, self).start() - import deluge.common # Steps taken from http://www.faqs.org/faqs/unix-faq/programmer/faq/ # Section 1.7 - if self.options.fork and not deluge.common.windows_check(): + if self.options.fork: # fork() so the parent can exit, returns control to the command line # or shell invoking the program. if os.fork(): @@ -103,7 +114,21 @@ class Web(_UI): # use that may prevent a filesystem unmount. import deluge.configmanager os.chdir(deluge.configmanager.get_config_dir()) - + + if self.options.pidfile: + open(self.options.pidfile, "wb").write("%d\n" % os.getpid()) + + if self.options.group: + if not self.options.group.isdigit(): + import grp + self.options.group = grp.getgrnam(self.options.group)[2] + os.setuid(self.options.group) + if self.options.user: + if not self.options.user.isdigit(): + import pwd + self.options.user = pwd.getpwnam(self.options.user)[2] + os.setuid(self.options.user) + import server self.__server = server.DelugeWeb() diff --git a/ez_setup.py b/ez_setup.py index d24e845e5..d38608de9 100644 --- a/ez_setup.py +++ b/ez_setup.py @@ -14,7 +14,7 @@ the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys -DEFAULT_VERSION = "0.6c9" +DEFAULT_VERSION = "0.6c11" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { @@ -52,6 +52,11 @@ md5_data = { 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', + 'setuptools-0.6c11-py2.3.egg' : '2baeac6e13d414a9d28e7ba5b5a596de', + 'setuptools-0.6c11-py2.4.egg' : 'bd639f9b0eac4c42497034dec2ec0c2b', + 'setuptools-0.6c11-py2.5.egg' : '64c94f3bf7a72a13ec83e0b24f2749b2', + 'setuptools-0.6c11-py2.6.egg' : 'bfa92100bd772d5a213eedd356d64086', + 'setuptools-0.6c11-py2.7.egg' : 'fe1f997bc722265116870bc7919059ea', } import sys, os diff --git a/gettextize.sh b/gettextize.sh old mode 100644 new mode 100755 diff --git a/msgfmt.py b/msgfmt.py old mode 100644 new mode 100755 index d9133a2cc..4bda37ebb --- a/msgfmt.py +++ b/msgfmt.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # -*- coding: iso-8859-1 -*- # Written by Martin v. Lwis # Plural forms support added by alexander smishlajev diff --git a/setup.py b/setup.py index c9b5234ee..94708a45e 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # # setup.py # @@ -67,6 +68,7 @@ _extra_compile_args = [ "-D_FILE_OFFSET_BITS=64", "-DNDEBUG", "-DTORRENT_USE_OPENSSL=1", + "-DBOOST_FILESYSTEM_VERSION=2", "-O2", ] @@ -399,21 +401,21 @@ cmdclass = { # Data files to be installed to the system _data_files = [ - ('share/icons/scalable/apps', ['deluge/data/icons/scalable/apps/deluge.svg']), - ('share/icons/hicolor/128x128/apps', ['deluge/data/icons/hicolor/128x128/apps/deluge.png']), - ('share/icons/hicolor/16x16/apps', ['deluge/data/icons/hicolor/16x16/apps/deluge.png']), - ('share/icons/hicolor/192x192/apps', ['deluge/data/icons/hicolor/192x192/apps/deluge.png']), - ('share/icons/hicolor/22x22/apps', ['deluge/data/icons/hicolor/22x22/apps/deluge.png']), - ('share/icons/hicolor/24x24/apps', ['deluge/data/icons/hicolor/24x24/apps/deluge.png']), - ('share/icons/hicolor/256x256/apps', ['deluge/data/icons/hicolor/256x256/apps/deluge.png']), - ('share/icons/hicolor/32x32/apps', ['deluge/data/icons/hicolor/32x32/apps/deluge.png']), - ('share/icons/hicolor/36x36/apps', ['deluge/data/icons/hicolor/36x36/apps/deluge.png']), - ('share/icons/hicolor/48x48/apps', ['deluge/data/icons/hicolor/48x48/apps/deluge.png']), - ('share/icons/hicolor/64x64/apps', ['deluge/data/icons/hicolor/64x64/apps/deluge.png']), - ('share/icons/hicolor/72x72/apps', ['deluge/data/icons/hicolor/72x72/apps/deluge.png']), - ('share/icons/hicolor/96x96/apps', ['deluge/data/icons/hicolor/96x96/apps/deluge.png']), - ('share/applications', ['deluge/data/share/applications/deluge.desktop']), - ('share/pixmaps', ['deluge/data/pixmaps/deluge.png', 'deluge/data/pixmaps/deluge.xpm']), + ('share/icons/scalable/apps', ['deluge/ui/data/icons/scalable/apps/deluge.svg']), + ('share/icons/hicolor/128x128/apps', ['deluge/ui/data/icons/hicolor/128x128/apps/deluge.png']), + ('share/icons/hicolor/16x16/apps', ['deluge/ui/data/icons/hicolor/16x16/apps/deluge.png']), + ('share/icons/hicolor/192x192/apps', ['deluge/ui/data/icons/hicolor/192x192/apps/deluge.png']), + ('share/icons/hicolor/22x22/apps', ['deluge/ui/data/icons/hicolor/22x22/apps/deluge.png']), + ('share/icons/hicolor/24x24/apps', ['deluge/ui/data/icons/hicolor/24x24/apps/deluge.png']), + ('share/icons/hicolor/256x256/apps', ['deluge/ui/data/icons/hicolor/256x256/apps/deluge.png']), + ('share/icons/hicolor/32x32/apps', ['deluge/ui/data/icons/hicolor/32x32/apps/deluge.png']), + ('share/icons/hicolor/36x36/apps', ['deluge/ui/data/icons/hicolor/36x36/apps/deluge.png']), + ('share/icons/hicolor/48x48/apps', ['deluge/ui/data/icons/hicolor/48x48/apps/deluge.png']), + ('share/icons/hicolor/64x64/apps', ['deluge/ui/data/icons/hicolor/64x64/apps/deluge.png']), + ('share/icons/hicolor/72x72/apps', ['deluge/ui/data/icons/hicolor/72x72/apps/deluge.png']), + ('share/icons/hicolor/96x96/apps', ['deluge/ui/data/icons/hicolor/96x96/apps/deluge.png']), + ('share/applications', ['deluge/ui/data/share/applications/deluge.desktop']), + ('share/pixmaps', ['deluge/ui/data/pixmaps/deluge.png', 'deluge/ui/data/pixmaps/deluge.xpm']), ('share/man/man1', [ 'docs/man/deluge.1', 'docs/man/deluged.1', @@ -459,10 +461,11 @@ setup( ext_package = "deluge", ext_modules = _ext_modules, package_data = {"deluge": ["ui/gtkui/glade/*.glade", - "data/pixmaps/*.png", - "data/pixmaps/*.svg", - "data/pixmaps/*.ico", - "data/pixmaps/flags/*.png", + "ui/data/pixmaps/*.png", + "ui/data/pixmaps/*.svg", + "ui/data/pixmaps/*.ico", + "ui/data/pixmaps/*.gif", + "ui/data/pixmaps/flags/*.png", "plugins/*.egg", "i18n/*.pot", "i18n/*/LC_MESSAGES/*.mo",