Merge branch 'master' into master-decode-string

This commit is contained in:
Chase Sterling 2012-12-10 20:34:46 -05:00
commit 73102f1362
338 changed files with 1035 additions and 734 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@ dist
_trial_temp _trial_temp
deluge/i18n/*/ deluge/i18n/*/
*.desktop *.desktop
.build_data

View file

@ -32,6 +32,7 @@
* Implemented sequential downloads UI handling. * Implemented sequential downloads UI handling.
* #378: Allow showing a pieces bar instead of a regular progress bar in a * #378: Allow showing a pieces bar instead of a regular progress bar in a
torrent's status tab. torrent's status tab.
* #2093: Make torrent opening compatible with all unicode paths.
==== Blocklist Plugin ==== ==== Blocklist Plugin ====
* #1382: Implemented whitelist support to both core and GTK UI. * #1382: Implemented whitelist support to both core and GTK UI.
@ -44,6 +45,58 @@
=== Deluge 1.3.6 (In Development) === === Deluge 1.3.6 (In Development) ===
==== Core ==== ==== Core ====
* Catch & log KeyError when removing a torrent from the queued torrents set * Catch & log KeyError when removing a torrent from the queued torrents set
* Fix moving/renaming torrents with non-ascii characters in libtorrent 0.16
* Make sure queue order is preserved when restarting
* #2160: Disable use of python bindings for libtorrent extensions and replace with session flag
* #2163: Fix unable add torrent file with empty (0:) encoding tag
* #2201: Fix error in authmanager if auth file has extra newlines
* #2109: Fix the Proxy settings not being cleared by setting None
* #2110: Fix accepting magnet uris with xt param anywhere within them
* #2204: Fix daemon shutdown hang
==== GtkUI ====
* Add move completed option to add torrent dialog
* Prevent jitter in torrent view
* Fix torrent creation with non-ascii characters
* Fix #2100 : Add option not to bring main window to front when adding torrents through ipcinterface
* Add Quit Dialog when toggling classic mode in preferences and only show connection manager when not in classic mode.
* #2169: Fix 'Download Location' in the Add Torrent Dialog not set correctly when folder typed into Other->Location field
* #2171: Fix the Add Peer dialog not responding if empty or invalid values entered
* #2104: Fix no title set for the appindicator
* #2086: Fix submenus and icons for appindicator
* #2146: Fix missing translations in View|Tabs submenu
* Fix torrent names on libtorrent 0.16 on windows
* #2147: Fix missing translations for plugin preferences page
* #1474: Fix the on_show_prefs hook not being executed immediatly after enabling a plugin
==== Console ====
* LP#1004793: Enable use of connect command in non-interactive mode
* Ensure console commands are executed in order
* #2065: Fix crash with missing closing quote
* #1397: Add support for -s STATE in info command
==== WebUI ====
* #2112: Fix world readable tmp directory in json_api
* #2069: Fix login window layout problem when using larger than default font size
* #1890: Fix columns in files and peers view could use some spacing
* #2103: Fix sorting by name is case-sensitive [sedulous]
* #2120: Fix manually entered values not being saved in spinners
* #2212: Fix unable to scroll in proxy preferences page
* Fix autoconnecting to the default host
==== Windows OS ====
* Hide the cmd windows when running deluged.exe or deluge-web.exe
* Add deluged-debug.exe and deluge-web-debug.exe that still show the cmd window
* Add gtk locale files to fix untranslated text
* Fix the Open Folder option not working with non-ascii paths
* Fix the daemon starting with config dir containing spaces
* Fix Windows tray submenu items requiring right-click instead of left-click
==== OS X ====
* Fix Open File/Folder option
==== Execute ====
* Fix execute plugin not working with unicode torrent names
=== Deluge 1.3.5 (09 April 2012) === === Deluge 1.3.5 (09 April 2012) ===
==== GtkUI ==== ==== GtkUI ====

View file

@ -11,6 +11,7 @@
* chardet * chardet
* geoip-database (optional) * geoip-database (optional)
* setproctitle (optional) * setproctitle (optional)
* rencode >= 1.0.2 (optional), a pure python version is included
* libtorrent >= 0.16.1, or build the included version * libtorrent >= 0.16.1, or build the included version

View file

@ -45,9 +45,9 @@ supports.
""" """
REQUIRED_VERSION = "0.14.9.0" REQUIRED_VERSION = "0.16.1.0"
def check_version(LT): def check_version(lt):
from deluge.common import VersionSplit from deluge.common import VersionSplit
if VersionSplit(lt.version) < VersionSplit(REQUIRED_VERSION): if VersionSplit(lt.version) < VersionSplit(REQUIRED_VERSION):
raise ImportError("This version of Deluge requires libtorrent >=%s!" % REQUIRED_VERSION) raise ImportError("This version of Deluge requires libtorrent >=%s!" % REQUIRED_VERSION)

View file

@ -68,6 +68,7 @@ if not hasattr(json, "dumps"):
import pkg_resources import pkg_resources
import gettext import gettext
import locale import locale
import sys
from deluge.error import * from deluge.error import *
@ -231,7 +232,7 @@ def open_file(path):
""" """
if windows_check(): if windows_check():
os.startfile("%s" % path) os.startfile(path.decode("utf8"))
elif osx_check(): elif osx_check():
subprocess.Popen(["open", "%s" % path]) subprocess.Popen(["open", "%s" % path])
else: else:
@ -448,7 +449,9 @@ def is_magnet(uri):
True True
""" """
if uri[:20] == "magnet:?xt=urn:btih:": magnet_scheme = 'magnet:?'
xt_param = 'xt=urn:btih:'
if uri.startswith(magnet_scheme) and xt_param in uri:
return True return True
return False return False
@ -542,15 +545,23 @@ def is_ip(ip):
import socket import socket
#first we test ipv4 #first we test ipv4
try: try:
if socket.inet_pton(socket.AF_INET, "%s" % (ip)): if windows_check():
return True if socket.inet_aton("%s" % (ip)):
return True
else:
if socket.inet_pton(socket.AF_INET, "%s" % (ip)):
return True
except socket.error: except socket.error:
if not socket.has_ipv6: if not socket.has_ipv6:
return False return False
#now test ipv6 #now test ipv6
try: try:
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)): if windows_check():
log.warning("ipv6 check unavailable on windows")
return True return True
else:
if socket.inet_pton(socket.AF_INET6, "%s" % (ip)):
return True
except socket.error: except socket.error:
return False return False
@ -757,3 +768,39 @@ def setup_translations(setup_pygtk=False):
log.exception(e) log.exception(e)
import __builtin__ import __builtin__
__builtin__.__dict__["_"] = lambda x: x __builtin__.__dict__["_"] = lambda x: x
def unicode_argv():
""" Gets sys.argv as list of unicode objects on any platform."""
if windows_check():
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
else:
# On other platforms, we have to find the likely encoding of the args and decode
# First check if sys.stdout or stdin have encoding set
encoding = getattr(sys.stdout, "encoding") or getattr(sys.stdin, "encoding")
# If that fails, check what the locale is set to
encoding = encoding or locale.getpreferredencoding()
# As a last resort, just default to utf-8
encoding = encoding or "utf-8"
return [arg.decode(encoding) for arg in sys.argv]

View file

@ -34,6 +34,7 @@
# #
import logging import logging
from collections import defaultdict
from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail from twisted.internet.defer import maybeDeferred, succeed, DeferredList, fail
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
@ -225,6 +226,8 @@ class ComponentRegistry(object):
""" """
def __init__(self): def __init__(self):
self.components = {} self.components = {}
# Stores all of the components that are dependent on a particular component
self.dependents = defaultdict(list)
def register(self, obj): def register(self, obj):
""" """
@ -243,6 +246,9 @@ class ComponentRegistry(object):
"Component already registered with name %s" % name) "Component already registered with name %s" % name)
self.components[obj._component_name] = obj self.components[obj._component_name] = obj
if obj._component_depend:
for depend in obj._component_depend:
self.dependents[depend].append(name)
def deregister(self, obj): def deregister(self, obj):
""" """
@ -317,11 +323,23 @@ class ComponentRegistry(object):
elif isinstance(names, str): elif isinstance(names, str):
names = [names] names = [names]
def on_dependents_stopped(result, name):
return self.components[name]._component_stop()
stopped_in_deferred = set()
deferreds = [] deferreds = []
for name in names: for name in names:
if name in stopped_in_deferred:
continue
if name in self.components: if name in self.components:
deferreds.append(self.components[name]._component_stop()) if name in self.dependents:
# If other components depend on this component, stop them first
d = self.stop(self.dependents[name]).addCallback(on_dependents_stopped, name)
deferreds.append(d)
stopped_in_deferred.update(self.dependents[name])
else:
deferreds.append(self.components[name]._component_stop())
return DeferredList(deferreds) return DeferredList(deferreds)
@ -360,7 +378,7 @@ class ComponentRegistry(object):
:param names: a list of Components to resume :param names: a list of Components to resume
:type names: list :type names: list
:returns: a Deferred object that will fire once all Components have been sucessfully resumed :returns: a Deferred object that will fire once all Components have been successfully resumed
:rtype: twisted.internet.defer.Deferred :rtype: twisted.internet.defer.Deferred
""" """
@ -384,16 +402,14 @@ class ComponentRegistry(object):
be called when the program is exiting to ensure all Components have a be called when the program is exiting to ensure all Components have a
chance to properly shutdown. chance to properly shutdown.
:returns: a Deferred object that will fire once all Components have been sucessfully resumed :returns: a Deferred object that will fire once all Components have been successfully shut down
:rtype: twisted.internet.defer.Deferred :rtype: twisted.internet.defer.Deferred
""" """
deferreds = [] def on_stopped(result):
return DeferredList(map(lambda c: c._component_shutdown(), self.components.values()))
for component in self.components.values(): return self.stop(self.components.keys()).addCallback(on_stopped)
deferreds.append(component._component_shutdown())
return DeferredList(deferreds)
def update(self): def update(self):
""" """

View file

@ -75,7 +75,8 @@ class AlertManager(component.Component):
def stop(self): def stop(self):
for dc in self.delayed_calls: for dc in self.delayed_calls:
dc.cancel() if dc.active():
dc.cancel()
self.delayed_calls = [] self.delayed_calls = []
def register_handler(self, alert_type, handler): def register_handler(self, alert_type, handler):

View file

@ -236,10 +236,10 @@ class AuthManager(component.Component):
f = open(auth_file, "r").readlines() f = open(auth_file, "r").readlines()
for line in f: for line in f:
if line.startswith("#"):
# This is a comment line
continue
line = line.strip() line = line.strip()
if line.startswith("#") or not line:
# This line is a comment or empty
continue
try: try:
lsplit = line.split(":") lsplit = line.split(":")
except Exception, e: except Exception, e:

View file

@ -94,6 +94,8 @@ class Core(component.Component):
self.settings.user_agent = "Deluge/%(deluge_version)s Libtorrent/%(lt_version)s" % \ self.settings.user_agent = "Deluge/%(deluge_version)s Libtorrent/%(lt_version)s" % \
{ 'deluge_version': deluge.common.get_version(), { 'deluge_version': deluge.common.get_version(),
'lt_version': self.get_libtorrent_version().rpartition(".")[0] } 'lt_version': self.get_libtorrent_version().rpartition(".")[0] }
# Increase the alert queue size so that alerts don't get lost
self.settings.alert_queue_size = 10000
# Set session settings # Set session settings
self.settings.send_redundant_have = True self.settings.send_redundant_have = True

View file

@ -153,9 +153,8 @@ class FilterManager(component.Component):
#sanitize input: filter-value must be a list of strings #sanitize input: filter-value must be a list of strings
for key, value in filter_dict.items(): for key, value in filter_dict.items():
if isinstance(value, str): if isinstance(value, basestring):
filter_dict[key] = [value] filter_dict[key] = [value]
if "id"in filter_dict: #optimized filter for id: if "id"in filter_dict: #optimized filter for id:
torrent_ids = list(filter_dict["id"]) torrent_ids = list(filter_dict["id"])

View file

@ -37,9 +37,12 @@
import os import os
import time import time
import logging import logging
import re
from urllib import unquote from urllib import unquote
from urlparse import urlparse from urlparse import urlparse
from twisted.internet.defer import Deferred, DeferredList
from twisted.internet.task import LoopingCall
from deluge._libtorrent import lt from deluge._libtorrent import lt
import deluge.common import deluge.common
@ -116,7 +119,6 @@ class Torrent(object):
# We use this to return dicts that only contain changes from the previous # We use this to return dicts that only contain changes from the previous
# {session_id: status_dict, ...} # {session_id: status_dict, ...}
self.prev_status = {} self.prev_status = {}
from twisted.internet.task import LoopingCall
self.prev_status_cleanup_loop = LoopingCall(self.cleanup_prev_status) self.prev_status_cleanup_loop = LoopingCall(self.cleanup_prev_status)
self.prev_status_cleanup_loop.start(10) self.prev_status_cleanup_loop.start(10)
@ -125,14 +127,10 @@ class Torrent(object):
# Set the torrent_id for this torrent # Set the torrent_id for this torrent
self.torrent_id = str(handle.info_hash()) self.torrent_id = str(handle.info_hash())
# Let's us know if we're waiting on a lt alert # Keep a list of Deferreds for file indexes we're waiting for file_rename alerts on
self.waiting_on_resume_data = False
# Keep a list of file indexes we're waiting for file_rename alerts on
# This also includes the old_folder and new_folder to know what signal to send
# This is so we can send one folder_renamed signal instead of multiple # This is so we can send one folder_renamed signal instead of multiple
# file_renamed signals. # file_renamed signals.
# [(old_folder, new_folder, [*indexes]), ...] # [{index: Deferred, ...}, ...]
self.waiting_on_folder_rename = [] self.waiting_on_folder_rename = []
# We store the filename just in case we need to make a copy of the torrentfile # We store the filename just in case we need to make a copy of the torrentfile
@ -177,13 +175,7 @@ class Torrent(object):
# Tracker list # Tracker list
self.trackers = [] self.trackers = []
# Create a list of trackers # Create a list of trackers
for value in self.handle.trackers(): for tracker in self.handle.trackers():
if lt.version_minor < 15:
tracker = {}
tracker["url"] = value.url
tracker["tier"] = value.tier
else:
tracker = value
self.trackers.append(tracker) self.trackers.append(tracker)
# Various torrent options # Various torrent options
@ -252,7 +244,7 @@ class Torrent(object):
def get_name(self): def get_name(self):
if self.handle.has_metadata(): if self.handle.has_metadata():
name = self.torrent_info.file_at(0).path.split("/", 1)[0] name = self.torrent_info.file_at(0).path.replace("\\", "/", 1).split("/", 1)[0]
if not name: if not name:
name = self.torrent_info.name() name = self.torrent_info.name()
try: try:
@ -885,31 +877,32 @@ class Torrent(object):
def move_storage(self, dest): def move_storage(self, dest):
"""Move a torrent's storage location""" """Move a torrent's storage location"""
try:
if deluge.common.windows_check(): dest = unicode(dest, "utf-8")
# Attempt to convert utf8 path to unicode except TypeError:
# Note: Inconsistent encoding for 'dest', needs future investigation # String is already unicode
try: pass
dest_u = unicode(dest, "utf-8")
except TypeError:
# String is already unicode
dest_u = dest
else:
dest_u = dest
if not os.path.exists(dest_u): if not os.path.exists(dest):
try: try:
# Try to make the destination path if it doesn't exist # Try to make the destination path if it doesn't exist
os.makedirs(dest_u) os.makedirs(dest)
except IOError, e: except IOError, e:
log.exception(e) log.exception(e)
log.error("Could not move storage for torrent %s since %s does " log.error("Could not move storage for torrent %s since %s does "
"not exist and could not create the directory.", "not exist and could not create the directory.",
self.torrent_id, dest_u) self.torrent_id, dest)
return False return False
dest_bytes = dest.encode('utf-8')
try: try:
self.handle.move_storage(dest_u) # libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise
except: try:
self.handle.move_storage(dest)
except TypeError:
self.handle.move_storage(dest_bytes)
except Exception, e:
log.error("Error calling libtorrent move_storage: %s" % e)
return False return False
return True return True
@ -918,7 +911,6 @@ class Torrent(object):
"""Signals libtorrent to build resume data for this torrent, it gets """Signals libtorrent to build resume data for this torrent, it gets
returned in a libtorrent alert""" returned in a libtorrent alert"""
self.handle.save_resume_data() self.handle.save_resume_data()
self.waiting_on_resume_data = True
def write_torrentfile(self): def write_torrentfile(self):
"""Writes the torrent file""" """Writes the torrent file"""
@ -985,12 +977,26 @@ class Torrent(object):
"""Renames files in the torrent. 'filenames' should be a list of """Renames files in the torrent. 'filenames' should be a list of
(index, filename) pairs.""" (index, filename) pairs."""
for index, filename in filenames: for index, filename in filenames:
# Make sure filename is a unicode object
try:
filename = unicode(filename, "utf-8")
except TypeError:
pass
filename = sanitize_filepath(filename) filename = sanitize_filepath(filename)
self.handle.rename_file(index, filename.encode("utf-8")) # libtorrent needs unicode object if wstrings are enabled, utf8 bytestring otherwise
try:
self.handle.rename_file(index, filename)
except TypeError:
self.handle.rename_file(index, filename.encode("utf-8"))
def rename_folder(self, folder, new_folder): def rename_folder(self, folder, new_folder):
"""Renames a folder within a torrent. This basically does a file rename """
on all of the folders children.""" Renames a folder within a torrent. This basically does a file rename
on all of the folders children.
:returns: A deferred which fires when the rename is complete
:rtype: twisted.internet.defer.Deferred
"""
log.debug("attempting to rename folder: %s to %s", folder, new_folder) log.debug("attempting to rename folder: %s to %s", folder, new_folder)
if len(new_folder) < 1: if len(new_folder) < 1:
log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder) log.error("Attempting to rename a folder with an invalid folder name: %s", new_folder)
@ -998,13 +1004,57 @@ class Torrent(object):
new_folder = sanitize_filepath(new_folder, folder=True) new_folder = sanitize_filepath(new_folder, folder=True)
wait_on_folder = (folder, new_folder, []) def on_file_rename_complete(result, wait_dict, index):
wait_dict.pop(index, None)
wait_on_folder = {}
self.waiting_on_folder_rename.append(wait_on_folder)
for f in self.get_files(): for f in self.get_files():
if f["path"].startswith(folder): if f["path"].startswith(folder):
# Keep a list of filerenames we're waiting on # Keep track of filerenames we're waiting on
wait_on_folder[2].append(f["index"]) wait_on_folder[f["index"]] = Deferred().addBoth(on_file_rename_complete, wait_on_folder, f["index"])
self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1).encode("utf-8")) self.handle.rename_file(f["index"], f["path"].replace(folder, new_folder, 1).encode("utf-8"))
self.waiting_on_folder_rename.append(wait_on_folder)
def on_folder_rename_complete(result, torrent, folder, new_folder):
component.get("EventManager").emit(TorrentFolderRenamedEvent(torrent.torrent_id, folder, new_folder))
# Empty folders are removed after libtorrent folder renames
self.remove_empty_folders(folder)
torrent.waiting_on_folder_rename = filter(None, torrent.waiting_on_folder_rename)
component.get("TorrentManager").save_resume_data((self.torrent_id,))
d = DeferredList(wait_on_folder.values())
d.addBoth(on_folder_rename_complete, self, folder, new_folder)
return d
def remove_empty_folders(self, folder):
"""
Recursively removes folders but only if they are empty.
Cleans up after libtorrent folder renames.
"""
info = self.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):
from errno import ENOTEMPTY
if errno == ENOTEMPTY:
# 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 cleanup_prev_status(self): def cleanup_prev_status(self):
""" """

View file

@ -38,13 +38,12 @@
import cPickle import cPickle
import os import os
import time
import shutil import shutil
import operator import operator
import logging import logging
import re
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet.defer import Deferred, DeferredList
from deluge._libtorrent import lt from deluge._libtorrent import lt
@ -133,7 +132,7 @@ class TorrentManager(component.Component):
def __init__(self): def __init__(self):
component.Component.__init__(self, "TorrentManager", interval=5, component.Component.__init__(self, "TorrentManager", interval=5,
depend=["CorePluginManager"]) depend=["CorePluginManager", "AlertManager"])
log.debug("TorrentManager init..") log.debug("TorrentManager init..")
# Set the libtorrent session # Set the libtorrent session
self.session = component.get("Core").session self.session = component.get("Core").session
@ -151,15 +150,11 @@ class TorrentManager(component.Component):
self.last_seen_complete_loop = None self.last_seen_complete_loop = None
self.queued_torrents = set() self.queued_torrents = set()
# This is a list of torrent_id when we shutdown the torrentmanager. # This is a map of torrent_ids to Deferreds used to track needed resume data.
# We use this list to determine if all active torrents have been paused # The Deferreds will be completed when resume data has been saved.
# and that their resume data has been written. self.waiting_on_resume_data = {}
self.shutdown_torrent_pause_list = []
# self.num_resume_data used to save resume_data in bulk # Keeps track of resume data
self.num_resume_data = 0
# Keeps track of resume data that needs to be saved to disk
self.resume_data = {} self.resume_data = {}
# Register set functions # Register set functions
@ -216,11 +211,14 @@ class TorrentManager(component.Component):
# Try to load the state from file # Try to load the state from file
self.load_state() self.load_state()
# Save the state every 5 minutes # Save the state periodically
self.save_state_timer = LoopingCall(self.save_state) self.save_state_timer = LoopingCall(self.save_state)
self.save_state_timer.start(200, False) self.save_state_timer.start(200, False)
self.save_resume_data_timer = LoopingCall(self.save_resume_data) self.save_resume_data_timer = LoopingCall(self.save_resume_data)
self.save_resume_data_timer.start(190) self.save_resume_data_timer.start(190, False)
# Force update for all resume data a bit less frequently
self.save_all_resume_data_timer = LoopingCall(self.save_resume_data, self.torrents.keys())
self.save_all_resume_data_timer.start(900, False)
if self.last_seen_complete_loop: if self.last_seen_complete_loop:
self.last_seen_complete_loop.start(60) self.last_seen_complete_loop.start(60)
@ -233,45 +231,21 @@ class TorrentManager(component.Component):
if self.save_resume_data_timer.running: if self.save_resume_data_timer.running:
self.save_resume_data_timer.stop() self.save_resume_data_timer.stop()
if self.save_all_resume_data_timer.running:
self.save_all_resume_data_timer.stop()
if self.last_seen_complete_loop: if self.last_seen_complete_loop:
self.last_seen_complete_loop.stop() self.last_seen_complete_loop.stop()
# Save state on shutdown # Save state on shutdown
self.save_state() self.save_state()
# Make another list just to make sure all paused torrents will be self.session.pause()
# passed to self.save_resume_data(). With
# self.shutdown_torrent_pause_list it is possible to have a case when
# torrent_id is removed from it in self.on_alert_torrent_paused()
# before we call self.save_resume_data() here.
save_resume_data_list = []
for key in self.torrents: for key in self.torrents:
# Stop the status cleanup LoopingCall here # Stop the status cleanup LoopingCall here
self.torrents[key].prev_status_cleanup_loop.stop() self.torrents[key].prev_status_cleanup_loop.stop()
if not self.torrents[key].handle.is_paused():
# We set auto_managed false to prevent lt from resuming the torrent
self.torrents[key].handle.auto_managed(False)
self.torrents[key].handle.pause()
self.shutdown_torrent_pause_list.append(key)
save_resume_data_list.append(key)
self.save_resume_data(save_resume_data_list) return self.save_resume_data(self.torrents.keys())
# We have to wait for all torrents to pause and write their resume data
wait = True
while wait:
if self.shutdown_torrent_pause_list:
wait = True
else:
wait = False
for torrent in self.torrents.values():
if torrent.waiting_on_resume_data:
wait = True
break
time.sleep(0.01)
# Wait for all alerts
self.alerts.handle_alerts(True)
def update(self): def update(self):
for torrent_id, torrent in self.torrents.items(): for torrent_id, torrent in self.torrents.items():
@ -452,9 +426,16 @@ class TorrentManager(component.Component):
# before adding to the session. # before adding to the session.
if options["mapped_files"]: if options["mapped_files"]:
for index, fname in options["mapped_files"].items(): for index, fname in options["mapped_files"].items():
try:
fname = unicode(fname, "utf-8")
except TypeError:
pass
fname = deluge.core.torrent.sanitize_filepath(fname) fname = deluge.core.torrent.sanitize_filepath(fname)
log.debug("renaming file index %s to %s", index, fname) log.debug("renaming file index %s to %s", index, fname)
torrent_info.rename_file(index, utf8_encoded(fname)) try:
torrent_info.rename_file(index, fname)
except TypeError:
torrent_info.rename_file(index, fname.encode("utf-8"))
add_torrent_params["ti"] = torrent_info add_torrent_params["ti"] = torrent_info
@ -589,12 +570,11 @@ class TorrentManager(component.Component):
:raises InvalidTorrentError: if the torrent_id is not in the session :raises InvalidTorrentError: if the torrent_id is not in the session
""" """
try:
if torrent_id not in self.torrents: torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
except KeyError:
raise InvalidTorrentError("torrent_id not in session") raise InvalidTorrentError("torrent_id not in session")
torrent_name = self.torrents[torrent_id].get_status(["name"])["name"]
# Emit the signal to the clients # Emit the signal to the clients
component.get("EventManager").emit(PreTorrentRemovedEvent(torrent_id)) component.get("EventManager").emit(PreTorrentRemovedEvent(torrent_id))
@ -606,9 +586,7 @@ class TorrentManager(component.Component):
return False return False
# Remove fastresume data if it is exists # Remove fastresume data if it is exists
resume_data = self.load_resume_data_file() self.resume_data.pop(torrent_id, None)
resume_data.pop(torrent_id, None)
self.save_resume_data_file(resume_data)
# Remove the .torrent file in the state # Remove the .torrent file in the state
self.torrents[torrent_id].delete_torrentfile() self.torrents[torrent_id].delete_torrentfile()
@ -678,7 +656,7 @@ class TorrentManager(component.Component):
# Reorder the state.torrents list to add torrents in the correct queue # Reorder the state.torrents list to add torrents in the correct queue
# order. # order.
state.torrents.sort(key=operator.attrgetter("queue")) state.torrents.sort(key=operator.attrgetter("queue"), reverse=self.config["queue_new_to_top"])
resume_data = self.load_resume_data_file() resume_data = self.load_resume_data_file()
@ -770,17 +748,34 @@ class TorrentManager(component.Component):
def save_resume_data(self, torrent_ids=None): def save_resume_data(self, torrent_ids=None):
""" """
Saves resume data for list of torrent_ids or for all torrents if Saves resume data for list of torrent_ids or for all torrents
torrent_ids is None needing resume data updated if torrent_ids is None
:returns: A Deferred whose callback will be invoked when save is complete
:rtype: twisted.internet.defer.Deferred
""" """
if torrent_ids is None: if torrent_ids is None:
torrent_ids = self.torrents.keys() torrent_ids = (t[0] for t in self.torrents.iteritems() if t[1].handle.need_save_resume_data())
deferreds = []
def on_torrent_resume_save(result, torrent_id):
self.waiting_on_resume_data.pop(torrent_id, None)
for torrent_id in torrent_ids: for torrent_id in torrent_ids:
d = self.waiting_on_resume_data.get(torrent_id)
if not d:
d = Deferred().addBoth(on_torrent_resume_save, torrent_id)
self.waiting_on_resume_data[torrent_id] = d
deferreds.append(d)
self.torrents[torrent_id].save_resume_data() self.torrents[torrent_id].save_resume_data()
self.num_resume_data = len(torrent_ids) def on_all_resume_data_finished(result):
if result:
self.save_resume_data_file()
return DeferredList(deferreds).addBoth(on_all_resume_data_finished)
def load_resume_data_file(self): def load_resume_data_file(self):
resume_data = {} resume_data = {}
@ -800,73 +795,22 @@ class TorrentManager(component.Component):
return resume_data return resume_data
def save_resume_data_file(self, resume_data=None): def save_resume_data_file(self):
""" """
Saves the resume data file with the contents of self.resume_data. If Saves the resume data file with the contents of self.resume_data.
`resume_data` is None, then we grab the resume_data from the file on
disk, else, we update `resume_data` with self.resume_data and save
that to disk.
:param resume_data: the current resume_data, this will be loaded from disk if not provided
:type resume_data: dict
""" """
# Check to see if we're waiting on more resume data
if self.num_resume_data or not self.resume_data:
return
path = os.path.join(get_config_dir(), "state", "torrents.fastresume") path = os.path.join(get_config_dir(), "state", "torrents.fastresume")
# First step is to load the existing file and update the dictionary
if resume_data is None:
resume_data = self.load_resume_data_file()
resume_data.update(self.resume_data)
self.resume_data = {}
try: try:
log.debug("Saving fastresume file: %s", path) log.debug("Saving fastresume file: %s", path)
fastresume_file = open(path, "wb") fastresume_file = open(path, "wb")
fastresume_file.write(lt.bencode(resume_data)) fastresume_file.write(lt.bencode(self.resume_data))
fastresume_file.flush() fastresume_file.flush()
os.fsync(fastresume_file.fileno()) os.fsync(fastresume_file.fileno())
fastresume_file.close() fastresume_file.close()
except IOError: except IOError:
log.warning("Error trying to save fastresume file") 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):
from errno import ENOTEMPTY
if errno == ENOTEMPTY:
# 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 get_queue_position(self, torrent_id): def get_queue_position(self, torrent_id):
"""Get queue position of torrent""" """Get queue position of torrent"""
return self.torrents[torrent_id].get_queue_position() return self.torrents[torrent_id].get_queue_position()
@ -981,14 +925,9 @@ class TorrentManager(component.Component):
if torrent.state != old_state: if torrent.state != old_state:
component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state)) component.get("EventManager").emit(TorrentStateChangedEvent(torrent_id, torrent.state))
# Don't save resume data for each torrent after self.stop() was called. # Write the fastresume file if we are not waiting on a bulk write
# We save resume data in bulk in self.stop() in this case. if torrent_id not in self.waiting_on_resume_data:
if self.save_resume_data_timer.running: self.save_resume_data((torrent_id,))
# Write the fastresume file
self.save_resume_data((torrent_id, ))
if torrent_id in self.shutdown_torrent_pause_list:
self.shutdown_torrent_pause_list.remove(torrent_id)
def on_alert_torrent_checked(self, alert): def on_alert_torrent_checked(self, alert):
log.debug("on_alert_torrent_checked") log.debug("on_alert_torrent_checked")
@ -1098,32 +1037,21 @@ class TorrentManager(component.Component):
def on_alert_save_resume_data(self, alert): def on_alert_save_resume_data(self, alert):
log.debug("on_alert_save_resume_data") log.debug("on_alert_save_resume_data")
try: torrent_id = str(alert.handle.info_hash())
torrent_id = str(alert.handle.info_hash())
torrent = self.torrents[torrent_id]
except:
return
# Libtorrent in add_torrent() expects resume_data to be bencoded if torrent_id in self.torrents:
self.resume_data[torrent_id] = lt.bencode(alert.resume_data) # Libtorrent in add_torrent() expects resume_data to be bencoded
self.num_resume_data -= 1 self.resume_data[torrent_id] = lt.bencode(alert.resume_data)
torrent.waiting_on_resume_data = False if torrent_id in self.waiting_on_resume_data:
self.waiting_on_resume_data[torrent_id].callback(None)
self.save_resume_data_file()
def on_alert_save_resume_data_failed(self, alert): def on_alert_save_resume_data_failed(self, alert):
log.debug("on_alert_save_resume_data_failed: %s", alert.message()) log.debug("on_alert_save_resume_data_failed: %s", alert.message())
try: torrent_id = str(alert.handle.info_hash())
torrent = self.torrents[str(alert.handle.info_hash())]
except:
return
self.num_resume_data -= 1
torrent.waiting_on_resume_data = False
self.save_resume_data_file()
if torrent_id in self.waiting_on_resume_data:
self.waiting_on_resume_data[torrent_id].errback(Exception(alert.message()))
def on_alert_file_renamed(self, alert): def on_alert_file_renamed(self, alert):
log.debug("on_alert_file_renamed") log.debug("on_alert_file_renamed")
@ -1134,24 +1062,12 @@ class TorrentManager(component.Component):
except: except:
return return
# We need to see if this file index is in a waiting_on_folder list # We need to see if this file index is in a waiting_on_folder dict
folder_rename = False for wait_on_folder in torrent.waiting_on_folder_rename:
for i, wait_on_folder in enumerate(torrent.waiting_on_folder_rename): if alert.index in wait_on_folder:
if alert.index in wait_on_folder[2]: wait_on_folder[alert.index].callback(None)
folder_rename = True break
if len(wait_on_folder[2]) == 1: else:
# 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
# This isn't the last file to be renamed in this folder, so just
# remove the index and continue
torrent.waiting_on_folder_rename[i][2].remove(alert.index)
if not folder_rename:
# This is just a regular file rename so send the signal # This is just a regular file rename so send the signal
component.get("EventManager").emit(TorrentFileRenamedEvent(torrent_id, alert.index, alert.name)) component.get("EventManager").emit(TorrentFileRenamedEvent(torrent_id, alert.index, alert.name))
self.save_resume_data((torrent_id,)) self.save_resume_data((torrent_id,))

View file

@ -86,7 +86,7 @@ def start_ui():
help="Rotate logfiles.", action="store_true", default=False) help="Rotate logfiles.", action="store_true", default=False)
# Get the options and args from the OptionParser # Get the options and args from the OptionParser
(options, args) = parser.parse_args() (options, args) = parser.parse_args(deluge.common.unicode_argv()[1:])
if options.quiet: if options.quiet:
options.loglevel = "none" options.loglevel = "none"

View file

@ -403,12 +403,11 @@ class GtkUI(GtkPluginBase):
sw.add(self.treeView) sw.add(self.treeView)
sw.show_all() sw.show_all()
component.get("Preferences").add_page( component.get("Preferences").add_page(
"AutoAdd", self.glade.get_widget("prefs_box") _("AutoAdd"), self.glade.get_widget("prefs_box")
) )
self.on_show_prefs()
def disable(self): def disable(self):
component.get("Preferences").remove_page("AutoAdd") component.get("Preferences").remove_page(_("AutoAdd"))
component.get("PluginManager").deregister_hook( component.get("PluginManager").deregister_hook(
"on_apply_prefs", self.on_apply_prefs "on_apply_prefs", self.on_apply_prefs
) )

View file

@ -54,13 +54,13 @@ class GtkUI(GtkPluginBase):
def enable(self): def enable(self):
self.glade = gtk.glade.XML(get_resource("extractor_prefs.glade")) self.glade = gtk.glade.XML(get_resource("extractor_prefs.glade"))
component.get("Preferences").add_page("Extractor", self.glade.get_widget("extractor_prefs_box")) component.get("Preferences").add_page(_("Extractor"), self.glade.get_widget("extractor_prefs_box"))
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
self.on_show_prefs() self.on_show_prefs()
def disable(self): def disable(self):
component.get("Preferences").remove_page("Extractor") component.get("Preferences").remove_page(_("Extractor"))
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_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) component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)
del self.glade del self.glade

View file

@ -287,7 +287,7 @@ class GtkUI(GtkPluginBase, GtkUiNotifications):
if parent: if parent:
parent.remove(self.prefs) parent.remove(self.prefs)
index = prefs.notebook.append_page(self.prefs) index = prefs.notebook.append_page(self.prefs)
prefs.liststore.append([index, "Notifications"]) prefs.liststore.append([index, _("Notifications")])
component.get("PluginManager").register_hook("on_apply_prefs", component.get("PluginManager").register_hook("on_apply_prefs",
self.on_apply_prefs) self.on_apply_prefs)
@ -320,7 +320,7 @@ class GtkUI(GtkPluginBase, GtkUiNotifications):
def disable(self): def disable(self):
GtkUiNotifications.disable(self) GtkUiNotifications.disable(self)
component.get("Preferences").remove_page("Notifications") component.get("Preferences").remove_page(_("Notifications"))
component.get("PluginManager").deregister_hook("on_apply_prefs", component.get("PluginManager").deregister_hook("on_apply_prefs",
self.on_apply_prefs) self.on_apply_prefs)
component.get("PluginManager").deregister_hook("on_show_prefs", component.get("PluginManager").deregister_hook("on_show_prefs",

View file

@ -170,7 +170,7 @@ class GtkUI(GtkPluginBase):
client.register_event_handler("SchedulerEvent", self.on_scheduler_event) client.register_event_handler("SchedulerEvent", self.on_scheduler_event)
def disable(self): def disable(self):
component.get("Preferences").remove_page("Scheduler") component.get("Preferences").remove_page(_("Scheduler"))
# Remove status item # Remove status item
component.get("StatusBar").remove_item(self.status_item) component.get("StatusBar").remove_item(self.status_item)
del self.status_item del self.status_item
@ -294,4 +294,4 @@ class GtkUI(GtkPluginBase):
vbox.pack_start(frame, False, False) vbox.pack_start(frame, False, False)
vbox.show_all() vbox.show_all()
component.get("Preferences").add_page("Scheduler", vbox) component.get("Preferences").add_page(_("Scheduler"), vbox)

View file

@ -53,14 +53,14 @@ class GtkUI(GtkPluginBase):
def enable(self): def enable(self):
self.glade = gtk.glade.XML(get_resource("config.glade")) self.glade = gtk.glade.XML(get_resource("config.glade"))
component.get("Preferences").add_page("WebUi", self.glade.get_widget("prefs_box")) component.get("Preferences").add_page(_("WebUi"), self.glade.get_widget("prefs_box"))
component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs) component.get("PluginManager").register_hook("on_apply_prefs", self.on_apply_prefs)
component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs) component.get("PluginManager").register_hook("on_show_prefs", self.on_show_prefs)
client.webui.get_config().addCallback(self.cb_get_config) client.webui.get_config().addCallback(self.cb_get_config)
client.webui.got_deluge_web().addCallback(self.cb_chk_deluge_web) client.webui.got_deluge_web().addCallback(self.cb_chk_deluge_web)
def disable(self): def disable(self):
component.get("Preferences").remove_page("WebUi") component.get("Preferences").remove_page(_("WebUi"))
component.get("PluginManager").deregister_hook("on_apply_prefs", self.on_apply_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) component.get("PluginManager").deregister_hook("on_show_prefs", self.on_show_prefs)

View file

@ -19,7 +19,7 @@ rencode module versions, so you should check that you are using the
same rencode version throughout your project. same rencode version throughout your project.
""" """
__version__ = '1.0.1' __version__ = '1.0.2'
__all__ = ['dumps', 'loads'] __all__ = ['dumps', 'loads']
# Original bencode module by Petru Paler, et al. # Original bencode module by Petru Paler, et al.
@ -107,6 +107,9 @@ STR_FIXED_COUNT = 64
LIST_FIXED_START = STR_FIXED_START+STR_FIXED_COUNT LIST_FIXED_START = STR_FIXED_START+STR_FIXED_COUNT
LIST_FIXED_COUNT = 64 LIST_FIXED_COUNT = 64
# Whether strings should be decoded when loading
_decode_utf8 = False
def decode_int(x, f): def decode_int(x, f):
f += 1 f += 1
newf = x.index(CHR_TERM, f) newf = x.index(CHR_TERM, f)
@ -159,12 +162,8 @@ def decode_string(x, f):
raise ValueError raise ValueError
colon += 1 colon += 1
s = x[colon:colon+n] s = x[colon:colon+n]
try: if _decode_utf8:
t = s.decode("utf8") s = s.decode('utf8')
if len(t) != len(s):
s = t
except UnicodeDecodeError:
pass
return (s, colon+n) return (s, colon+n)
def decode_list(x, f): def decode_list(x, f):
@ -218,12 +217,8 @@ def make_fixed_length_string_decoders():
def make_decoder(slen): def make_decoder(slen):
def f(x, f): def f(x, f):
s = x[f+1:f+1+slen] s = x[f+1:f+1+slen]
try: if _decode_utf8:
t = s.decode("utf8") s = s.decode("utf8")
if len(t) != len(s):
s = t
except UnicodeDecodeError:
pass
return (s, f+1+slen) return (s, f+1+slen)
return f return f
for i in range(STR_FIXED_COUNT): for i in range(STR_FIXED_COUNT):
@ -279,7 +274,9 @@ def encode_dict(x,r):
r.append(CHR_TERM) r.append(CHR_TERM)
def loads(x): def loads(x, decode_utf8=False):
global _decode_utf8
_decode_utf8 = decode_utf8
try: try:
r, l = decode_func[x[0]](x, 0) r, l = decode_func[x[0]](x, 0)
except (IndexError, KeyError): except (IndexError, KeyError):

View file

@ -3,8 +3,8 @@ from twisted.internet import threads
import deluge.component as component import deluge.component as component
class testcomponent(component.Component): class testcomponent(component.Component):
def __init__(self, name): def __init__(self, name, depend=None):
component.Component.__init__(self, name) component.Component.__init__(self, name, depend=depend)
self.start_count = 0 self.start_count = 0
self.stop_count = 0 self.stop_count = 0
@ -51,6 +51,7 @@ class testcomponent_shutdown(component.Component):
class ComponentTestClass(unittest.TestCase): class ComponentTestClass(unittest.TestCase):
def tearDown(self): def tearDown(self):
component.stop()
component._ComponentRegistry.components = {} component._ComponentRegistry.components = {}
def test_start_component(self): def test_start_component(self):
@ -63,16 +64,22 @@ class ComponentTestClass(unittest.TestCase):
d.addCallback(on_start, c) d.addCallback(on_start, c)
return d return d
def test_start_depends(self): def test_start_stop_depends(self):
def on_stop(result, c1, c2):
self.assertEquals(c1._component_state, "Stopped")
self.assertEquals(c2._component_state, "Stopped")
self.assertEquals(c1.stop_count, 1)
self.assertEquals(c2.stop_count, 1)
def on_start(result, c1, c2): def on_start(result, c1, c2):
self.assertEquals(c1._component_state, "Started") self.assertEquals(c1._component_state, "Started")
self.assertEquals(c2._component_state, "Started") self.assertEquals(c2._component_state, "Started")
self.assertEquals(c1.start_count, 1) self.assertEquals(c1.start_count, 1)
self.assertEquals(c2.start_count, 1) self.assertEquals(c2.start_count, 1)
return component.stop(["test_start_depends_c1"]).addCallback(on_stop, c1, c2)
c1 = testcomponent("test_start_depends_c1") c1 = testcomponent("test_start_depends_c1")
c2 = testcomponent("test_start_depends_c2") c2 = testcomponent("test_start_depends_c2", depend=["test_start_depends_c1"])
c2._component_depend = ["test_start_depends_c1"]
d = component.start(["test_start_depends_c2"]) d = component.start(["test_start_depends_c2"])
d.addCallback(on_start, c1, c2) d.addCallback(on_start, c1, c2)
@ -80,15 +87,12 @@ class ComponentTestClass(unittest.TestCase):
def start_with_depends(self): def start_with_depends(self):
c1 = testcomponent_delaystart("test_start_all_c1") c1 = testcomponent_delaystart("test_start_all_c1")
c2 = testcomponent("test_start_all_c2") c2 = testcomponent("test_start_all_c2", depend=["test_start_all_c4"])
c3 = testcomponent_delaystart("test_start_all_c3") c3 = testcomponent_delaystart("test_start_all_c3",
c4 = testcomponent("test_start_all_c4") depend=["test_start_all_c5", "test_start_all_c1"])
c4 = testcomponent("test_start_all_c4", depend=["test_start_all_c3"])
c5 = testcomponent("test_start_all_c5") c5 = testcomponent("test_start_all_c5")
c3._component_depend = ["test_start_all_c5", "test_start_all_c1"]
c4._component_depend = ["test_start_all_c3"]
c2._component_depend = ["test_start_all_c4"]
d = component.start() d = component.start()
return (d, c1, c2, c3, c4, c5) return (d, c1, c2, c3, c4, c5)
@ -130,15 +134,15 @@ class ComponentTestClass(unittest.TestCase):
return d return d
def test_stop_all(self): def test_stop_all(self):
def on_stop(*args): def on_stop(result, *args):
for c in args[1:]: for c in args:
self.assertEquals(c._component_state, "Stopped") self.assertEquals(c._component_state, "Stopped")
self.assertEquals(c.stop_count, 1) self.assertEquals(c.stop_count, 1)
def on_start(*args): def on_start(result, *args):
for c in args[1:]: for c in args:
self.assertEquals(c._component_state, "Started") self.assertEquals(c._component_state, "Started")
return component.stop().addCallback(on_stop, *args[1:]) return component.stop().addCallback(on_stop, *args)
ret = self.start_with_depends() ret = self.start_with_depends()
ret[0].addCallback(on_start, *ret[1:]) ret[0].addCallback(on_start, *ret[1:])

View file

@ -140,7 +140,7 @@ class DelugeTransferProtocol(Protocol):
""" """
try: try:
self.message_received(rencode.loads(zlib.decompress(data))) self.message_received(rencode.loads(zlib.decompress(data), decode_utf8=True))
except Exception, e: except Exception, e:
log.warn("Failed to decompress (%d bytes) and load serialized data "\ log.warn("Failed to decompress (%d bytes) and load serialized data "\
"with rencode: %s" % (len(data), str(e))) "with rencode: %s" % (len(data), str(e)))

View file

@ -37,17 +37,14 @@
import logging import logging
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, ssl, defer from twisted.internet import reactor, ssl, defer
import sys
import subprocess
import deluge.common import deluge.common
from deluge import error from deluge import error
from deluge.event import known_events from deluge.event import known_events
from deluge.transfer import DelugeTransferProtocol from deluge.transfer import DelugeTransferProtocol
if deluge.common.windows_check():
import win32api
else:
import subprocess
RPC_RESPONSE = 1 RPC_RESPONSE = 1
RPC_ERROR = 2 RPC_ERROR = 2
RPC_EVENT = 3 RPC_EVENT = 3
@ -628,13 +625,10 @@ class Client(object):
:raises OSError: received from subprocess.call() :raises OSError: received from subprocess.call()
""" """
# subprocess.popen does not work with unicode args (with non-ascii characters) on windows
config = config.encode(sys.getfilesystemencoding())
try: try:
if deluge.common.windows_check(): subprocess.Popen(["deluged", "--port=%s" % port, "--config=%s" % config])
win32api.WinExec("deluged --port=%s --config=\"%s\"" % (port, config))
elif deluge.common.osx_check():
subprocess.call(["nohup", "deluged", "--port=%s" % port, "--config=%s" % config])
else:
subprocess.call(["deluged", "--port=%s" % port, "--config=%s" % config])
except OSError, e: except OSError, e:
from errno import ENOENT from errno import ENOENT
if e.errno == ENOENT: if e.errno == ENOENT:

View file

@ -106,8 +106,11 @@ class TorrentInfo(object):
else: else:
path = utf8_encoded(os.path.join(prefix, utf8_encoded(os.path.join(*f["path"]), self.encoding)), self.encoding) path = utf8_encoded(os.path.join(prefix, utf8_encoded(os.path.join(*f["path"]), self.encoding)), self.encoding)
f["index"] = index f["index"] = index
if "sha1" in f and len(f["sha1"]) == 20:
f["sha1"] = f["sha1"].encode('hex')
if "ed2k" in f and len(f["ed2k"]) == 16:
f["ed2k"] = f["ed2k"].encode('hex')
paths[path] = f paths[path] = f
dirname = os.path.dirname(path) dirname = os.path.dirname(path)
while dirname: while dirname:
dirinfo = dirs.setdefault(dirname, {}) dirinfo = dirs.setdefault(dirname, {})

View file

@ -133,7 +133,22 @@ class Command(BaseCommand):
deferred.callback(True) deferred.callback(True)
self.console.write("Setting %s to %s for torrents %s.." % (key, val, torrent_ids)) self.console.write("Setting %s to %s for torrents %s.." % (key, val, torrent_ids))
client.core.set_torrent_options(torrent_ids, {key: val}).addCallback(on_set_config)
for tid in torrent_ids:
if key == "move_on_completed_path":
client.core.set_torrent_move_completed_path(tid, val).addCallback(on_set_config)
elif key == "move_on_completed":
client.core.set_torrent_move_completed(tid, val).addCallback(on_set_config)
elif key == "is_auto_managed":
client.core.set_torrent_auto_managed(tid, val).addCallback(on_set_config)
elif key == "remove_at_ratio":
client.core.set_torrent_remove_at_ratio(tid, val).addCallback(on_set_config)
elif key == "prioritize_first_last":
client.core.set_torrent_prioritize_first_last(tid, val).addCallback(on_set_config)
else:
client.core.set_torrent_options(torrent_ids, {key: val}).addCallback(on_set_config)
break
return deferred return deferred
def complete(self, line): def complete(self, line):

View file

@ -250,6 +250,8 @@ def torrent_action(idx, data, mode, ids):
for tid in ids: for tid in ids:
if "move_on_completed_path" in options: if "move_on_completed_path" in options:
client.core.set_torrent_move_completed_path(tid, options["move_on_completed_path"]) client.core.set_torrent_move_completed_path(tid, options["move_on_completed_path"])
if "move_on_completed" in options:
client.core.set_torrent_move_completed(tid, options["move_on_completed"])
if "is_auto_managed" in options: if "is_auto_managed" in options:
client.core.set_torrent_auto_managed(tid, options["is_auto_managed"]) client.core.set_torrent_auto_managed(tid, options["is_auto_managed"])
if "remove_at_ratio" in options: if "remove_at_ratio" in options:

View file

@ -310,6 +310,8 @@ class TorrentDetail(BaseMode, component.Component):
color_partially_selected = "magenta" color_partially_selected = "magenta"
color_highlighted = "white" color_highlighted = "white"
for fl in files: for fl in files:
#from sys import stderr
#print >> stderr, fl[6]
# kick out if we're going to draw too low on the screen # kick out if we're going to draw too low on the screen
if (off >= self.rows-1): if (off >= self.rows-1):
self.more_to_draw = True self.more_to_draw = True
@ -317,18 +319,34 @@ class TorrentDetail(BaseMode, component.Component):
self.file_limit = idx self.file_limit = idx
if idx >= self.file_off:
# set fg/bg colors based on if we are selected/marked or not
# default values # default color values
fg = "white" fg = "white"
bg = "black" bg = "black"
attr = ""
if fl[6] == -2: priority = -1 #Mixed
elif fl[6] == 0:
priority = 0 #Do Not Download
fg = "red"
elif fl[6] == 1:
priority = 1 #Normal
elif fl[6] <= 6:
priority = 2 #High
fg = "yellow"
elif fl[6] == 7:
priority = 3 #Highest
fg = "green"
if idx >= self.file_off:
# set fg/bg colors based on whether the file is selected/marked or not
if fl[1] in self.marked: if fl[1] in self.marked:
bg = color_selected bg = color_selected
if fl[3]: if fl[3]:
if self.marked[fl[1]] < self.__get_contained_files_count(file_list=fl[3]): if self.marked[fl[1]] < self.__get_contained_files_count(file_list=fl[3]):
bg = color_partially_selected bg = color_partially_selected
attr = "bold"
if idx == self.current_file_idx: if idx == self.current_file_idx:
self.current_file = fl self.current_file = fl
@ -339,9 +357,14 @@ class TorrentDetail(BaseMode, component.Component):
if self.marked[fl[1]] < self.__get_contained_files_count(file_list = fl[3]): if self.marked[fl[1]] < self.__get_contained_files_count(file_list = fl[3]):
fg = color_partially_selected fg = color_partially_selected
else: else:
fg = "black" if fg == "white":
fg = "black"
attr = "bold"
color_string = "{!%s,%s!}"%(fg,bg) if attr:
color_string = "{!%s,%s,%s!}"%(fg, bg, attr)
else:
color_string = "{!%s,%s!}"%(fg, bg)
#actually draw the dir/file string #actually draw the dir/file string
if fl[3] and fl[4]: # this is an expanded directory if fl[3] and fl[4]: # this is an expanded directory

View file

@ -213,9 +213,6 @@ class AddTorrentDialog(component.Component):
new_row = None new_row = None
for filename in filenames: for filename in filenames:
# Convert the path to unicode
filename = unicode(filename)
# Get the torrent data from the torrent file # Get the torrent data from the torrent file
try: try:
info = deluge.ui.common.TorrentInfo(filename) info = deluge.ui.common.TorrentInfo(filename)
@ -825,14 +822,15 @@ class AddTorrentDialog(component.Component):
self.save_torrent_options(row) self.save_torrent_options(row)
# The options we want all the torrents to have # The options, except file renames, we want all the torrents to have
options = self.options[model.get_value(row, 0)] options = self.options[model.get_value(row, 0)].copy()
del options["mapped_files"]
# Set all the torrent options # Set all the torrent options
row = model.get_iter_first() row = model.get_iter_first()
while row != None: while row != None:
torrent_id = model.get_value(row, 0) torrent_id = model.get_value(row, 0)
self.options[torrent_id] = options self.options[torrent_id].update(options)
row = model.iter_next(row) row = model.iter_next(row)
def _on_button_revert_clicked(self, widget): def _on_button_revert_clicked(self, widget):
@ -863,7 +861,7 @@ class AddTorrentDialog(component.Component):
def _on_filename_edited(self, renderer, path, new_text): def _on_filename_edited(self, renderer, path, new_text):
index = self.files_treestore[path][3] index = self.files_treestore[path][3]
new_text = new_text.strip(os.path.sep) new_text = new_text.strip(os.path.sep).strip()
# Return if the text hasn't changed # Return if the text hasn't changed
if new_text == self.files_treestore[path][1]: if new_text == self.files_treestore[path][1]:
@ -881,9 +879,14 @@ class AddTorrentDialog(component.Component):
if index > -1: if index > -1:
# We're renaming a file! Yay! That's easy! # We're renaming a file! Yay! That's easy!
if not new_text:
return
parent = self.files_treestore.iter_parent(itr) parent = self.files_treestore.iter_parent(itr)
file_path = os.path.join(self.get_file_path(parent), new_text) file_path = os.path.join(self.get_file_path(parent), new_text)
# Don't rename if filename exists
for row in self.files_treestore[parent].iterchildren():
if new_text == row[1]:
return
if os.path.sep in new_text: if os.path.sep in new_text:
# There are folders in this path, so we need to create them # There are folders in this path, so we need to create them
# and then move the file iter to top # and then move the file iter to top

View file

@ -111,7 +111,7 @@ def build_menu_radio_list(value_list, callback, pref_value=None,
if show_notset: if show_notset:
menuitem = gtk.RadioMenuItem(group, notset_label) menuitem = gtk.RadioMenuItem(group, notset_label)
menuitem.set_name(notset_label) menuitem.set_name("unlimited")
if pref_value < notset_lessthan and pref_value != None: if pref_value < notset_lessthan and pref_value != None:
menuitem.set_active(True) menuitem.set_active(True)
if show_activated and pref_value == 1: if show_activated and pref_value == 1:
@ -124,7 +124,7 @@ def build_menu_radio_list(value_list, callback, pref_value=None,
menuitem = gtk.SeparatorMenuItem() menuitem = gtk.SeparatorMenuItem()
menu.append(menuitem) menu.append(menuitem)
menuitem = gtk.MenuItem(_("Other...")) menuitem = gtk.MenuItem(_("Other..."))
menuitem.set_name(_("Other...")) menuitem.set_name("other")
menuitem.connect("activate", callback) menuitem.connect("activate", callback)
menu.append(menuitem) menu.append(menuitem)

View file

@ -174,7 +174,7 @@ class CreateTorrentDialog:
chooser.destroy() chooser.destroy()
return return
path = result.decode('utf-8').encode(sys.getfilesystemencoding()) path = result.decode('utf-8')
self.files_treestore.clear() self.files_treestore.clear()
self.files_treestore.append(None, [result, gtk.STOCK_FILE, deluge.common.get_path_size(path)]) self.files_treestore.append(None, [result, gtk.STOCK_FILE, deluge.common.get_path_size(path)])
@ -202,7 +202,7 @@ class CreateTorrentDialog:
chooser.destroy() chooser.destroy()
return return
path = result.decode('utf-8').encode(sys.getfilesystemencoding()) path = result.decode('utf-8')
self.files_treestore.clear() self.files_treestore.clear()
self.files_treestore.append(None, [result, gtk.STOCK_OPEN, deluge.common.get_path_size(path)]) self.files_treestore.append(None, [result, gtk.STOCK_OPEN, deluge.common.get_path_size(path)])

View file

@ -2465,6 +2465,19 @@ used sparingly.</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkCheckButton" id="chk_focus_main_window_on_add">
<property name="label" translatable="yes">Focus window when adding torrent</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child> <child>
<object class="GtkCheckButton" id="piecesbar_toggle"> <object class="GtkCheckButton" id="piecesbar_toggle">
<property name="visible">True</property> <property name="visible">True</property>
@ -2491,7 +2504,7 @@ status tab (&lt;b&gt;EXPERIMENTAL!!!&lt;/b&gt;)</property>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">1</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<child> <child>

View file

@ -61,6 +61,7 @@
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="image">menu-item-image1</property> <property name="image">menu-item-image1</property>
<property name="use_stock">False</property> <property name="use_stock">False</property>
<property name="always_show_image">True</property>
<signal name="activate" handler="on_menuitem_add_torrent_activate" swapped="no"/> <signal name="activate" handler="on_menuitem_add_torrent_activate" swapped="no"/>
</object> </object>
</child> </child>
@ -109,6 +110,7 @@
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="image">download-limit-image</property> <property name="image">download-limit-image</property>
<property name="use_stock">False</property> <property name="use_stock">False</property>
<property name="always_show_image">True</property>
</object> </object>
</child> </child>
<child> <child>
@ -120,6 +122,7 @@
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="image">upload-limit-image</property> <property name="image">upload-limit-image</property>
<property name="use_stock">False</property> <property name="use_stock">False</property>
<property name="always_show_image">True</property>
</object> </object>
</child> </child>
<child> <child>

View file

@ -149,6 +149,7 @@ DEFAULT_PREFS = {
"pieces_color_waiting": [4874, 56494, 0], "pieces_color_waiting": [4874, 56494, 0],
"pieces_color_downloading": [65535, 55255, 0], "pieces_color_downloading": [65535, 55255, 0],
"pieces_color_completed": [4883, 26985, 56540], "pieces_color_completed": [4883, 26985, 56540],
"focus_main_window_on_add": True,
} }
class GtkUI(object): class GtkUI(object):
@ -245,9 +246,8 @@ class GtkUI(object):
component.stop() component.stop()
# Process any pending gtk events since the mainloop has been quit # Process any pending gtk events since the mainloop has been quit
if not deluge.common.windows_check(): while gtk.events_pending():
while gtk.events_pending() and reactor.running: gtk.main_iteration(0)
reactor.doIteration(0)
# Shutdown all components # Shutdown all components
component.shutdown() component.shutdown()

View file

@ -59,8 +59,10 @@ log = logging.getLogger(__name__)
class IPCProtocolServer(Protocol): class IPCProtocolServer(Protocol):
def dataReceived(self, data): def dataReceived(self, data):
data = rencode.loads(data) config = ConfigManager("gtkui.conf")
component.get("MainWindow").present() data = rencode.loads(data, decode_utf8=True)
if not data or config["focus_main_window_on_add"]:
component.get("MainWindow").present()
process_args(data) process_args(data)
class IPCProtocolClient(Protocol): class IPCProtocolClient(Protocol):

View file

@ -93,6 +93,7 @@ class PluginManager(deluge.pluginmanagerbase.PluginManagerBase,
def _on_plugin_enabled_event(self, name): def _on_plugin_enabled_event(self, name):
self.enable_plugin(name) self.enable_plugin(name)
self.run_on_show_prefs()
def _on_plugin_disabled_event(self, name): def _on_plugin_disabled_event(self, name):
self.disable_plugin(name) self.disable_plugin(name)

View file

@ -121,7 +121,8 @@ class Preferences(component.Component):
self.accounts_frame = self.builder.get_object("AccountsFrame") self.accounts_frame = self.builder.get_object("AccountsFrame")
# Setup plugin tab listview # Setup plugin tab listview
self.plugin_liststore = gtk.ListStore(str, bool) # The third entry is for holding translated plugin names
self.plugin_liststore = gtk.ListStore(str, bool, str)
self.plugin_liststore.set_sort_column_id(0, gtk.SORT_ASCENDING) self.plugin_liststore.set_sort_column_id(0, gtk.SORT_ASCENDING)
self.plugin_listview = self.builder.get_object("plugin_listview") self.plugin_listview = self.builder.get_object("plugin_listview")
self.plugin_listview.set_model(self.plugin_liststore) self.plugin_listview.set_model(self.plugin_liststore)
@ -131,7 +132,7 @@ class Preferences(component.Component):
self.plugin_listview.append_column( self.plugin_listview.append_column(
gtk.TreeViewColumn(_("Enabled"), render, active=1)) gtk.TreeViewColumn(_("Enabled"), render, active=1))
self.plugin_listview.append_column( self.plugin_listview.append_column(
gtk.TreeViewColumn(_("Plugin"), gtk.CellRendererText(), text=0)) gtk.TreeViewColumn(_("Plugin"), gtk.CellRendererText(), text=2))
# Connect to the 'changed' event of TreeViewSelection to get selection # Connect to the 'changed' event of TreeViewSelection to get selection
# changes. # changes.
@ -552,6 +553,8 @@ class Preferences(component.Component):
self.gtkui_config["classic_mode"]) self.gtkui_config["classic_mode"])
self.builder.get_object("chk_show_rate_in_title").set_active( self.builder.get_object("chk_show_rate_in_title").set_active(
self.gtkui_config["show_rate_in_title"]) self.gtkui_config["show_rate_in_title"])
self.builder.get_object("chk_focus_main_window_on_add").set_active(
self.gtkui_config["focus_main_window_on_add"])
self.builder.get_object("piecesbar_toggle").set_active( self.builder.get_object("piecesbar_toggle").set_active(
self.gtkui_config["show_piecesbar"] self.gtkui_config["show_piecesbar"]
) )
@ -583,6 +586,7 @@ class Preferences(component.Component):
row = self.plugin_liststore.append() row = self.plugin_liststore.append()
self.plugin_liststore.set_value(row, 0, plugin) self.plugin_liststore.set_value(row, 0, plugin)
self.plugin_liststore.set_value(row, 1, enabled) self.plugin_liststore.set_value(row, 1, enabled)
self.plugin_liststore.set_value(row, 2, _(plugin))
# Now show the dialog # Now show the dialog
self.pref_dialog.show() self.pref_dialog.show()
@ -739,6 +743,8 @@ class Preferences(component.Component):
new_gtkui_config["show_rate_in_title"] = \ new_gtkui_config["show_rate_in_title"] = \
self.builder.get_object("chk_show_rate_in_title").get_active() self.builder.get_object("chk_show_rate_in_title").get_active()
new_gtkui_config["focus_main_window_on_add"] = \
self.builder.get_object("chk_focus_main_window_on_add").get_active()
## Other tab ## ## Other tab ##
new_gtkui_config["show_new_releases"] = \ new_gtkui_config["show_new_releases"] = \

View file

@ -173,7 +173,7 @@ class QueuedTorrents(component.Component):
def on_button_add_clicked(self, widget): def on_button_add_clicked(self, widget):
# Add all the torrents in the liststore # Add all the torrents in the liststore
def add_torrent(model, path, iter, data): def add_torrent(model, path, iter, data):
torrent_path = model.get_value(iter, 1) torrent_path = model.get_value(iter, 1).decode('utf-8')
process_args([torrent_path]) process_args([torrent_path])
self.liststore.foreach(add_torrent, None) self.liststore.foreach(add_torrent, None)

View file

@ -392,9 +392,9 @@ class StatusBar(component.Component):
def _on_set_download_speed(self, widget): def _on_set_download_speed(self, widget):
log.debug("_on_set_download_speed") log.debug("_on_set_download_speed")
if widget.get_name() == _("Unlimited"): if widget.get_name() == "unlimited":
value = -1 value = -1
elif widget.get_name() == _("Other..."): elif widget.get_name() == "other":
value = common.show_other_dialog( value = common.show_other_dialog(
_("Set Maximum Download Speed"), _("KiB/s"), None, "downloading.svg", self.max_download_speed) _("Set Maximum Download Speed"), _("KiB/s"), None, "downloading.svg", self.max_download_speed)
if value == None: if value == None:
@ -420,9 +420,9 @@ class StatusBar(component.Component):
def _on_set_upload_speed(self, widget): def _on_set_upload_speed(self, widget):
log.debug("_on_set_upload_speed") log.debug("_on_set_upload_speed")
if widget.get_name() == _("Unlimited"): if widget.get_name() == "unlimited":
value = -1 value = -1
elif widget.get_name() == _("Other..."): elif widget.get_name() == "other":
value = common.show_other_dialog( value = common.show_other_dialog(
_("Set Maximum Upload Speed"), _("KiB/s"), None, "seeding.svg", self.max_upload_speed) _("Set Maximum Upload Speed"), _("KiB/s"), None, "seeding.svg", self.max_upload_speed)
if value == None: if value == None:
@ -447,9 +447,9 @@ class StatusBar(component.Component):
def _on_set_connection_limit(self, widget): def _on_set_connection_limit(self, widget):
log.debug("_on_set_connection_limit") log.debug("_on_set_connection_limit")
if widget.get_name() == _("Unlimited"): if widget.get_name() == "unlimited":
value = -1 value = -1
elif widget.get_name() == _("Other..."): elif widget.get_name() == "other":
value = common.show_other_dialog( value = common.show_other_dialog(
_("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, self.max_connections) _("Set Maximum Connections"), "", gtk.STOCK_NETWORK, None, self.max_connections)
if value == None: if value == None:

View file

@ -138,10 +138,9 @@ class SystemTray(component.Component):
self.tray.connect("activate", self.on_tray_clicked) self.tray.connect("activate", self.on_tray_clicked)
self.tray.connect("popup-menu", self.on_tray_popup) self.tray.connect("popup-menu", self.on_tray_popup)
# For some reason these icons do not display in appindicator self.builder.get_object("download-limit-image").set_from_file(
self.builder.get_object("download-limit-image").set_from_file(
deluge.common.get_pixmap("downloading16.png")) deluge.common.get_pixmap("downloading16.png"))
self.builder.get_object("upload-limit-image").set_from_file( self.builder.get_object("upload-limit-image").set_from_file(
deluge.common.get_pixmap("seeding16.png")) deluge.common.get_pixmap("seeding16.png"))
client.register_event_handler("ConfigValueChangedEvent", self.config_value_changed) client.register_event_handler("ConfigValueChangedEvent", self.config_value_changed)
@ -157,21 +156,14 @@ class SystemTray(component.Component):
if self.config["enable_system_tray"]: if self.config["enable_system_tray"]:
if self.config["classic_mode"]: if self.config["classic_mode"]:
self.hide_widget_list.remove("menuitem_quitdaemon") try:
self.hide_widget_list.remove("separatormenuitem4") self.hide_widget_list.remove("menuitem_quitdaemon")
self.hide_widget_list.remove("separatormenuitem4")
except ValueError:
pass
self.builder.get_object("menuitem_quitdaemon").hide() self.builder.get_object("menuitem_quitdaemon").hide()
self.builder.get_object("separatormenuitem4").hide() self.builder.get_object("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.builder.get_object("menuitem_download_limit").hide()
self.builder.get_object("menuitem_upload_limit").hide()
self.builder.get_object("separatormenuitem3").hide()
# Show widgets in the hide list because we've connected to a host # Show widgets in the hide list because we've connected to a host
for widget in self.hide_widget_list: for widget in self.hide_widget_list:
self.builder.get_object(widget).show() self.builder.get_object(widget).show()
@ -266,16 +258,15 @@ class SystemTray(component.Component):
def build_tray_bwsetsubmenu(self): def build_tray_bwsetsubmenu(self):
# Create the Download speed list sub-menu # Create the Download speed list sub-menu
submenu_bwdownset = common.build_menu_radio_list( submenu_bwdownset = common.build_menu_radio_list(
self.config["tray_download_speed_list"], self.tray_setbwdown, self.config["tray_download_speed_list"], self.on_tray_setbwdown,
self.max_download_speed, self.max_download_speed,
_("KiB/s"), show_notset=True, show_other=True) _("KiB/s"), show_notset=True, show_other=True)
# Create the Upload speed list sub-menu # Create the Upload speed list sub-menu
submenu_bwupset = common.build_menu_radio_list( submenu_bwupset = common.build_menu_radio_list(
self.config["tray_upload_speed_list"], self.tray_setbwup, self.config["tray_upload_speed_list"], self.on_tray_setbwup,
self.max_upload_speed, self.max_upload_speed,
_("KiB/s"), show_notset=True, show_other=True) _("KiB/s"), show_notset=True, show_other=True)
# Add the sub-menus to the tray menu # Add the sub-menus to the tray menu
self.builder.get_object("menuitem_download_limit").set_submenu( self.builder.get_object("menuitem_download_limit").set_submenu(
submenu_bwdownset) submenu_bwdownset)
@ -286,10 +277,6 @@ class SystemTray(component.Component):
submenu_bwdownset.show_all() submenu_bwdownset.show_all()
submenu_bwupset.show_all() submenu_bwupset.show_all()
# 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): def disable(self,invert_app_ind_conf=False):
"""Disables the system tray icon or appindicator.""" """Disables the system tray icon or appindicator."""
try: try:
@ -360,6 +347,7 @@ class SystemTray(component.Component):
popup_function = gtk.status_icon_position_menu popup_function = gtk.status_icon_position_menu
if deluge.common.windows_check(): if deluge.common.windows_check():
popup_function = None popup_function = None
button = 0
self.tray_menu.popup(None, None, popup_function, self.tray_menu.popup(None, None, popup_function,
button, activate_time, status_icon) button, activate_time, status_icon)
@ -399,10 +387,22 @@ class SystemTray(component.Component):
self.window.quit(shutdown=True) self.window.quit(shutdown=True)
def tray_setbwdown(self, widget, data=None): def on_tray_setbwdown(self, widget, data=None):
if isinstance(widget, gtk.RadioMenuItem):
#ignore previous radiomenuitem value
if not widget.get_active():
return
self.setbwlimit(widget, _("Set Maximum Download Speed"), "max_download_speed", self.setbwlimit(widget, _("Set Maximum Download Speed"), "max_download_speed",
"tray_download_speed_list", self.max_download_speed, "downloading.svg") "tray_download_speed_list", self.max_download_speed, "downloading.svg")
def on_tray_setbwup(self, widget, data=None):
if isinstance(widget, gtk.RadioMenuItem):
#ignore previous radiomenuitem value
if not widget.get_active():
return
self.setbwlimit(widget, _("Set Maximum Upload Speed"), "max_upload_speed",
"tray_upload_speed_list", self.max_upload_speed, "seeding.svg")
def _on_window_hide(self, widget, data=None): def _on_window_hide(self, widget, data=None):
"""_on_window_hide - update the menuitem's status""" """_on_window_hide - update the menuitem's status"""
log.debug("_on_window_hide") log.debug("_on_window_hide")
@ -413,26 +413,21 @@ class SystemTray(component.Component):
log.debug("_on_window_show") log.debug("_on_window_show")
self.builder.get_object("menuitem_show_deluge").set_active(True) self.builder.get_object("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")
def setbwlimit(self, widget, string, core_key, ui_key, default, image): def setbwlimit(self, widget, string, core_key, ui_key, default, image):
"""Sets the bandwidth limit based on the user selection.""" """Sets the bandwidth limit based on the user selection."""
value = widget.get_children()[0].get_text().rstrip(" " + _("KiB/s")) value = widget.get_children()[0].get_text().split(" ")[0]
if value == _("Unlimited"): log.debug('setbwlimit: %s', value)
if widget.get_name() == "unlimited":
value = -1 value = -1
if widget.get_name() == "other":
if value == _("Other..."):
value = common.show_other_dialog(string, _("KiB/s"), None, image, default) value = common.show_other_dialog(string, _("KiB/s"), None, image, default)
if value == None: if value == None:
return return
elif value == 0:
value = -1
# Set the config in the core # Set the config in the core
client.core.set_config({core_key: value}) client.core.set_config({core_key: value})
self.build_tray_bwsetsubmenu()
def unlock_tray(self, is_showing_dlg=[False]): def unlock_tray(self, is_showing_dlg=[False]):
try: try:
from hashlib import sha1 as sha_hash from hashlib import sha1 as sha_hash

View file

@ -113,6 +113,15 @@ class TorrentDetails(component.Component):
("Options", True) ("Options", True)
] ]
self.translate_tabs = {
"All" : _("_All"),
"Status" : _("_Status"),
"Details" : _("_Details"),
"Files" : _("_Files"),
"Peers" : _("_Peers"),
"Options" : _("_Options")
}
# Get the state from saved file # Get the state from saved file
state = self.load_state() state = self.load_state()
@ -242,8 +251,8 @@ class TorrentDetails(component.Component):
def hide_tab(self, tab_name): def hide_tab(self, tab_name):
"""Hides tab by name""" """Hides tab by name"""
self.notebook.remove_page(self.tabs[tab_name].position)
self.tabs[tab_name].is_visible = False self.tabs[tab_name].is_visible = False
self.notebook.remove_page(self.tabs[tab_name].position)
self.regenerate_positions() self.regenerate_positions()
self.generate_menu() self.generate_menu()
@ -275,7 +284,8 @@ class TorrentDetails(component.Component):
"""Generates the checklist menu for all the tabs and attaches it""" """Generates the checklist menu for all the tabs and attaches it"""
menu = gtk.Menu() menu = gtk.Menu()
# Create 'All' menuitem and a separator # Create 'All' menuitem and a separator
menuitem = gtk.CheckMenuItem("All") menuitem = gtk.CheckMenuItem(self.translate_tabs["All"], True)
menuitem.set_name("All")
all_tabs = True all_tabs = True
for key in self.tabs: for key in self.tabs:
@ -297,7 +307,8 @@ class TorrentDetails(component.Component):
menuitem_list.sort() menuitem_list.sort()
for pos, name in menuitem_list: for pos, name in menuitem_list:
menuitem = gtk.CheckMenuItem(name) menuitem = gtk.CheckMenuItem(self.translate_tabs[name], True)
menuitem.set_name(name)
menuitem.set_active(self.tabs[name].is_visible) menuitem.set_active(self.tabs[name].is_visible)
menuitem.connect("toggled", self._on_menuitem_toggled) menuitem.connect("toggled", self._on_menuitem_toggled)
menu.append(menuitem) menu.append(menuitem)
@ -386,7 +397,7 @@ class TorrentDetails(component.Component):
def _on_menuitem_toggled(self, widget): def _on_menuitem_toggled(self, widget):
# Get the tab name # Get the tab name
name = widget.get_child().get_text() name = widget.get_name()
if name == "All": if name == "All":
if widget.get_active(): if widget.get_active():
self.show_all_tabs() self.show_all_tabs()

View file

@ -107,7 +107,9 @@ class _UI(object):
return self.__args return self.__args
def start(self): def start(self):
(self.__options, self.__args) = self.__parser.parse_args() # Make sure all arguments are unicode
argv = deluge.common.unicode_argv()[1:]
(self.__options, self.__args) = self.__parser.parse_args(argv)
if self.__options.quiet: if self.__options.quiet:
self.__options.loglevel = "none" self.__options.loglevel = "none"

View file

@ -1,4 +0,0 @@
* Add Window torrent options
- Add an options manager
* Preferences window options
- Add an options manager

View file

@ -244,10 +244,57 @@ dl.singleline dd {
} }
/* Files TreeGrid */ /* Files TreeGrid */
.x-treegrid-col { .x-treegrid-root-table {
overflow: hidden; border-right: 1px solid;
} }
.x-treegrid-root-node {
overflow: auto;
}
.x-treegrid-hd-hidden {
visibility: hidden;
border: 0;
width: 0;
}
.x-treegrid-col {
border-bottom: 1px solid;
height: 20px;
overflow: hidden;
vertical-align: top;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
white-space: nowrap;
}
.x-treegrid-text {
padding-left: 4px;
-moz-user-select: none;
-khtml-user-select: none;
}
.x-treegrid-resizer {
border-left:1px solid;
border-right:1px solid;
position:absolute;
left:0;
top:0;
}
.x-treegrid-header-inner {
overflow: hidden;
}
.x-treegrid-root-table,
.x-treegrid-col {
border-color: #ededed;
}
.x-treegrid-resizer {
border-left-color:#555;
border-right-color:#555;
}
/* Options Tab Styles */ /* Options Tab Styles */
.x-deluge-options-label { .x-deluge-options-label {

11
deluge/ui/web/gen_gettext.py Normal file → Executable file
View file

@ -2,15 +2,14 @@
""" """
Script to go through the javascript files and dynamically generate gettext.js Script to go through the javascript files and dynamically generate gettext.js
""" """
import os import os
import re import re
import glob
import cStringIO as StringIO
output_file = "gettext.js"
string_re = re.compile('_\\(\'(.*?)\'\\)') string_re = re.compile('_\\(\'(.*?)\'\\)')
strings = {} strings = {}
gettext_tpl = """## -*- coding: utf-8 -*- gettext_tpl = """## -*- coding: utf-8 -*-
/* /*
* Script: gettext.js * Script: gettext.js
@ -59,10 +58,10 @@ for root, dnames, files in os.walk('js/deluge-all'):
keys = strings.keys() keys = strings.keys()
keys.sort() keys.sort()
fp = StringIO.StringIO() fp = open(output_file, 'w')
fp.write(gettext_tpl) fp.write(gettext_tpl)
for key in keys: for key in keys:
fp.write('// %s\n' % ', '.join(map(lambda x: '%s:%s' % x, strings[key]))) fp.write('// %s\n' % ', '.join(map(lambda x: '%s:%s' % x, strings[key])))
fp.write("GetText.add('%(key)s', '${escape(_(\"%(key)s\"))}')\n\n" % locals()) fp.write("GetText.add('%(key)s', '${escape(_(\"%(key)s\"))}')\n\n" % locals())
fp.seek(0) fp.close()
print fp.read()

File diff suppressed because it is too large Load diff

View file

@ -1,2 +0,0 @@
images from the fruge icon set.
See LICENSE for a list of icons not taken from fruge.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 588 B

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.data.SortTypes.js * Deluge.data.SortTypes.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -39,7 +39,7 @@ Ext.namespace('Deluge.data');
* *
* @class Deluge.data.SortTypes * @class Deluge.data.SortTypes
* @singleton * @singleton
*/ */
Deluge.data.SortTypes = { Deluge.data.SortTypes = {
asIPAddress: function(value) { asIPAddress: function(value) {
var d = value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\:(\d+)/); var d = value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\:(\d+)/);
@ -48,6 +48,10 @@ Deluge.data.SortTypes = {
asQueuePosition: function(value) { asQueuePosition: function(value) {
return (value > -1) ? value : Number.MAX_VALUE; return (value > -1) ? value : Number.MAX_VALUE;
},
asName: function(value) {
return String(value).toLowerCase();
} }
} }
/*! /*!
@ -121,7 +125,7 @@ Deluge.data.Peer = Ext.data.Record.create([
]); ]);
/*! /*!
* Deluge.data.TorrentRecord.js * Deluge.data.TorrentRecord.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -168,7 +172,8 @@ Deluge.data.Torrent = Ext.data.Record.create([{
type: 'int' type: 'int'
}, { }, {
name: 'name', name: 'name',
type: 'string' type: 'string',
sortType: Deluge.data.SortTypes.asName
}, { }, {
name: 'total_size', name: 'total_size',
type: 'int' type: 'int'
@ -349,7 +354,7 @@ Deluge.details.DetailsTab = Ext.extend(Ext.Panel, {
title: _('Details'), title: _('Details'),
fields: {}, fields: {},
autoScroll: true,
queuedItems: {}, queuedItems: {},
oldData: {}, oldData: {},
@ -428,7 +433,7 @@ Deluge.details.DetailsTab = Ext.extend(Ext.Panel, {
}); });
/*! /*!
* Deluge.details.FilesTab.js * Deluge.details.FilesTab.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -457,12 +462,11 @@ Deluge.details.DetailsTab = Ext.extend(Ext.Panel, {
* this exception statement from your version. If you delete this exception * this exception statement from your version. If you delete this exception
* statement from all source files in the program, then also delete it here. * statement from all source files in the program, then also delete it here.
*/ */
Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, { Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
title: _('Files'), title: _('Files'),
autoScroll: true,
rootVisible: false, rootVisible: false,
columns: [{ columns: [{
@ -502,7 +506,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
} }
}) })
}], }],
selModel: new Ext.tree.MultiSelectionModel(), selModel: new Ext.tree.MultiSelectionModel(),
initComponent: function() { initComponent: function() {
@ -558,7 +562,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
this.clear(); this.clear();
this.torrentId = torrentId; this.torrentId = torrentId;
} }
deluge.client.web.get_torrent_files(torrentId, { deluge.client.web.get_torrent_files(torrentId, {
success: this.onRequestComplete, success: this.onRequestComplete,
scope: this, scope: this,
@ -591,7 +595,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
folderSort: true folderSort: true
}); });
}, },
onContextMenu: function(node, e) { onContextMenu: function(node, e) {
e.stopEvent(); e.stopEvent();
var selModel = this.getSelectionModel(); var selModel = this.getSelectionModel();
@ -601,7 +605,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
} }
deluge.menus.filePriorities.showAt(e.getPoint()); deluge.menus.filePriorities.showAt(e.getPoint());
}, },
onItemClick: function(baseItem, e) { onItemClick: function(baseItem, e) {
switch (baseItem.id) { switch (baseItem.id) {
case 'expandAll': case 'expandAll':
@ -628,7 +632,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
return; return;
} }
}); });
var priorities = new Array(Ext.keys(indexes).length); var priorities = new Array(Ext.keys(indexes).length);
for (var index in indexes) { for (var index in indexes) {
priorities[index] = indexes[index]; priorities[index] = indexes[index];
@ -645,7 +649,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
break; break;
} }
}, },
onRequestComplete: function(files, options) { onRequestComplete: function(files, options) {
if (!this.getRootNode().hasChildNodes()) { if (!this.getRootNode().hasChildNodes()) {
this.createFileTree(files); this.createFileTree(files);
@ -1544,6 +1548,7 @@ Deluge.add.AddWindow = Ext.extend(Deluge.add.Window, {
}, { }, {
text: _('Infohash'), text: _('Infohash'),
iconCls: 'icon-add-magnet', iconCls: 'icon-add-magnet',
hidden: true,
disabled: true disabled: true
}, '->', { }, '->', {
text: _('Remove'), text: _('Remove'),
@ -4392,7 +4397,7 @@ Deluge.preferences.ProxyField = Ext.extend(Ext.form.FieldSet, {
}); });
/*! /*!
* Deluge.preferences.ProxyPage.js * Deluge.preferences.ProxyPage.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -4432,11 +4437,12 @@ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, {
config = Ext.apply({ config = Ext.apply({
border: false, border: false,
title: _('Proxy'), title: _('Proxy'),
layout: 'form' layout: 'form',
autoScroll: true
}, config); }, config);
Deluge.preferences.Proxy.superclass.constructor.call(this, config); Deluge.preferences.Proxy.superclass.constructor.call(this, config);
}, },
initComponent: function() { initComponent: function() {
Deluge.preferences.Proxy.superclass.initComponent.call(this); Deluge.preferences.Proxy.superclass.initComponent.call(this);
this.peer = this.add(new Deluge.preferences.ProxyField({ this.peer = this.add(new Deluge.preferences.ProxyField({
@ -4444,28 +4450,28 @@ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, {
name: 'peer' name: 'peer'
})); }));
this.peer.on('change', this.onProxyChange, this); this.peer.on('change', this.onProxyChange, this);
this.web_seed = this.add(new Deluge.preferences.ProxyField({ this.web_seed = this.add(new Deluge.preferences.ProxyField({
title: _('Web Seed'), title: _('Web Seed'),
name: 'web_seed' name: 'web_seed'
})); }));
this.web_seed.on('change', this.onProxyChange, this); this.web_seed.on('change', this.onProxyChange, this);
this.tracker = this.add(new Deluge.preferences.ProxyField({ this.tracker = this.add(new Deluge.preferences.ProxyField({
title: _('Tracker'), title: _('Tracker'),
name: 'tracker' name: 'tracker'
})); }));
this.tracker.on('change', this.onProxyChange, this); this.tracker.on('change', this.onProxyChange, this);
this.dht = this.add(new Deluge.preferences.ProxyField({ this.dht = this.add(new Deluge.preferences.ProxyField({
title: _('DHT'), title: _('DHT'),
name: 'dht' name: 'dht'
})); }));
this.dht.on('change', this.onProxyChange, this); this.dht.on('change', this.onProxyChange, this);
deluge.preferences.getOptionsManager().bind('proxies', this); deluge.preferences.getOptionsManager().bind('proxies', this);
}, },
getValue: function() { getValue: function() {
return { return {
'dht': this.dht.getValue(), 'dht': this.dht.getValue(),
@ -4474,18 +4480,18 @@ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, {
'web_seed': this.web_seed.getValue() 'web_seed': this.web_seed.getValue()
} }
}, },
setValue: function(value) { setValue: function(value) {
for (var proxy in value) { for (var proxy in value) {
this[proxy].setValue(value[proxy]); this[proxy].setValue(value[proxy]);
} }
}, },
onProxyChange: function(field, newValue, oldValue) { onProxyChange: function(field, newValue, oldValue) {
var newValues = this.getValue(); var newValues = this.getValue();
var oldValues = Ext.apply({}, newValues); var oldValues = Ext.apply({}, newValues);
oldValues[field.getName()] = oldValue; oldValues[field.getName()] = oldValue;
this.fireEvent('change', this, newValues, oldValues); this.fireEvent('change', this, newValues, oldValues);
} }
}); });
@ -7039,7 +7045,7 @@ Ext.each(Deluge.Keys.Grid, function(key) {
}); });
/*! /*!
* Deluge.LoginWindow.js * Deluge.LoginWindow.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -7070,7 +7076,7 @@ Ext.each(Deluge.Keys.Grid, function(key) {
*/ */
Deluge.LoginWindow = Ext.extend(Ext.Window, { Deluge.LoginWindow = Ext.extend(Ext.Window, {
firstShow: true, firstShow: true,
bodyStyle: 'padding: 10px 5px;', bodyStyle: 'padding: 10px 5px;',
buttonAlign: 'center', buttonAlign: 'center',
@ -7084,36 +7090,39 @@ Deluge.LoginWindow = Ext.extend(Ext.Window, {
title: _('Login'), title: _('Login'),
width: 300, width: 300,
height: 120, height: 120,
initComponent: function() { initComponent: function() {
Deluge.LoginWindow.superclass.initComponent.call(this); Deluge.LoginWindow.superclass.initComponent.call(this);
this.on('show', this.onShow, this); this.on('show', this.onShow, this);
this.addButton({ this.addButton({
text: _('Login'), text: _('Login'),
handler: this.onLogin, handler: this.onLogin,
scope: this scope: this
}); });
this.form = this.add({ this.form = this.add({
xtype: 'form', xtype: 'form',
baseCls: 'x-plain', baseCls: 'x-plain',
labelWidth: 55, labelWidth: 120,
width: 300, labelAlign: 'right',
defaults: {width: 200}, defaults: {width: 110},
defaultType: 'textfield' defaultType: 'textfield'
}); });
this.passwordField = this.form.add({ this.passwordField = this.form.add({
xtype: 'textfield', xtype: 'textfield',
fieldLabel: _('Password'), fieldLabel: _('Password'),
grow: true,
growMin: '110',
growMax: '145',
id: '_password', id: '_password',
name: 'password', name: 'password',
inputType: 'password' inputType: 'password'
}); });
this.passwordField.on('specialkey', this.onSpecialKey, this); this.passwordField.on('specialkey', this.onSpecialKey, this);
}, },
logout: function() { logout: function() {
deluge.events.fire('logout'); deluge.events.fire('logout');
deluge.client.auth.delete_session({ deluge.client.auth.delete_session({
@ -7123,17 +7132,17 @@ Deluge.LoginWindow = Ext.extend(Ext.Window, {
scope: this scope: this
}); });
}, },
show: function(skipCheck) { show: function(skipCheck) {
if (this.firstShow) { if (this.firstShow) {
deluge.client.on('error', this.onClientError, this); deluge.client.on('error', this.onClientError, this);
this.firstShow = false; this.firstShow = false;
} }
if (skipCheck) { if (skipCheck) {
return Deluge.LoginWindow.superclass.show.call(this); return Deluge.LoginWindow.superclass.show.call(this);
} }
deluge.client.auth.check_session({ deluge.client.auth.check_session({
success: function(result) { success: function(result) {
if (result) { if (result) {
@ -7148,11 +7157,11 @@ Deluge.LoginWindow = Ext.extend(Ext.Window, {
scope: this scope: this
}); });
}, },
onSpecialKey: function(field, e) { onSpecialKey: function(field, e) {
if (e.getKey() == 13) this.onLogin(); if (e.getKey() == 13) this.onLogin();
}, },
onLogin: function() { onLogin: function() {
var passwordField = this.passwordField; var passwordField = this.passwordField;
deluge.client.auth.login(passwordField.getValue(), { deluge.client.auth.login(passwordField.getValue(), {
@ -7178,16 +7187,16 @@ Deluge.LoginWindow = Ext.extend(Ext.Window, {
scope: this scope: this
}); });
}, },
onClientError: function(errorObj, response, requestOptions) { onClientError: function(errorObj, response, requestOptions) {
if (errorObj.error.code == 1) { if (errorObj.error.code == 1) {
deluge.events.fire('logout'); deluge.events.fire('logout');
this.show(true); this.show(true);
} }
}, },
onShow: function() { onShow: function() {
this.passwordField.focus(true, 100); this.passwordField.focus(true, 300);
} }
}); });
/*! /*!
@ -9046,7 +9055,7 @@ Deluge.Toolbar = Ext.extend(Ext.Toolbar, {
idProperty: 'id', idProperty: 'id',
fields: [ fields: [
{name: 'queue', sortType: Deluge.data.SortTypes.asQueuePosition}, {name: 'queue', sortType: Deluge.data.SortTypes.asQueuePosition},
{name: 'name'}, {name: 'name', sortType: Deluge.data.SortTypes.asName},
{name: 'total_size', type: 'int'}, {name: 'total_size', type: 'int'},
{name: 'state'}, {name: 'state'},
{name: 'progress', type: 'float'}, {name: 'progress', type: 'float'},
@ -9055,7 +9064,7 @@ Deluge.Toolbar = Ext.extend(Ext.Toolbar, {
{name: 'num_peers', type: 'int'}, {name: 'num_peers', type: 'int'},
{name: 'total_peers', type: 'int'}, {name: 'total_peers', type: 'int'},
{name: 'download_payload_rate', type: 'int'}, {name: 'download_payload_rate', type: 'int'},
{name: 'upload_payload_speed', type: 'int'}, {name: 'upload_payload_rate', type: 'int'},
{name: 'eta', type: 'int', sortType: etaSorter}, {name: 'eta', type: 'int', sortType: etaSorter},
{name: 'ratio', type: 'float'}, {name: 'ratio', type: 'float'},
{name: 'distributed_copies', type: 'float'}, {name: 'distributed_copies', type: 'float'},

File diff suppressed because one or more lines are too long

View file

@ -59,15 +59,18 @@ Deluge.LoginWindow = Ext.extend(Ext.Window, {
this.form = this.add({ this.form = this.add({
xtype: 'form', xtype: 'form',
baseCls: 'x-plain', baseCls: 'x-plain',
labelWidth: 55, labelWidth: 120,
width: 300, labelAlign: 'right',
defaults: {width: 200}, defaults: {width: 110},
defaultType: 'textfield' defaultType: 'textfield'
}); });
this.passwordField = this.form.add({ this.passwordField = this.form.add({
xtype: 'textfield', xtype: 'textfield',
fieldLabel: _('Password'), fieldLabel: _('Password'),
grow: true,
growMin: '110',
growMax: '145',
id: '_password', id: '_password',
name: 'password', name: 'password',
inputType: 'password' inputType: 'password'

View file

@ -265,7 +265,7 @@
idProperty: 'id', idProperty: 'id',
fields: [ fields: [
{name: 'queue', sortType: Deluge.data.SortTypes.asQueuePosition}, {name: 'queue', sortType: Deluge.data.SortTypes.asQueuePosition},
{name: 'name'}, {name: 'name', sortType: Deluge.data.SortTypes.asName},
{name: 'total_size', type: 'int'}, {name: 'total_size', type: 'int'},
{name: 'state'}, {name: 'state'},
{name: 'progress', type: 'float'}, {name: 'progress', type: 'float'},

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.add.FilesTab.js * Deluge.add.FilesTab.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -40,7 +40,7 @@ Deluge.add.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
layout: 'fit', layout: 'fit',
title: _('Files'), title: _('Files'),
autoScroll: true, autoScroll: false,
animate: false, animate: false,
border: false, border: false,
disabled: true, disabled: true,

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.add.OptionsPanel.js * Deluge.add.OptionsPanel.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -67,7 +67,7 @@ Deluge.add.OptionsTab = Ext.extend(Ext.form.FormPanel, {
width: 400, width: 400,
labelSeparator: '' labelSeparator: ''
})); }));
var panel = this.add({ var panel = this.add({
border: false, border: false,
layout: 'column', layout: 'column',
@ -78,7 +78,6 @@ Deluge.add.OptionsTab = Ext.extend(Ext.form.FormPanel, {
border: false, border: false,
autoHeight: true, autoHeight: true,
defaultType: 'radio', defaultType: 'radio',
width: 100
}); });
this.optionsManager.bind('compact_allocation', fieldset.add({ this.optionsManager.bind('compact_allocation', fieldset.add({
@ -86,6 +85,7 @@ Deluge.add.OptionsTab = Ext.extend(Ext.form.FormPanel, {
columns: 1, columns: 1,
vertical: true, vertical: true,
labelSeparator: '', labelSeparator: '',
width: 80,
items: [{ items: [{
name: 'compact_allocation', name: 'compact_allocation',
value: false, value: false,
@ -107,35 +107,32 @@ Deluge.add.OptionsTab = Ext.extend(Ext.form.FormPanel, {
title: _('Bandwidth'), title: _('Bandwidth'),
border: false, border: false,
autoHeight: true, autoHeight: true,
labelWidth: 100, bodyStyle: 'margin-left: 7px',
labelWidth: 105,
width: 200, width: 200,
defaultType: 'spinnerfield' defaultType: 'spinnerfield'
}); });
this.optionsManager.bind('max_download_speed', fieldset.add({ this.optionsManager.bind('max_download_speed', fieldset.add({
fieldLabel: _('Max Down Speed'), fieldLabel: _('Max Down Speed'),
labelStyle: 'margin-left: 10px',
name: 'max_download_speed', name: 'max_download_speed',
width: 60 width: 60
})); }));
this.optionsManager.bind('max_upload_speed', fieldset.add({ this.optionsManager.bind('max_upload_speed', fieldset.add({
fieldLabel: _('Max Up Speed'), fieldLabel: _('Max Up Speed'),
labelStyle: 'margin-left: 10px',
name: 'max_upload_speed', name: 'max_upload_speed',
width: 60 width: 60
})); }));
this.optionsManager.bind('max_connections', fieldset.add({ this.optionsManager.bind('max_connections', fieldset.add({
fieldLabel: _('Max Connections'), fieldLabel: _('Max Connections'),
labelStyle: 'margin-left: 10px',
name: 'max_connections', name: 'max_connections',
width: 60 width: 60
})); }));
this.optionsManager.bind('max_upload_slots', fieldset.add({ this.optionsManager.bind('max_upload_slots', fieldset.add({
fieldLabel: _('Max Upload Slots'), fieldLabel: _('Max Upload Slots'),
labelStyle: 'margin-left: 10px',
name: 'max_upload_slots', name: 'max_upload_slots',
width: 60 width: 60
})); }));
fieldset = panel.add({ fieldset = panel.add({
title: _('General'), title: _('General'),
border: false, border: false,

View file

@ -81,7 +81,7 @@ Deluge.add.UrlWindow = Ext.extend(Deluge.add.Window, {
var cookies = this.cookieField.getValue(); var cookies = this.cookieField.getValue();
var torrentId = this.createTorrentId(); var torrentId = this.createTorrentId();
if (url.substring(0,20) == 'magnet:?xt=urn:btih:') { if (url.indexOf('magnet:?') == 0 && url.indexOf('xt=urn:btih') > -1) {
deluge.client.web.get_magnet_info(url, { deluge.client.web.get_magnet_info(url, {
success: this.onGotInfo, success: this.onGotInfo,
scope: this, scope: this,

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.data.SortTypes.js * Deluge.data.SortTypes.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -39,7 +39,7 @@ Ext.namespace('Deluge.data');
* *
* @class Deluge.data.SortTypes * @class Deluge.data.SortTypes
* @singleton * @singleton
*/ */
Deluge.data.SortTypes = { Deluge.data.SortTypes = {
asIPAddress: function(value) { asIPAddress: function(value) {
var d = value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\:(\d+)/); var d = value.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\:(\d+)/);
@ -48,5 +48,9 @@ Deluge.data.SortTypes = {
asQueuePosition: function(value) { asQueuePosition: function(value) {
return (value > -1) ? value : Number.MAX_VALUE; return (value > -1) ? value : Number.MAX_VALUE;
},
asName: function(value) {
return String(value).toLowerCase();
} }
} }

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.data.TorrentRecord.js * Deluge.data.TorrentRecord.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -47,7 +47,8 @@ Deluge.data.Torrent = Ext.data.Record.create([{
type: 'int' type: 'int'
}, { }, {
name: 'name', name: 'name',
type: 'string' type: 'string',
sortType: Deluge.data.SortTypes.asName
}, { }, {
name: 'total_size', name: 'total_size',
type: 'int' type: 'int'

View file

@ -36,7 +36,7 @@ Deluge.details.DetailsTab = Ext.extend(Ext.Panel, {
title: _('Details'), title: _('Details'),
fields: {}, fields: {},
autoScroll: true,
queuedItems: {}, queuedItems: {},
oldData: {}, oldData: {},

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.details.FilesTab.js * Deluge.details.FilesTab.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -29,12 +29,11 @@
* this exception statement from your version. If you delete this exception * this exception statement from your version. If you delete this exception
* statement from all source files in the program, then also delete it here. * statement from all source files in the program, then also delete it here.
*/ */
Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, { Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
title: _('Files'), title: _('Files'),
autoScroll: true,
rootVisible: false, rootVisible: false,
columns: [{ columns: [{
@ -74,7 +73,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
} }
}) })
}], }],
selModel: new Ext.tree.MultiSelectionModel(), selModel: new Ext.tree.MultiSelectionModel(),
initComponent: function() { initComponent: function() {
@ -130,7 +129,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
this.clear(); this.clear();
this.torrentId = torrentId; this.torrentId = torrentId;
} }
deluge.client.web.get_torrent_files(torrentId, { deluge.client.web.get_torrent_files(torrentId, {
success: this.onRequestComplete, success: this.onRequestComplete,
scope: this, scope: this,
@ -163,7 +162,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
folderSort: true folderSort: true
}); });
}, },
onContextMenu: function(node, e) { onContextMenu: function(node, e) {
e.stopEvent(); e.stopEvent();
var selModel = this.getSelectionModel(); var selModel = this.getSelectionModel();
@ -173,7 +172,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
} }
deluge.menus.filePriorities.showAt(e.getPoint()); deluge.menus.filePriorities.showAt(e.getPoint());
}, },
onItemClick: function(baseItem, e) { onItemClick: function(baseItem, e) {
switch (baseItem.id) { switch (baseItem.id) {
case 'expandAll': case 'expandAll':
@ -200,7 +199,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
return; return;
} }
}); });
var priorities = new Array(Ext.keys(indexes).length); var priorities = new Array(Ext.keys(indexes).length);
for (var index in indexes) { for (var index in indexes) {
priorities[index] = indexes[index]; priorities[index] = indexes[index];
@ -217,7 +216,7 @@ Deluge.details.FilesTab = Ext.extend(Ext.ux.tree.TreeGrid, {
break; break;
} }
}, },
onRequestComplete: function(files, options) { onRequestComplete: function(files, options) {
if (!this.getRootNode().hasChildNodes()) { if (!this.getRootNode().hasChildNodes()) {
this.createFileTree(files); this.createFileTree(files);

View file

@ -35,7 +35,7 @@
if (!value.replace(' ', '').replace(' ', '')){ if (!value.replace(' ', '').replace(' ', '')){
return ''; return '';
} }
return String.format('<img src="flag/{0}" />', value); return String.format('<img src="{0}flag/{1}" />', deluge.config.base, value);
} }
function peerAddressRenderer(value, p, record) { function peerAddressRenderer(value, p, record) {
var seed = (record.data['seed'] == 1024) ? 'x-deluge-seed' : 'x-deluge-peer'; var seed = (record.data['seed'] == 1024) ? 'x-deluge-seed' : 'x-deluge-peer';

View file

@ -1,6 +1,6 @@
/*! /*!
* Deluge.preferences.ProxyPage.js * Deluge.preferences.ProxyPage.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -40,11 +40,12 @@ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, {
config = Ext.apply({ config = Ext.apply({
border: false, border: false,
title: _('Proxy'), title: _('Proxy'),
layout: 'form' layout: 'form',
autoScroll: true
}, config); }, config);
Deluge.preferences.Proxy.superclass.constructor.call(this, config); Deluge.preferences.Proxy.superclass.constructor.call(this, config);
}, },
initComponent: function() { initComponent: function() {
Deluge.preferences.Proxy.superclass.initComponent.call(this); Deluge.preferences.Proxy.superclass.initComponent.call(this);
this.peer = this.add(new Deluge.preferences.ProxyField({ this.peer = this.add(new Deluge.preferences.ProxyField({
@ -52,28 +53,28 @@ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, {
name: 'peer' name: 'peer'
})); }));
this.peer.on('change', this.onProxyChange, this); this.peer.on('change', this.onProxyChange, this);
this.web_seed = this.add(new Deluge.preferences.ProxyField({ this.web_seed = this.add(new Deluge.preferences.ProxyField({
title: _('Web Seed'), title: _('Web Seed'),
name: 'web_seed' name: 'web_seed'
})); }));
this.web_seed.on('change', this.onProxyChange, this); this.web_seed.on('change', this.onProxyChange, this);
this.tracker = this.add(new Deluge.preferences.ProxyField({ this.tracker = this.add(new Deluge.preferences.ProxyField({
title: _('Tracker'), title: _('Tracker'),
name: 'tracker' name: 'tracker'
})); }));
this.tracker.on('change', this.onProxyChange, this); this.tracker.on('change', this.onProxyChange, this);
this.dht = this.add(new Deluge.preferences.ProxyField({ this.dht = this.add(new Deluge.preferences.ProxyField({
title: _('DHT'), title: _('DHT'),
name: 'dht' name: 'dht'
})); }));
this.dht.on('change', this.onProxyChange, this); this.dht.on('change', this.onProxyChange, this);
deluge.preferences.getOptionsManager().bind('proxies', this); deluge.preferences.getOptionsManager().bind('proxies', this);
}, },
getValue: function() { getValue: function() {
return { return {
'dht': this.dht.getValue(), 'dht': this.dht.getValue(),
@ -82,18 +83,18 @@ Deluge.preferences.Proxy = Ext.extend(Ext.form.FormPanel, {
'web_seed': this.web_seed.getValue() 'web_seed': this.web_seed.getValue()
} }
}, },
setValue: function(value) { setValue: function(value) {
for (var proxy in value) { for (var proxy in value) {
this[proxy].setValue(value[proxy]); this[proxy].setValue(value[proxy]);
} }
}, },
onProxyChange: function(field, newValue, oldValue) { onProxyChange: function(field, newValue, oldValue) {
var newValues = this.getValue(); var newValues = this.getValue();
var oldValues = Ext.apply({}, newValues); var oldValues = Ext.apply({}, newValues);
oldValues[field.getName()] = oldValue; oldValues[field.getName()] = oldValue;
this.fireEvent('change', this, newValues, oldValues); this.fireEvent('change', this, newValues, oldValues);
} }
}); });

View file

@ -1543,7 +1543,7 @@ Ext.override(Ext.ux.form.SpinnerField, {
}); });
/*! /*!
* Ext.ux.form.SpinnerGroup.js * Ext.ux.form.SpinnerGroup.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -1581,6 +1581,7 @@ Ext.ux.form.SpinnerGroup = Ext.extend(Ext.form.CheckboxGroup, {
// private // private
defaultType: 'spinnerfield', defaultType: 'spinnerfield',
anchor: '98%',
// private // private
groupCls: 'x-form-spinner-group', groupCls: 'x-form-spinner-group',
@ -1697,6 +1698,7 @@ Ext.ux.form.SpinnerGroup = Ext.extend(Ext.form.CheckboxGroup, {
this.items.each(function(field) { this.items.each(function(field) {
field.on('spin', this.onFieldChange, this); field.on('spin', this.onFieldChange, this);
field.on('change', this.onFieldChange, this);
}, this); }, this);
if (this.lazyValueSet) { if (this.lazyValueSet) {

View file

@ -191,7 +191,7 @@ Ext.ns("Ext.ux.form");Ext.ux.form.SpinnerField=Ext.extend(Ext.form.NumberField,{
Ext.override(Ext.ux.form.SpinnerField,{onBlur:Ext.form.Field.prototype.onBlur}); Ext.override(Ext.ux.form.SpinnerField,{onBlur:Ext.form.Field.prototype.onBlur});
/* /*
* Ext.ux.form.SpinnerGroup.js * Ext.ux.form.SpinnerGroup.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -220,7 +220,7 @@ Ext.override(Ext.ux.form.SpinnerField,{onBlur:Ext.form.Field.prototype.onBlur});
* this exception statement from your version. If you delete this exception * this exception statement from your version. If you delete this exception
* statement from all source files in the program, then also delete it here. * statement from all source files in the program, then also delete it here.
*/ */
Ext.ns("Ext.ux.form");Ext.ux.form.SpinnerGroup=Ext.extend(Ext.form.CheckboxGroup,{defaultType:"spinnerfield",groupCls:"x-form-spinner-group",colCfg:{},onRender:function(h,f){if(!this.el){var o={cls:this.groupCls,layout:"column",border:false,renderTo:h};var a=Ext.apply({defaultType:this.defaultType,layout:"form",border:false,labelWidth:60,defaults:{hideLabel:true,anchor:"60%"}},this.colCfg);if(this.items[0].items){Ext.apply(o,{layoutConfig:{columns:this.items.length},defaults:this.defaults,items:this.items});for(var e=0,k=this.items.length;e<k;e++){Ext.applyIf(this.items[e],a)}}else{var d,m=[];if(typeof this.columns=="string"){this.columns=this.items.length}if(!Ext.isArray(this.columns)){var j=[];for(var e=0;e<this.columns;e++){j.push((100/this.columns)*0.01)}this.columns=j}d=this.columns.length;for(var e=0;e<d;e++){var b=Ext.apply({items:[]},a);b[this.columns[e]<=1?"columnWidth":"width"]=this.columns[e];if(this.defaults){b.defaults=Ext.apply(b.defaults||{},this.defaults)}m.push(b)}if(this.vertical){var q=Math.ceil(this.items.length/d),n=0;for(var e=0,k=this.items.length;e<k;e++){if(e>0&&e%q==0){n++}if(this.items[e].fieldLabel){this.items[e].hideLabel=false}m[n].items.push(this.items[e])}}else{for(var e=0,k=this.items.length;e<k;e++){var p=e%d;if(this.items[e].fieldLabel){this.items[e].hideLabel=false}m[p].items.push(this.items[e])}}Ext.apply(o,{layoutConfig:{columns:d},items:m})}this.panel=new Ext.Panel(o);this.el=this.panel.getEl();if(this.forId&&this.itemCls){var c=this.el.up(this.itemCls).child("label",true);if(c){c.setAttribute("htmlFor",this.forId)}}var g=this.panel.findBy(function(l){return l.isFormField},this);this.items=new Ext.util.MixedCollection();this.items.addAll(g);this.items.each(function(l){l.on("spin",this.onFieldChange,this)},this);if(this.lazyValueSet){this.setValue(this.value);delete this.value;delete this.lazyValueSet}if(this.lazyRawValueSet){this.setRawValue(this.rawValue);delete this.rawValue;delete this.lazyRawValueSet}}Ext.ux.form.SpinnerGroup.superclass.onRender.call(this,h,f)},onFieldChange:function(a){this.fireEvent("change",this,this.getValue())},initValue:Ext.emptyFn,getValue:function(){var a=[this.items.getCount()];this.items.each(function(c,b){a[b]=Number(c.getValue())});return a},getRawValue:function(){var a=[this.items.getCount()];this.items.each(function(c,b){a[b]=Number(c.getRawValue())});return a},setValue:function(a){if(!this.rendered){this.value=a;this.lazyValueSet=true}else{this.items.each(function(c,b){c.setValue(a[b])})}},setRawValue:function(a){if(!this.rendered){this.rawValue=a;this.lazyRawValueSet=true}else{this.items.each(function(c,b){c.setRawValue(a[b])})}}});Ext.reg("spinnergroup",Ext.ux.form.SpinnerGroup); Ext.ns("Ext.ux.form");Ext.ux.form.SpinnerGroup=Ext.extend(Ext.form.CheckboxGroup,{defaultType:"spinnerfield",anchor:"98%",groupCls:"x-form-spinner-group",colCfg:{},onRender:function(h,f){if(!this.el){var o={cls:this.groupCls,layout:"column",border:false,renderTo:h};var a=Ext.apply({defaultType:this.defaultType,layout:"form",border:false,labelWidth:60,defaults:{hideLabel:true,anchor:"60%"}},this.colCfg);if(this.items[0].items){Ext.apply(o,{layoutConfig:{columns:this.items.length},defaults:this.defaults,items:this.items});for(var e=0,k=this.items.length;e<k;e++){Ext.applyIf(this.items[e],a)}}else{var d,m=[];if(typeof this.columns=="string"){this.columns=this.items.length}if(!Ext.isArray(this.columns)){var j=[];for(var e=0;e<this.columns;e++){j.push((100/this.columns)*0.01)}this.columns=j}d=this.columns.length;for(var e=0;e<d;e++){var b=Ext.apply({items:[]},a);b[this.columns[e]<=1?"columnWidth":"width"]=this.columns[e];if(this.defaults){b.defaults=Ext.apply(b.defaults||{},this.defaults)}m.push(b)}if(this.vertical){var q=Math.ceil(this.items.length/d),n=0;for(var e=0,k=this.items.length;e<k;e++){if(e>0&&e%q==0){n++}if(this.items[e].fieldLabel){this.items[e].hideLabel=false}m[n].items.push(this.items[e])}}else{for(var e=0,k=this.items.length;e<k;e++){var p=e%d;if(this.items[e].fieldLabel){this.items[e].hideLabel=false}m[p].items.push(this.items[e])}}Ext.apply(o,{layoutConfig:{columns:d},items:m})}this.panel=new Ext.Panel(o);this.el=this.panel.getEl();if(this.forId&&this.itemCls){var c=this.el.up(this.itemCls).child("label",true);if(c){c.setAttribute("htmlFor",this.forId)}}var g=this.panel.findBy(function(l){return l.isFormField},this);this.items=new Ext.util.MixedCollection();this.items.addAll(g);this.items.each(function(l){l.on("spin",this.onFieldChange,this);l.on("change",this.onFieldChange,this)},this);if(this.lazyValueSet){this.setValue(this.value);delete this.value;delete this.lazyValueSet}if(this.lazyRawValueSet){this.setRawValue(this.rawValue);delete this.rawValue;delete this.lazyRawValueSet}}Ext.ux.form.SpinnerGroup.superclass.onRender.call(this,h,f)},onFieldChange:function(a){this.fireEvent("change",this,this.getValue())},initValue:Ext.emptyFn,getValue:function(){var a=[this.items.getCount()];this.items.each(function(c,b){a[b]=Number(c.getValue())});return a},getRawValue:function(){var a=[this.items.getCount()];this.items.each(function(c,b){a[b]=Number(c.getRawValue())});return a},setValue:function(a){if(!this.rendered){this.value=a;this.lazyValueSet=true}else{this.items.each(function(c,b){c.setValue(a[b])})}},setRawValue:function(a){if(!this.rendered){this.rawValue=a;this.lazyRawValueSet=true}else{this.items.each(function(c,b){c.setRawValue(a[b])})}}});Ext.reg("spinnergroup",Ext.ux.form.SpinnerGroup);
/* /*
* Ext.ux.form.ToggleField.js * Ext.ux.form.ToggleField.js
* *

View file

@ -1,6 +1,6 @@
/*! /*!
* Ext.ux.form.SpinnerGroup.js * Ext.ux.form.SpinnerGroup.js
* *
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com> * Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
* *
* This program is free software; you can redistribute it and/or modify * This program is free software; you can redistribute it and/or modify
@ -38,6 +38,7 @@ Ext.ux.form.SpinnerGroup = Ext.extend(Ext.form.CheckboxGroup, {
// private // private
defaultType: 'spinnerfield', defaultType: 'spinnerfield',
anchor: '98%',
// private // private
groupCls: 'x-form-spinner-group', groupCls: 'x-form-spinner-group',
@ -154,6 +155,7 @@ Ext.ux.form.SpinnerGroup = Ext.extend(Ext.form.CheckboxGroup, {
this.items.each(function(field) { this.items.each(function(field) {
field.on('spin', this.onFieldChange, this); field.on('spin', this.onFieldChange, this);
field.on('change', this.onFieldChange, this);
}, this); }, this);
if (this.lazyValueSet) { if (this.lazyValueSet) {

View file

@ -722,24 +722,29 @@ class WebApi(JSONComponent):
:rtype: dictionary :rtype: dictionary
""" """
try: magnet_scheme = 'magnet:?'
s = uri.split("&")[0][20:] xt_param = 'xt=urn:btih:'
if len(s) == 32: dn_param = 'dn='
info_hash = base64.b32decode(s).encode("hex") if uri.startswith(magnet_scheme):
elif len(s) == 40:
info_hash = s
else:
return False
name = None name = None
for i in uri.split("&")[1:]: info_hash = None
if i[:3] == "dn=": for param in uri[len(magnet_scheme):].split('&'):
name = unquote_plus(i.split("=")[1]) if param.startswith(xt_param):
if not name: xt_hash = param[len(xt_param):]
name = info_hash if len(xt_hash) == 32:
return {"name":name, "info_hash":info_hash, "files_tree":''} info_hash = base64.b32decode(xt_hash).encode("hex")
except Exception, e: elif len(xt_hash) == 40:
log.exception(e) info_hash = xt_hash
return False else:
break
elif param.startswith(dn_param):
name = unquote_plus(param[len(dn_param):])
if info_hash:
if not name:
name = info_hash
return {"name":name, "info_hash":info_hash, "files_tree":''}
return False
@export @export
def add_torrents(self, torrents): def add_torrents(self, torrents):
@ -754,7 +759,7 @@ class WebApi(JSONComponent):
>>> json_api.web.add_torrents([{ >>> json_api.web.add_torrents([{
"path": "/tmp/deluge-web/some-torrent-file.torrent", "path": "/tmp/deluge-web/some-torrent-file.torrent",
"options": {"download_path": "/home/deluge/"} "options": {"download_location": "/home/deluge/"}
}]) }])
""" """

Binary file not shown.

Before

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 995 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 992 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 829 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 854 B

Some files were not shown because too many files have changed in this diff Show more