]> granicus.if.org Git - transmission/commitdiff
#6089: Beautified JavaScript (patch by skybon)
authorMike Gelfand <mikedld@mikedld.com>
Thu, 10 Mar 2016 19:05:13 +0000 (19:05 +0000)
committerMike Gelfand <mikedld@mikedld.com>
Thu, 10 Mar 2016 19:05:13 +0000 (19:05 +0000)
12 files changed:
.jsbeautifyrc [new file with mode: 0644]
web/javascript/common.js
web/javascript/dialog.js
web/javascript/file-row.js
web/javascript/formatter.js
web/javascript/inspector.js
web/javascript/notifications.js
web/javascript/prefs-dialog.js
web/javascript/remote.js
web/javascript/torrent-row.js
web/javascript/torrent.js
web/javascript/transmission.js

diff --git a/.jsbeautifyrc b/.jsbeautifyrc
new file mode 100644 (file)
index 0000000..ea29661
--- /dev/null
@@ -0,0 +1,9 @@
+{
+    "indent_size": 4,
+    "indent_char": " ",
+    "indent_level": 0,
+    "indent_with_tabs": false,
+    "preserve_newlines": true,
+    "max_preserve_newlines": 2,
+    "jslint_happy": true
+}
index 7162d3f828c549ff739b50aa5d6ed9bdf5d7614f..fd7913dc3943a351e49b5ed48d1a44c0de9d1950 100644 (file)
@@ -10,91 +10,110 @@ var transmission,
     isMobileDevice = RegExp("(iPhone|iPod|Android)").test(navigator.userAgent),
     scroll_timeout;
 
-if (!Array.indexOf){
-       Array.prototype.indexOf = function(obj){
-               var i, len;
-               for (i=0, len=this.length; i<len; i++)
-                       if (this[i]==obj)
-                               return i;
-               return -1;
-       }
-}
+if (!Array.indexOf) {
+    Array.prototype.indexOf = function (obj) {
+        var i, len;
+        for (i = 0, len = this.length; i < len; i++) {
+            if (this[i] == obj) {
+                return i;
+            };
+        };
+        return -1;
+    };
+};
 
 // http://forum.jquery.com/topic/combining-ui-dialog-and-tabs
 $.fn.tabbedDialog = function (dialog_opts) {
-       this.tabs({selected: 0});
-       this.dialog(dialog_opts);
-       this.find('.ui-tab-dialog-close').append(this.parent().find('.ui-dialog-titlebar-close'));
-       this.find('.ui-tab-dialog-close').css({'position':'absolute','right':'0', 'top':'16px'});
-       this.find('.ui-tab-dialog-close > a').css({'float':'none','padding':'0'});
-       var tabul = this.find('ul:first');
-       this.parent().addClass('ui-tabs').prepend(tabul).draggable('option','handle',tabul);
-       this.siblings('.ui-dialog-titlebar').remove();
-       tabul.addClass('ui-dialog-titlebar');
+    this.tabs({
+        selected: 0
+    });
+    this.dialog(dialog_opts);
+    this.find('.ui-tab-dialog-close').append(this.parent().find('.ui-dialog-titlebar-close'));
+    this.find('.ui-tab-dialog-close').css({
+        'position': 'absolute',
+        'right': '0',
+        'top': '16px'
+    });
+    this.find('.ui-tab-dialog-close > a').css({
+        'float': 'none',
+        'padding': '0'
+    });
+    var tabul = this.find('ul:first');
+    this.parent().addClass('ui-tabs').prepend(tabul).draggable('option', 'handle', tabul);
+    this.siblings('.ui-dialog-titlebar').remove();
+    tabul.addClass('ui-dialog-titlebar');
 }
 
-$(document).ready(function() {
-
-       // IE8 and below don’t support ES5 Date.now()
-       if (!Date.now) {
-               Date.now = function() {
-                       return +new Date();
-               };
-       }
-
-       // IE specific fixes here
-       if ($.browser.msie) {
-               try {
-                       document.execCommand("BackgroundImageCache", false, true);
-               } catch(err) {}
-               $('.dialog_container').css('height',$(window).height()+'px');
-       }
-
-       if ($.browser.safari) {
-               // Move search field's margin down for the styled input
-               $('#torrent_search').css('margin-top', 3);
-       }
-       if (isMobileDevice){
-               window.onload = function(){ setTimeout(function() { window.scrollTo(0,1); },500); };
-               window.onorientationchange = function(){ setTimeout(function() { window.scrollTo(0,1); },100); };
-               if (window.navigator.standalone)
-                       // Fix min height for isMobileDevice when run in full screen mode from home screen
-                       // so the footer appears in the right place
-                       $('body div#torrent_container').css('min-height', '338px');
-               $("label[for=torrent_upload_url]").text("URL: ");
-       } else {
-               // Fix for non-Safari-3 browsers: dark borders to replace shadows.
-               $('div.dialog_container div.dialog_window').css('border', '1px solid #777');
-       }
-
-       // Initialise the dialog controller
-       dialog = new Dialog();
-
-       // Initialise the main Transmission controller
-       transmission = new Transmission();
+$(document).ready(function () {
+
+    // IE8 and below don’t support ES5 Date.now()
+    if (!Date.now) {
+        Date.now = function () {
+            return +new Date();
+        };
+    };
+
+    // IE specific fixes here
+    if ($.browser.msie) {
+        try {
+            document.execCommand("BackgroundImageCache", false, true);
+        } catch (err) {};
+        $('.dialog_container').css('height', $(window).height() + 'px');
+    };
+
+    if ($.browser.safari) {
+        // Move search field's margin down for the styled input
+        $('#torrent_search').css('margin-top', 3);
+    };
+
+    if (isMobileDevice) {
+        window.onload = function () {
+            setTimeout(function () {
+                window.scrollTo(0, 1);
+            }, 500);
+        };
+        window.onorientationchange = function () {
+            setTimeout(function () {
+                window.scrollTo(0, 1);
+            }, 100);
+        };
+        if (window.navigator.standalone) {
+            // Fix min height for isMobileDevice when run in full screen mode from home screen
+            // so the footer appears in the right place
+            $('body div#torrent_container').css('min-height', '338px');
+        };
+        $("label[for=torrent_upload_url]").text("URL: ");
+    } else {
+        // Fix for non-Safari-3 browsers: dark borders to replace shadows.
+        $('div.dialog_container div.dialog_window').css('border', '1px solid #777');
+    };
+
+    // Initialise the dialog controller
+    dialog = new Dialog();
+
+    // Initialise the main Transmission controller
+    transmission = new Transmission();
 });
 
 /**
  * Checks to see if the content actually changed before poking the DOM.
  */
-function setInnerHTML(e, html)
-{
-       if (!e)
-               return;
-
-       /* innerHTML is listed as a string, but the browser seems to change it.
-        * For example, "&infin;" gets changed to "∞" somewhere down the line.
-        * So, let's use an arbitrary  different field to test our state... */
-       if (e.currentHTML != html)
-       {
-               e.currentHTML = html;
-               e.innerHTML = html;
-       }
+function setInnerHTML(e, html) {
+    if (!e) {
+        return;
+    };
+
+    /* innerHTML is listed as a string, but the browser seems to change it.
+     * For example, "&infin;" gets changed to "∞" somewhere down the line.
+     * So, let's use an arbitrary  different field to test our state... */
+    if (e.currentHTML != html) {
+        e.currentHTML = html;
+        e.innerHTML = html;
+    };
 };
 
-function sanitizeText(text)
-{
-       return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+function sanitizeText(text) {
+    return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
 };
 
 /**
@@ -102,99 +121,100 @@ function sanitizeText(text)
  * on torrents whose state hasn't changed since the last update,
  * so see if the text actually changed before poking the DOM.
  */
-function setTextContent(e, text)
-{
-       if (e && (e.textContent != text))
-               e.textContent = text;
+function setTextContent(e, text) {
+    if (e && (e.textContent != text)) {
+        e.textContent = text;
+    };
 };
 
 /*
  *   Given a numerator and denominator, return a ratio string
  */
-Math.ratio = function(numerator, denominator) {
-       var result = Math.floor(100 * numerator / denominator) / 100;
+Math.ratio = function (numerator, denominator) {
+    var result = Math.floor(100 * numerator / denominator) / 100;
 
-       // check for special cases
-       if (result==Number.POSITIVE_INFINITY || result==Number.NEGATIVE_INFINITY) result = -2;
-       else if (isNaN(result)) result = -1;
+    // check for special cases
+    if (result == Number.POSITIVE_INFINITY || result == Number.NEGATIVE_INFINITY) {
+        result = -2;
+    } else if (isNaN(result)) {
+        result = -1;
+    };
 
-       return result;
+    return result;
 };
 
 /**
  * Round a string of a number to a specified number of decimal places
  */
-Number.prototype.toTruncFixed = function(place) {
-       var ret = Math.floor(this * Math.pow (10, place)) / Math.pow(10, place);
-       return ret.toFixed(place);
-}
+Number.prototype.toTruncFixed = function (place) {
+    var ret = Math.floor(this * Math.pow(10, place)) / Math.pow(10, place);
+    return ret.toFixed(place);
+};
 
-Number.prototype.toStringWithCommas = function() {
+Number.prototype.toStringWithCommas = function () {
     return this.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ",");
-}
-
+};
 
 /*
  * Trim whitespace from a string
  */
 String.prototype.trim = function () {
-       return this.replace(/^\s*/, "").replace(/\s*$/, "");
-}
+    return this.replace(/^\s*/, "").replace(/\s*$/, "");
+};
 
 /***
-****  Preferences
-***/
-
-function Prefs() { }
-Prefs.prototype = { };
-
-Prefs._RefreshRate        = 'refresh_rate';
-
-Prefs._FilterMode         = 'filter';
-Prefs._FilterAll          = 'all';
-Prefs._FilterActive       = 'active';
-Prefs._FilterSeeding      = 'seeding';
-Prefs._FilterDownloading  = 'downloading';
-Prefs._FilterPaused       = 'paused';
-Prefs._FilterFinished     = 'finished';
-
-Prefs._SortDirection      = 'sort_direction';
-Prefs._SortAscending      = 'ascending';
-Prefs._SortDescending     = 'descending';
-
-Prefs._SortMethod         = 'sort_method';
-Prefs._SortByAge          = 'age';
-Prefs._SortByActivity     = 'activity';
-Prefs._SortByName         = 'name';
-Prefs._SortByQueue        = 'queue_order';
-Prefs._SortBySize         = 'size';
-Prefs._SortByProgress     = 'percent_completed';
-Prefs._SortByRatio        = 'ratio';
-Prefs._SortByState        = 'state';
-
-Prefs._CompactDisplayState= 'compact_display_state';
-
-Prefs._Defaults =
-{
-       'filter': 'all',
-       'refresh_rate' : 5,
-       'sort_direction': 'ascending',
-       'sort_method': 'name',
-       'turtle-state' : false,
-       'compact_display_state' : false
+ ****  Preferences
+ ***/
+
+function Prefs() {};
+Prefs.prototype = {};
+
+Prefs._RefreshRate = 'refresh_rate';
+
+Prefs._FilterMode = 'filter';
+Prefs._FilterAll = 'all';
+Prefs._FilterActive = 'active';
+Prefs._FilterSeeding = 'seeding';
+Prefs._FilterDownloading = 'downloading';
+Prefs._FilterPaused = 'paused';
+Prefs._FilterFinished = 'finished';
+
+Prefs._SortDirection = 'sort_direction';
+Prefs._SortAscending = 'ascending';
+Prefs._SortDescending = 'descending';
+
+Prefs._SortMethod = 'sort_method';
+Prefs._SortByAge = 'age';
+Prefs._SortByActivity = 'activity';
+Prefs._SortByName = 'name';
+Prefs._SortByQueue = 'queue_order';
+Prefs._SortBySize = 'size';
+Prefs._SortByProgress = 'percent_completed';
+Prefs._SortByRatio = 'ratio';
+Prefs._SortByState = 'state';
+
+Prefs._CompactDisplayState = 'compact_display_state';
+
+Prefs._Defaults = {
+    'filter': 'all',
+    'refresh_rate': 5,
+    'sort_direction': 'ascending',
+    'sort_method': 'name',
+    'turtle-state': false,
+    'compact_display_state': false
 };
 
 /*
  * Set a preference option
  */
-Prefs.setValue = function(key, val)
-{
-       if (!(key in Prefs._Defaults))
-               console.warn("unrecognized preference key '%s'", key);
-
-       var date = new Date();
-       date.setFullYear (date.getFullYear() + 1);
-       document.cookie = key+"="+val+"; expires="+date.toGMTString()+"; path=/";
+Prefs.setValue = function (key, val) {
+    if (!(key in Prefs._Defaults)) {
+        console.warn("unrecognized preference key '%s'", key);
+    };
+
+    var date = new Date();
+    date.setFullYear(date.getFullYear() + 1);
+    document.cookie = key + "=" + val + "; expires=" + date.toGMTString() + "; path=/";
 };
 
 /**
@@ -203,26 +223,31 @@ Prefs.setValue = function(key, val)
  * @param key the preference's key
  * @param fallback if the option isn't set, return this instead
  */
-Prefs.getValue = function(key, fallback)
-{
-       var val;
-
-       if (!(key in Prefs._Defaults))
-               console.warn("unrecognized preference key '%s'", key);
-
-       var lines = document.cookie.split(';');
-       for (var i=0, len=lines.length; !val && i<len; ++i) {
-               var line = lines[i].trim();
-               var delim = line.indexOf('=');
-               if ((delim === key.length) && line.indexOf(key) === 0)
-                       val = line.substring(delim + 1);
-       }
-
-       // FIXME: we support strings and booleans... add number support too?
-       if (!val) val = fallback;
-       else if (val === 'true') val = true;
-       else if (val === 'false') val = false;
-       return val;
+Prefs.getValue = function (key, fallback) {
+    var val;
+
+    if (!(key in Prefs._Defaults)) {
+        console.warn("unrecognized preference key '%s'", key);
+    };
+
+    var lines = document.cookie.split(';');
+    for (var i = 0, len = lines.length; !val && i < len; ++i) {
+        var line = lines[i].trim();
+        var delim = line.indexOf('=');
+        if ((delim === key.length) && line.indexOf(key) === 0) {
+            val = line.substring(delim + 1);
+        };
+    };
+
+    // FIXME: we support strings and booleans... add number support too?
+    if (!val) {
+        val = fallback;
+    } else if (val === 'true') {
+        val = true;
+    } else if (val === 'false') {
+        val = false;
+    };
+    return val;
 };
 
 /**
@@ -230,41 +255,40 @@ Prefs.getValue = function(key, fallback)
  *
  * @pararm o object to be populated (optional)
  */
-Prefs.getClutchPrefs = function(o)
-{
-       if (!o)
-               o = { };
-       for (var key in Prefs._Defaults)
-               o[key] = Prefs.getValue(key, Prefs._Defaults[key]);
-       return o;
+Prefs.getClutchPrefs = function (o) {
+    if (!o) {
+        o = {};
+    };
+    for (var key in Prefs._Defaults) {
+        o[key] = Prefs.getValue(key, Prefs._Defaults[key]);
+    };
+    return o;
 };
 
-
 // forceNumeric() plug-in implementation
 jQuery.fn.forceNumeric = function () {
-       return this.each(function () {
-               $(this).keydown(function (e) {
-                       var key = e.which || e.keyCode;
-                       return !e.shiftKey && !e.altKey && !e.ctrlKey &&
-                               // numbers
-                               key >= 48 && key <= 57 ||
-                               // Numeric keypad
-                               key >= 96 && key <= 105 ||
-                               // comma, period and minus, . on keypad
-                               key === 190 || key === 188 || key === 109 || key === 110 ||
-                               // Backspace and Tab and Enter
-                               key === 8 || key === 9 || key === 13 ||
-                               // Home and End
-                               key === 35 || key === 36 ||
-                               // left and right arrows
-                               key === 37 || key === 39 ||
-                               // Del and Ins
-                               key === 46 || key === 45;
-               });
-       });
+    return this.each(function () {
+        $(this).keydown(function (e) {
+            var key = e.which || e.keyCode;
+            return !e.shiftKey && !e.altKey && !e.ctrlKey &&
+                // numbers
+                key >= 48 && key <= 57 ||
+                // Numeric keypad
+                key >= 96 && key <= 105 ||
+                // comma, period and minus, . on keypad
+                key === 190 || key === 188 || key === 109 || key === 110 ||
+                // Backspace and Tab and Enter
+                key === 8 || key === 9 || key === 13 ||
+                // Home and End
+                key === 35 || key === 36 ||
+                // left and right arrows
+                key === 37 || key === 39 ||
+                // Del and Ins
+                key === 46 || key === 45;
+        });
+    });
 }
 
-
 /**
  * http://blog.stevenlevithan.com/archives/parseuri
  *
@@ -272,31 +296,35 @@ jQuery.fn.forceNumeric = function () {
  * (c) Steven Levithan <stevenlevithan.com>
  * MIT License
  */
-function parseUri (str) {
-       var     o   = parseUri.options,
-               m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
-               uri = {},
-               i   = 14;
-
-       while (i--) uri[o.key[i]] = m[i] || "";
-
-       uri[o.q.name] = {};
-       uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
-               if ($1) uri[o.q.name][$1] = $2;
-       });
-
-       return uri;
+function parseUri(str) {
+    var o = parseUri.options;
+    var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str);
+    var uri = {};
+    var i = 14;
+
+    while (i--) {
+        uri[o.key[i]] = m[i] || "";
+    };
+
+    uri[o.q.name] = {};
+    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
+        if ($1) {
+            uri[o.q.name][$1] = $2;
+        };
+    });
+
+    return uri;
 };
 
 parseUri.options = {
-       strictMode: false,
-       key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
-       q:   {
-               name:   "queryKey",
-               parser: /(?:^|&)([^&=]*)=?([^&]*)/g
-       },
-       parser: {
-               strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
-               loose:  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
-       }
+    strictMode: false,
+    key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
+    q: {
+        name: "queryKey",
+        parser: /(?:^|&)([^&=]*)=?([^&]*)/g
+    },
+    parser: {
+        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
+        loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
+    }
 };
index feade3139913a155f208237e6b8f39fdb5976cbc..13baca3dd7386ae6133a4addb86808d9bdf740d5 100644 (file)
  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  */
 
-function Dialog(){
-       this.initialize();
-}
+function Dialog() {
+    this.initialize();
+};
 
 Dialog.prototype = {
 
-       /*
-        * Constructor
-        */
-       initialize: function() {
-
-               /*
-                * Private Interface Variables
-                */
-               this._container = $('#dialog_container');
-               this._heading = $('#dialog_heading');
-               this._message = $('#dialog_message');
-               this._cancel_button = $('#dialog_cancel_button');
-               this._confirm_button = $('#dialog_confirm_button');
-               this._callback = null;
-
-               // Observe the buttons
-               this._cancel_button.bind('click', {dialog: this}, this.onCancelClicked);
-               this._confirm_button.bind('click', {dialog: this}, this.onConfirmClicked);
-       },
-
-
-
-
-
-       /*--------------------------------------------
-        *
-        *  E V E N T   F U N C T I O N S
-        *
-        *--------------------------------------------*/
-
-       hideDialog: function()
-       {
-               $('body.dialog_showing').removeClass('dialog_showing');
-               this._container.hide();
-               transmission.hideMobileAddressbar();
-               transmission.updateButtonStates();
-       },
-
-       onCancelClicked: function(event)
-       {
-               event.data.dialog.hideDialog();
-       },
-
-       onConfirmClicked: function(event)
-       {
-               var dialog = event.data.dialog;
-               dialog._callback();
-               dialog.hideDialog();
-       },
-
-       /*--------------------------------------------
-        *
-        *  I N T E R F A C E   F U N C T I O N S
-        *
-        *--------------------------------------------*/
-
-       /*
-        * Display a confirm dialog
-        */
-       confirm: function(dialog_heading, dialog_message, confirm_button_label,
-                         callback, cancel_button_label)
-       {
-               if (!isMobileDevice)
-                       $('.dialog_container').hide();
-               setTextContent(this._heading[0], dialog_heading);
-               setTextContent(this._message[0], dialog_message);
-               setTextContent(this._cancel_button[0], cancel_button_label || 'Cancel');
-               setTextContent(this._confirm_button[0], confirm_button_label);
-               this._confirm_button.show();
-               this._callback = callback;
-               $('body').addClass('dialog_showing');
-               this._container.show();
-               transmission.updateButtonStates();
-               if (isMobileDevice)
-                       transmission.hideMobileAddressbar();
-       },
-
-       /*
-        * Display an alert dialog
-        */
-       alert: function(dialog_heading, dialog_message, cancel_button_label) {
-               if (!isMobileDevice)
-                       $('.dialog_container').hide();
-               setTextContent(this._heading[0], dialog_heading);
-               setTextContent(this._message[0], dialog_message);
-               // jquery::hide() doesn't work here in Safari for some odd reason
-               this._confirm_button.css('display', 'none');
-               setTextContent(this._cancel_button[0], cancel_button_label);
-               // Just in case
-               $('#upload_container').hide();
-               $('#move_container').hide();
-               $('body').addClass('dialog_showing');
-               transmission.updateButtonStates();
-               if (isMobileDevice)
-                       transmission.hideMobileAddressbar();
-               this._container.show();
-       }
-
-
-}
+    /*
+     * Constructor
+     */
+    initialize: function () {
+
+        /*
+         * Private Interface Variables
+         */
+        this._container = $('#dialog_container');
+        this._heading = $('#dialog_heading');
+        this._message = $('#dialog_message');
+        this._cancel_button = $('#dialog_cancel_button');
+        this._confirm_button = $('#dialog_confirm_button');
+        this._callback = null;
+
+        // Observe the buttons
+        this._cancel_button.bind('click', {
+            dialog: this
+        }, this.onCancelClicked);
+        this._confirm_button.bind('click', {
+            dialog: this
+        }, this.onConfirmClicked);
+    },
+
+    /*--------------------------------------------
+     *
+     *  E V E N T   F U N C T I O N S
+     *
+     *--------------------------------------------*/
+
+    hideDialog: function () {
+        $('body.dialog_showing').removeClass('dialog_showing');
+        this._container.hide();
+        transmission.hideMobileAddressbar();
+        transmission.updateButtonStates();
+    },
+
+    onCancelClicked: function (event) {
+        event.data.dialog.hideDialog();
+    },
+
+    onConfirmClicked: function (event) {
+        var dialog = event.data.dialog;
+        dialog._callback();
+        dialog.hideDialog();
+    },
+
+    /*--------------------------------------------
+     *
+     *  I N T E R F A C E   F U N C T I O N S
+     *
+     *--------------------------------------------*/
+
+    /*
+     * Display a confirm dialog
+     */
+    confirm: function (dialog_heading, dialog_message, confirm_button_label,
+        callback, cancel_button_label) {
+        if (!isMobileDevice) {
+            $('.dialog_container').hide();
+        };
+        setTextContent(this._heading[0], dialog_heading);
+        setTextContent(this._message[0], dialog_message);
+        setTextContent(this._cancel_button[0], cancel_button_label || 'Cancel');
+        setTextContent(this._confirm_button[0], confirm_button_label);
+        this._confirm_button.show();
+        this._callback = callback;
+        $('body').addClass('dialog_showing');
+        this._container.show();
+        transmission.updateButtonStates();
+        if (isMobileDevice) {
+            transmission.hideMobileAddressbar();
+        };
+    },
+
+    /*
+     * Display an alert dialog
+     */
+    alert: function (dialog_heading, dialog_message, cancel_button_label) {
+        if (!isMobileDevice) {
+            $('.dialog_container').hide();
+        };
+        setTextContent(this._heading[0], dialog_heading);
+        setTextContent(this._message[0], dialog_message);
+        // jquery::hide() doesn't work here in Safari for some odd reason
+        this._confirm_button.css('display', 'none');
+        setTextContent(this._cancel_button[0], cancel_button_label);
+        // Just in case
+        $('#upload_container').hide();
+        $('#move_container').hide();
+        $('body').addClass('dialog_showing');
+        transmission.updateButtonStates();
+        if (isMobileDevice) {
+            transmission.hideMobileAddressbar();
+        };
+        this._container.show();
+    }
+};
index 12948464ae9e9fe3fc31913782649562c861f99a..62045051b607c42f1c5d24ca9393d037b7d1246b 100644 (file)
  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  */
 
-function FileRow(torrent, depth, name, indices, even)
-{
-       var fields = {
-               have: 0,
-               indices: [],
-               isWanted: true,
-               priorityLow: false,
-               priorityNormal: false,
-               priorityHigh: false,
-               me: this,
-               size: 0,
-               torrent: null
-       },
-
-       elements = {
-               priority_low_button: null,
-               priority_normal_button: null,
-               priority_high_button: null,
-               progress: null,
-               root: null
-       },
-
-       initialize = function(torrent, depth, name, indices, even) {
-               fields.torrent = torrent;
-               fields.indices = indices;
-               createRow(torrent, depth, name, even);
-       },
-
-       refreshWantedHTML = function()
-       {
-               var e = $(elements.root);
-               e.toggleClass('skip', !fields.isWanted);
-               e.toggleClass('complete', isDone());
-               $(e[0].checkbox).prop('disabled', !isEditable());
-               $(e[0].checkbox).prop('checked', fields.isWanted);
-       },
-       refreshProgressHTML = function()
-       {
-               var pct = 100 * (fields.size ? (fields.have / fields.size) : 1.0),
-                   c = [ Transmission.fmt.size(fields.have),
-                         ' of ',
-                         Transmission.fmt.size(fields.size),
-                         ' (',
-                         Transmission.fmt.percentString(pct),
-                         '%)' ].join('');
-               setTextContent(elements.progress, c);
-       },
-       refreshImpl = function() {
-               var i,
-                   file,
-                   have = 0,
-                   size = 0,
-                   wanted = false,
-                   low = false,
-                   normal = false,
-                   high = false;
-
-               // loop through the file_indices that affect this row
-               for (i=0; i<fields.indices.length; ++i) {
-                       file = fields.torrent.getFile (fields.indices[i]);
-                       have += file.bytesCompleted;
-                       size += file.length;
-                       wanted |= file.wanted;
-                       switch (file.priority) {
-                               case -1: low = true; break;
-                               case  0: normal = true; break;
-                               case  1: high = true; break;
-                       }
-               }
-
-               if ((fields.have != have) || (fields.size != size)) {
-                       fields.have = have;
-                       fields.size = size;
-                       refreshProgressHTML();
-               }
-
-               if (fields.isWanted !== wanted) {
-                       fields.isWanted = wanted;
-                       refreshWantedHTML();
-               }
-
-               if (fields.priorityLow !== low) {
-                       fields.priorityLow = low;
-                       $(elements.priority_low_button).toggleClass('selected', low);
-               }
-
-               if (fields.priorityNormal !== normal) {
-                       fields.priorityNormal = normal;
-                       $(elements.priority_normal_button).toggleClass('selected', normal);
-               }
-
-               if (fields.priorityHigh !== high) {
-                       fields.priorityHigh = high;
-                       $(elements.priority_high_button).toggleClass('selected', high);
-               }
-       },
-
-       isDone = function () {
-               return fields.have >= fields.size;
-       },
-       isEditable = function () {
-               return (fields.torrent.getFileCount()>1) && !isDone();
-       },
-
-       createRow = function(torrent, depth, name, even) {
-               var e, root, box;
-
-               root = document.createElement('li');
-               root.className = 'inspector_torrent_file_list_entry' + (even?'even':'odd');
-               elements.root = root;
-
-               e = document.createElement('input');
-               e.type = 'checkbox';
-               e.className = "file_wanted_control";
-               e.title = 'Download file';
-               $(e).change(function(ev){ fireWantedChanged( $(ev.currentTarget).prop('checked')); });
-               root.checkbox = e;
-               root.appendChild(e);
-
-               e = document.createElement('div');
-               e.className = 'file-priority-radiobox';
-               box = e;
-
-                       e = document.createElement('div');
-                       e.className = 'low';
-                       e.title = 'Low Priority';
-                       $(e).click(function(){ firePriorityChanged(-1); });
-                       elements.priority_low_button = e;
-                       box.appendChild(e);
-
-                       e = document.createElement('div');
-                       e.className = 'normal';
-                       e.title = 'Normal Priority';
-                       $(e).click(function(){ firePriorityChanged(0); });
-                       elements.priority_normal_button = e;
-                       box.appendChild(e);
-
-                       e = document.createElement('div');
-                       e.title = 'High Priority';
-                       e.className = 'high';
-                       $(e).click(function(){ firePriorityChanged(1); });
-                       elements.priority_high_button = e;
-                       box.appendChild(e);
-
-               root.appendChild(box);
-
-               e = document.createElement('div');
-               e.className = "inspector_torrent_file_list_entry_name";
-               setTextContent(e, name);
-               $(e).click(function(){ fireNameClicked(-1); });
-               root.appendChild(e);
-
-               e = document.createElement('div');
-               e.className = "inspector_torrent_file_list_entry_progress";
-               root.appendChild(e);
-               $(e).click(function(){ fireNameClicked(-1); });
-               elements.progress = e;
-
-               $(root).css('margin-left', '' + (depth*16) + 'px');
-
-               refreshImpl();
-               return root;
-       },
-
-       fireWantedChanged = function(do_want) {
-               $(fields.me).trigger('wantedToggled',[ fields.indices, do_want ]);
-       },
-       firePriorityChanged = function(priority) {
-               $(fields.me).trigger('priorityToggled',[ fields.indices, priority ]);
-       },
-       fireNameClicked = function() {
-               $(fields.me).trigger('nameClicked',[ fields.me, fields.indices ]);
-       };
-
-       /***
-       ****  PUBLIC
-       ***/
-
-       this.getElement = function() {
-               return elements.root;
-       };
-       this.refresh = function() {
-               refreshImpl();
-       };
-
-       initialize(torrent, depth, name, indices, even);
+function FileRow(torrent, depth, name, indices, even) {
+    var fields = {
+        have: 0,
+        indices: [],
+        isWanted: true,
+        priorityLow: false,
+        priorityNormal: false,
+        priorityHigh: false,
+        me: this,
+        size: 0,
+        torrent: null
+    };
+
+    var elements = {
+        priority_low_button: null,
+        priority_normal_button: null,
+        priority_high_button: null,
+        progress: null,
+        root: null
+    };
+
+    var initialize = function (torrent, depth, name, indices, even) {
+        fields.torrent = torrent;
+        fields.indices = indices;
+        createRow(torrent, depth, name, even);
+    };
+
+    var refreshWantedHTML = function () {
+        var e = $(elements.root);
+        e.toggleClass('skip', !fields.isWanted);
+        e.toggleClass('complete', isDone());
+        $(e[0].checkbox).prop('disabled', !isEditable());
+        $(e[0].checkbox).prop('checked', fields.isWanted);
+    };
+
+    var refreshProgressHTML = function () {
+        var pct = 100 * (fields.size ? (fields.have / fields.size) : 1.0)
+        var c = [Transmission.fmt.size(fields.have), ' of ', Transmission.fmt.size(fields.size), ' (', Transmission.fmt.percentString(pct), '%)'].join('');
+        setTextContent(elements.progress, c);
+    };
+
+    var refreshImpl = function () {
+        var i,
+            file,
+            have = 0,
+            size = 0,
+            wanted = false,
+            low = false,
+            normal = false,
+            high = false;
+
+        // loop through the file_indices that affect this row
+        for (i = 0; i < fields.indices.length; ++i) {
+            file = fields.torrent.getFile(fields.indices[i]);
+            have += file.bytesCompleted;
+            size += file.length;
+            wanted |= file.wanted;
+            switch (file.priority) {
+            case -1:
+                low = true;
+                break;
+            case 0:
+                normal = true;
+                break;
+            case 1:
+                high = true;
+                break;
+            }
+        }
+
+        if ((fields.have != have) || (fields.size != size)) {
+            fields.have = have;
+            fields.size = size;
+            refreshProgressHTML();
+        }
+
+        if (fields.isWanted !== wanted) {
+            fields.isWanted = wanted;
+            refreshWantedHTML();
+        }
+
+        if (fields.priorityLow !== low) {
+            fields.priorityLow = low;
+            $(elements.priority_low_button).toggleClass('selected', low);
+        }
+
+        if (fields.priorityNormal !== normal) {
+            fields.priorityNormal = normal;
+            $(elements.priority_normal_button).toggleClass('selected', normal);
+        }
+
+        if (fields.priorityHigh !== high) {
+            fields.priorityHigh = high;
+            $(elements.priority_high_button).toggleClass('selected', high);
+        }
+    };
+
+    var isDone = function () {
+        return fields.have >= fields.size;
+    };
+
+    var isEditable = function () {
+        return (fields.torrent.getFileCount() > 1) && !isDone();
+    };
+
+    var createRow = function (torrent, depth, name, even) {
+        var e, root, box;
+
+        root = document.createElement('li');
+        root.className = 'inspector_torrent_file_list_entry' + (even ? 'even' : 'odd');
+        elements.root = root;
+
+        e = document.createElement('input');
+        e.type = 'checkbox';
+        e.className = "file_wanted_control";
+        e.title = 'Download file';
+        $(e).change(function (ev) {
+            fireWantedChanged($(ev.currentTarget).prop('checked'));
+        });
+        root.checkbox = e;
+        root.appendChild(e);
+
+        e = document.createElement('div');
+        e.className = 'file-priority-radiobox';
+        box = e;
+
+        e = document.createElement('div');
+        e.className = 'low';
+        e.title = 'Low Priority';
+        $(e).click(function () {
+            firePriorityChanged(-1);
+        });
+        elements.priority_low_button = e;
+        box.appendChild(e);
+
+        e = document.createElement('div');
+        e.className = 'normal';
+        e.title = 'Normal Priority';
+        $(e).click(function () {
+            firePriorityChanged(0);
+        });
+        elements.priority_normal_button = e;
+        box.appendChild(e);
+
+        e = document.createElement('div');
+        e.title = 'High Priority';
+        e.className = 'high';
+        $(e).click(function () {
+            firePriorityChanged(1);
+        });
+        elements.priority_high_button = e;
+        box.appendChild(e);
+
+        root.appendChild(box);
+
+        e = document.createElement('div');
+        e.className = "inspector_torrent_file_list_entry_name";
+        setTextContent(e, name);
+        $(e).click(function () {
+            fireNameClicked(-1);
+        });
+        root.appendChild(e);
+
+        e = document.createElement('div');
+        e.className = "inspector_torrent_file_list_entry_progress";
+        root.appendChild(e);
+        $(e).click(function () {
+            fireNameClicked(-1);
+        });
+        elements.progress = e;
+
+        $(root).css('margin-left', '' + (depth * 16) + 'px');
+
+        refreshImpl();
+        return root;
+    };
+
+    var fireWantedChanged = function (do_want) {
+        $(fields.me).trigger('wantedToggled', [fields.indices, do_want]);
+    };
+
+    var firePriorityChanged = function (priority) {
+        $(fields.me).trigger('priorityToggled', [fields.indices, priority]);
+    };
+
+    var fireNameClicked = function () {
+        $(fields.me).trigger('nameClicked', [fields.me, fields.indices]);
+    };
+
+    /***
+     ****  PUBLIC
+     ***/
+
+    this.getElement = function () {
+        return elements.root;
+    };
+    this.refresh = function () {
+        refreshImpl();
+    };
+
+    initialize(torrent, depth, name, indices, even);
 };
index c22614791a19df7f1f445291cf226d4d619d2bd9..eb7ae88e6ffa44c5d06391a6d07413583eb9e5dd 100644 (file)
  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  */
 
-Transmission.fmt = (function()
-{
-       var speed_K = 1000;
-       var speed_B_str =  'B/s';
-       var speed_K_str = 'kB/s';
-       var speed_M_str = 'MB/s';
-       var speed_G_str = 'GB/s';
-       var speed_T_str = 'TB/s';
-
-       var size_K = 1000;
-       var size_B_str =  'B';
-       var size_K_str = 'kB';
-       var size_M_str = 'MB';
-       var size_G_str = 'GB';
-       var size_T_str = 'TB';
-
-       var mem_K = 1024;
-       var mem_B_str =   'B';
-       var mem_K_str = 'KiB';
-       var mem_M_str = 'MiB';
-       var mem_G_str = 'GiB';
-       var mem_T_str = 'TiB';
-
-       return {
-
-               updateUnits: function(u)
-               {
-/*
-                       speed_K     = u['speed-bytes'];
-                       speed_K_str = u['speed-units'][0];
-                       speed_M_str = u['speed-units'][1];
-                       speed_G_str = u['speed-units'][2];
-                       speed_T_str = u['speed-units'][3];
-
-                       size_K     = u['size-bytes'];
-                       size_K_str = u['size-units'][0];
-                       size_M_str = u['size-units'][1];
-                       size_G_str = u['size-units'][2];
-                       size_T_str = u['size-units'][3];
-
-                       mem_K     = u['memory-bytes'];
-                       mem_K_str = u['memory-units'][0];
-                       mem_M_str = u['memory-units'][1];
-                       mem_G_str = u['memory-units'][2];
-                       mem_T_str = u['memory-units'][3];
-*/
-               },
-
-               /*
-                *   Format a percentage to a string
-                */
-               percentString: function(x) {
-                       if (x < 10.0)
-                               return x.toTruncFixed(2);
-                       else if (x < 100.0)
-                               return x.toTruncFixed(1);
-                       else
-                               return x.toTruncFixed(0);
-               },
-
-               /*
-                *   Format a ratio to a string
-                */
-               ratioString: function(x) {
-                       if (x === -1)
-                               return "None";
-                       if (x === -2)
-                               return '&infin;';
-                       return this.percentString(x);
-               },
-
-               /**
-                * Formats the a memory size into a human-readable string
-                * @param {Number} bytes the filesize in bytes
-                * @return {String} human-readable string
-                */
-               mem: function(bytes)
-               {
-                       if (bytes < mem_K)
-                               return [ bytes, mem_B_str ].join(' ');
-
-                       var convertedSize;
-                       var unit;
-
-                       if (bytes < Math.pow(mem_K, 2))
-                       {
-                               convertedSize = bytes / mem_K;
-                               unit = mem_K_str;
-                       }
-                       else if (bytes < Math.pow(mem_K, 3))
-                       {
-                               convertedSize = bytes / Math.pow(mem_K, 2);
-                               unit = mem_M_str;
-                       }
-                       else if (bytes < Math.pow(mem_K, 4))
-                       {
-                               convertedSize = bytes / Math.pow(mem_K, 3);
-                               unit = mem_G_str;
-                       }
-                       else
-                       {
-                               convertedSize = bytes / Math.pow(mem_K, 4);
-                               unit = mem_T_str;
-                       }
-
-                       // try to have at least 3 digits and at least 1 decimal
-                       return convertedSize <= 9.995 ? [ convertedSize.toTruncFixed(2), unit ].join(' ')
-                                                     : [ convertedSize.toTruncFixed(1), unit ].join(' ');
-               },
-
-               /**
-                * Formats the a disk capacity or file size into a human-readable string
-                * @param {Number} bytes the filesize in bytes
-                * @return {String} human-readable string
-                */
-               size: function(bytes)
-               {
-                       if (bytes < size_K)
-                               return [ bytes, size_B_str ].join(' ');
-
-                       var convertedSize;
-                       var unit;
-
-                       if (bytes < Math.pow(size_K, 2))
-                       {
-                               convertedSize = bytes / size_K;
-                               unit = size_K_str;
-                       }
-                       else if (bytes < Math.pow(size_K, 3))
-                       {
-                               convertedSize = bytes / Math.pow(size_K, 2);
-                               unit = size_M_str;
-                       }
-                       else if (bytes < Math.pow(size_K, 4))
-                       {
-                               convertedSize = bytes / Math.pow(size_K, 3);
-                               unit = size_G_str;
-                       }
-                       else
-                       {
-                               convertedSize = bytes / Math.pow(size_K, 4);
-                               unit = size_T_str;
-                       }
-
-                       // try to have at least 3 digits and at least 1 decimal
-                       return convertedSize <= 9.995 ? [ convertedSize.toTruncFixed(2), unit ].join(' ')
-                                                     : [ convertedSize.toTruncFixed(1), unit ].join(' ');
-               },
-
-               speedBps: function(Bps)
-               {
-                       return this.speed(this.toKBps(Bps));
-               },
-
-               toKBps: function(Bps)
-               {
-                       return Math.floor(Bps / speed_K);
-               },
-
-               speed: function(KBps)
-               {
-                       var speed = KBps;
-
-                       if (speed <= 999.95) // 0 KBps to 999 K
-                               return [ speed.toTruncFixed(0), speed_K_str ].join(' ');
-
-                       speed /= speed_K;
-
-                       if (speed <= 99.995) // 1 M to 99.99 M
-                               return [ speed.toTruncFixed(2), speed_M_str ].join(' ');
-                       if (speed <= 999.95) // 100 M to 999.9 M
-                               return [ speed.toTruncFixed(1), speed_M_str ].join(' ');
-
-                       // insane speeds
-                       speed /= speed_K;
-                       return [ speed.toTruncFixed(2), speed_G_str ].join(' ');
-               },
-
-               timeInterval: function(seconds)
-               {
-                       var days    = Math.floor (seconds / 86400),
-                           hours   = Math.floor ((seconds % 86400) / 3600),
-                           minutes = Math.floor ((seconds % 3600) / 60),
-                           seconds = Math.floor (seconds % 60),
-                           d = days    + ' ' + (days    > 1 ? 'days'    : 'day'),
-                           h = hours   + ' ' + (hours   > 1 ? 'hours'   : 'hour'),
-                           m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute'),
-                           s = seconds + ' ' + (seconds > 1 ? 'seconds' : 'second');
-
-                       if (days) {
-                               if (days >= 4 || !hours)
-                                       return d;
-                               return d + ', ' + h;
-                       }
-                       if (hours) {
-                               if (hours >= 4 || !minutes)
-                                       return h;
-                               return h + ', ' + m;
-                       }
-                       if (minutes) {
-                               if (minutes >= 4 || !seconds)
-                                       return m;
-                               return m + ', ' + s;
-                       }
-                       return s;
-               },
-
-               timestamp: function(seconds)
-               {
-                       if (!seconds)
-                               return 'N/A';
-
-                       var myDate = new Date(seconds*1000);
-                       var now = new Date();
-
-                       var date = "";
-                       var time = "";
-
-                       var sameYear = now.getFullYear() === myDate.getFullYear();
-                       var sameMonth = now.getMonth() === myDate.getMonth();
-
-                       var dateDiff = now.getDate() - myDate.getDate();
-                       if (sameYear && sameMonth && Math.abs(dateDiff) <= 1){
-                               if (dateDiff === 0){
-                                       date = "Today";
-                               }
-                               else if (dateDiff === 1){
-                                       date = "Yesterday";
-                               }
-                               else{
-                                       date = "Tomorrow";
-                               }
-                       }
-                       else{
-                               date = myDate.toDateString();
-                       }
-
-                       var hours = myDate.getHours();
-                       var period = "AM";
-                       if (hours > 12){
-                               hours = hours - 12;
-                               period = "PM";
-                       }
-                       if (hours === 0){
-                               hours = 12;
-                       }
-                       if (hours < 10){
-                               hours = "0" + hours;
-                       }
-                       var minutes = myDate.getMinutes();
-                       if (minutes < 10){
-                               minutes = "0" + minutes;
-                       }
-                       var seconds = myDate.getSeconds();
-                               if (seconds < 10){
-                                       seconds = "0" + seconds;
-                       }
-
-                       time = [hours, minutes, seconds].join(':');
-
-                       return [date, time, period].join(' ');
-               },
-
-               ngettext: function(msgid, msgid_plural, n)
-               {
-                       // TODO(i18n): http://doc.qt.digia.com/4.6/i18n-plural-rules.html
-                       return n === 1 ? msgid : msgid_plural;
-               },
-
-               countString: function(msgid, msgid_plural, n)
-               {
-                       return [ n.toStringWithCommas(), this.ngettext(msgid,msgid_plural,n) ].join(' ');
-               },
-
-               peerStatus: function( flagStr )
-               {
-                       var formattedFlags = [];
-                       for (var i=0, flag; flag=flagStr[i]; ++i)
-                       {
-                               var explanation = null;
-                               switch (flag)
-                               {
-                                       case "O": explanation = "Optimistic unchoke"; break;
-                                       case "D": explanation = "Downloading from this peer"; break;
-                                       case "d": explanation = "We would download from this peer if they'd let us"; break;
-                                       case "U": explanation = "Uploading to peer"; break;
-                                       case "u": explanation = "We would upload to this peer if they'd ask"; break;
-                                       case "K": explanation = "Peer has unchoked us, but we're not interested"; break;
-                                       case "?": explanation = "We unchoked this peer, but they're not interested"; break;
-                                       case "E": explanation = "Encrypted Connection"; break;
-                                       case "H": explanation = "Peer was discovered through Distributed Hash Table (DHT)"; break;
-                                       case "X": explanation = "Peer was discovered through Peer Exchange (PEX)"; break;
-                                       case "I": explanation = "Peer is an incoming connection"; break;
-                                       case "T": explanation = "Peer is connected via uTP"; break;
-                               }
-
-                               if (!explanation) {
-                                       formattedFlags.push(flag);
-                               } else {
-                                       formattedFlags.push("<span title=\"" + flag + ': ' + explanation + "\">" + flag + "</span>");
-                               }
-                       }
-                       return formattedFlags.join('');
-               }
-       }
+Transmission.fmt = (function () {
+    var speed_K = 1000;
+    var speed_B_str = 'B/s';
+    var speed_K_str = 'kB/s';
+    var speed_M_str = 'MB/s';
+    var speed_G_str = 'GB/s';
+    var speed_T_str = 'TB/s';
+
+    var size_K = 1000;
+    var size_B_str = 'B';
+    var size_K_str = 'kB';
+    var size_M_str = 'MB';
+    var size_G_str = 'GB';
+    var size_T_str = 'TB';
+
+    var mem_K = 1024;
+    var mem_B_str = 'B';
+    var mem_K_str = 'KiB';
+    var mem_M_str = 'MiB';
+    var mem_G_str = 'GiB';
+    var mem_T_str = 'TiB';
+
+    return {
+
+        /*
+         *   Format a percentage to a string
+         */
+        percentString: function (x) {
+            if (x < 10.0) {
+                return x.toTruncFixed(2);
+            } else if (x < 100.0) {
+                return x.toTruncFixed(1);
+            } else {
+                return x.toTruncFixed(0);
+            }
+        },
+
+        /*
+         *   Format a ratio to a string
+         */
+        ratioString: function (x) {
+            if (x === -1) {
+                return "None";
+            }
+            if (x === -2) {
+                return '&infin;';
+            }
+            return this.percentString(x);
+        },
+
+        /**
+         * Formats the a memory size into a human-readable string
+         * @param {Number} bytes the filesize in bytes
+         * @return {String} human-readable string
+         */
+        mem: function (bytes) {
+            if (bytes < mem_K)
+                return [bytes, mem_B_str].join(' ');
+
+            var convertedSize;
+            var unit;
+
+            if (bytes < Math.pow(mem_K, 2)) {
+                convertedSize = bytes / mem_K;
+                unit = mem_K_str;
+            } else if (bytes < Math.pow(mem_K, 3)) {
+                convertedSize = bytes / Math.pow(mem_K, 2);
+                unit = mem_M_str;
+            } else if (bytes < Math.pow(mem_K, 4)) {
+                convertedSize = bytes / Math.pow(mem_K, 3);
+                unit = mem_G_str;
+            } else {
+                convertedSize = bytes / Math.pow(mem_K, 4);
+                unit = mem_T_str;
+            }
+
+            // try to have at least 3 digits and at least 1 decimal
+            return convertedSize <= 9.995 ? [convertedSize.toTruncFixed(2), unit].join(' ') : [convertedSize.toTruncFixed(1), unit].join(' ');
+        },
+
+        /**
+         * Formats the a disk capacity or file size into a human-readable string
+         * @param {Number} bytes the filesize in bytes
+         * @return {String} human-readable string
+         */
+        size: function (bytes) {
+            if (bytes < size_K) {
+                return [bytes, size_B_str].join(' ');
+            }
+
+            var convertedSize;
+            var unit;
+
+            if (bytes < Math.pow(size_K, 2)) {
+                convertedSize = bytes / size_K;
+                unit = size_K_str;
+            } else if (bytes < Math.pow(size_K, 3)) {
+                convertedSize = bytes / Math.pow(size_K, 2);
+                unit = size_M_str;
+            } else if (bytes < Math.pow(size_K, 4)) {
+                convertedSize = bytes / Math.pow(size_K, 3);
+                unit = size_G_str;
+            } else {
+                convertedSize = bytes / Math.pow(size_K, 4);
+                unit = size_T_str;
+            }
+
+            // try to have at least 3 digits and at least 1 decimal
+            return convertedSize <= 9.995 ? [convertedSize.toTruncFixed(2), unit].join(' ') : [convertedSize.toTruncFixed(1), unit].join(' ');
+        },
+
+        speedBps: function (Bps) {
+            return this.speed(this.toKBps(Bps));
+        },
+
+        toKBps: function (Bps) {
+            return Math.floor(Bps / speed_K);
+        },
+
+        speed: function (KBps) {
+            var speed = KBps;
+
+            if (speed <= 999.95) { // 0 KBps to 999 K
+                return [speed.toTruncFixed(0), speed_K_str].join(' ');
+            }
+
+            speed /= speed_K;
+
+            if (speed <= 99.995) { // 1 M to 99.99 M
+                return [speed.toTruncFixed(2), speed_M_str].join(' ');
+            }
+            if (speed <= 999.95) { // 100 M to 999.9 M
+                return [speed.toTruncFixed(1), speed_M_str].join(' ');
+            }
+
+            // insane speeds
+            speed /= speed_K;
+            return [speed.toTruncFixed(2), speed_G_str].join(' ');
+        },
+
+        timeInterval: function (seconds) {
+            var days = Math.floor(seconds / 86400),
+                hours = Math.floor((seconds % 86400) / 3600),
+                minutes = Math.floor((seconds % 3600) / 60),
+                seconds = Math.floor(seconds % 60),
+                d = days + ' ' + (days > 1 ? 'days' : 'day'),
+                h = hours + ' ' + (hours > 1 ? 'hours' : 'hour'),
+                m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute'),
+                s = seconds + ' ' + (seconds > 1 ? 'seconds' : 'second');
+
+            if (days) {
+                if (days >= 4 || !hours) {
+                    return d;
+                }
+                return d + ', ' + h;
+            }
+            if (hours) {
+                if (hours >= 4 || !minutes) {
+                    return h;
+                }
+                return h + ', ' + m;
+            }
+            if (minutes) {
+                if (minutes >= 4 || !seconds) {
+                    return m;
+                }
+                return m + ', ' + s;
+            }
+            return s;
+        },
+
+        timestamp: function (seconds) {
+            if (!seconds) {
+                return 'N/A';
+            }
+
+            var myDate = new Date(seconds * 1000);
+            var now = new Date();
+
+            var date = "";
+            var time = "";
+
+            var sameYear = now.getFullYear() === myDate.getFullYear();
+            var sameMonth = now.getMonth() === myDate.getMonth();
+
+            var dateDiff = now.getDate() - myDate.getDate();
+            if (sameYear && sameMonth && Math.abs(dateDiff) <= 1) {
+                if (dateDiff === 0) {
+                    date = "Today";
+                } else if (dateDiff === 1) {
+                    date = "Yesterday";
+                } else {
+                    date = "Tomorrow";
+                }
+            } else {
+                date = myDate.toDateString();
+            }
+
+            var hours = myDate.getHours();
+            var period = "AM";
+            if (hours > 12) {
+                hours = hours - 12;
+                period = "PM";
+            }
+            if (hours === 0) {
+                hours = 12;
+            }
+            if (hours < 10) {
+                hours = "0" + hours;
+            }
+            var minutes = myDate.getMinutes();
+            if (minutes < 10) {
+                minutes = "0" + minutes;
+            }
+            var seconds = myDate.getSeconds();
+            if (seconds < 10) {
+                seconds = "0" + seconds;
+            }
+
+            time = [hours, minutes, seconds].join(':');
+
+            return [date, time, period].join(' ');
+        },
+
+        ngettext: function (msgid, msgid_plural, n) {
+            // TODO(i18n): http://doc.qt.digia.com/4.6/i18n-plural-rules.html
+            return n === 1 ? msgid : msgid_plural;
+        },
+
+        countString: function (msgid, msgid_plural, n) {
+            return [n.toStringWithCommas(), this.ngettext(msgid, msgid_plural, n)].join(' ');
+        },
+
+        peerStatus: function (flagStr) {
+            var formattedFlags = [];
+            for (var i = 0, flag; flag = flagStr[i]; ++i) {
+                var explanation = null;
+                switch (flag) {
+                case "O":
+                    explanation = "Optimistic unchoke";
+                    break;
+                case "D":
+                    explanation = "Downloading from this peer";
+                    break;
+                case "d":
+                    explanation = "We would download from this peer if they'd let us";
+                    break;
+                case "U":
+                    explanation = "Uploading to peer";
+                    break;
+                case "u":
+                    explanation = "We would upload to this peer if they'd ask";
+                    break;
+                case "K":
+                    explanation = "Peer has unchoked us, but we're not interested";
+                    break;
+                case "?":
+                    explanation = "We unchoked this peer, but they're not interested";
+                    break;
+                case "E":
+                    explanation = "Encrypted Connection";
+                    break;
+                case "H":
+                    explanation = "Peer was discovered through Distributed Hash Table (DHT)";
+                    break;
+                case "X":
+                    explanation = "Peer was discovered through Peer Exchange (PEX)";
+                    break;
+                case "I":
+                    explanation = "Peer is an incoming connection";
+                    break;
+                case "T":
+                    explanation = "Peer is connected via uTP";
+                    break;
+                };
+
+                if (!explanation) {
+                    formattedFlags.push(flag);
+                } else {
+                    formattedFlags.push("<span title=\"" + flag + ': ' + explanation + "\">" + flag + "</span>");
+                };
+            };
+
+            return formattedFlags.join('');
+        }
+    }
 })();
index 19ea36aafbaa2de61bf61007e632c34cc5282d34..0ad78872baea65f7bf116ca191f9f001407ddc5a 100644 (file)
 
 function Inspector(controller) {
 
-       var data = {
-               controller: null,
-               elements: { },
-               torrents: [ ]
-       },
-
-       needsExtraInfo = function (torrents) {
-               var i, id, tor;
-
-               for (i = 0; tor = torrents[i]; i++)
-                       if (!tor.hasExtraInfo())
-                               return true;
-
-               return false;
-       },
-
-       refreshTorrents = function () {
-               var fields,
-                   ids = $.map(data.torrents.slice(0), function (t) {return t.getId();});
-
-               if (ids && ids.length)
-               {
-                       fields = ['id'].concat(Torrent.Fields.StatsExtra);
-
-                       if (needsExtraInfo(data.torrents))
-                               $.merge(fields, Torrent.Fields.InfoExtra);
-
-                       data.controller.updateTorrents(ids, fields);
-               }
-       },
-
-       onTabClicked = function (ev) {
-               var tab = ev.currentTarget;
-
-               if (isMobileDevice)
-                       ev.stopPropagation();
-
-               // select this tab and deselect the others
-               $(tab).addClass('selected').siblings().removeClass('selected');
-
-               // show this tab and hide the others
-               $('#' + tab.id.replace('tab','page')).show().siblings('.inspector-page').hide();
-
-               updateInspector();
-       },
-
-       updateInspector = function () {
-               var e = data.elements,
-                   torrents = data.torrents,
-                   name;
-
-               // update the name, which is shown on all the pages
-               if (!torrents || !torrents.length)
-                       name = 'No Selection';
-               else if(torrents.length === 1)
-                       name = torrents[0].getName();
-               else
-                       name = '' + torrents.length+' Transfers Selected';
-               setTextContent(e.name_lb, name || na);
-
-               // update the visible page
-               if ($(e.info_page).is(':visible'))
-                       updateInfoPage();
-               else if ($(e.peers_page).is(':visible'))
-                       updatePeersPage();
-               else if ($(e.trackers_page).is(':visible'))
-                       updateTrackersPage();
-               else if ($(e.files_page).is(':visible'))
-                       updateFilesPage();
-       },
-
-       /****
-       *****  GENERAL INFO PAGE
-       ****/
-
-       updateInfoPage = function () {
-               var torrents = data.torrents,
-                   e = data.elements,
-                   fmt = Transmission.fmt,
-                   none = 'None',
-                   mixed = 'Mixed',
-                   unknown = 'Unknown',
-                   isMixed, allPaused, allFinished,
-                   str,
-                   baseline, it, s, i, t,
-                   sizeWhenDone = 0,
-                   leftUntilDone = 0,
-                   available = 0,
-                   haveVerified = 0,
-                   haveUnverified = 0,
-                   verifiedPieces = 0,
-                   stateString,
-                   latest,
-                   pieces,
-                   size,
-                   pieceSize,
-                   creator, mixed_creator,
-                   date, mixed_date,
-                   v, u, f, d, pct,
-                   uri,
-                   now = Date.now();
-
-               //
-               //  state_lb
-               //
-
-               if(torrents.length <1)
-                       str = none;
-               else {
-                       isMixed = false;
-                       allPaused = true;
-                       allFinished = true;
-
-                       baseline = torrents[0].getStatus();
-                       for(i=0; t=torrents[i]; ++i) {
-                               it = t.getStatus();
-                               if(it != baseline)
-                                       isMixed = true;
-                               if(!t.isStopped())
-                                       allPaused = allFinished = false;
-                               if(!t.isFinished())
-                                       allFinished = false;
-                       }
-                       if( isMixed )
-                               str = mixed;
-                       else if( allFinished )
-                               str = 'Finished';
-                       else if( allPaused )
-                               str = 'Paused';
-                       else
-                               str = torrents[0].getStateString();
-               }
-               setTextContent(e.state_lb, str);
-               stateString = str;
-
-               //
-               //  have_lb
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       baseline = torrents[0].getStatus();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(!t.needsMetaData()) {
-                                       haveUnverified += t.getHaveUnchecked();
-                                       v = t.getHaveValid();
-                                       haveVerified += v;
-                                       if(t.getPieceSize())
-                                               verifiedPieces += v / t.getPieceSize();
-                                       sizeWhenDone += t.getSizeWhenDone();
-                                       leftUntilDone += t.getLeftUntilDone();
-                                       available += (t.getHave()) + t.getDesiredAvailable();
-                               }
-                       }
-
-                       d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 );
-                       str = fmt.percentString( d );
-
-                       if( !haveUnverified && !leftUntilDone )
-                               str = fmt.size(haveVerified) + ' (100%)';
-                       else if( !haveUnverified )
-                               str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%)';
-                       else
-                               str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%), ' + fmt.size(haveUnverified) + ' Unverified';
-               }
-               setTextContent(e.have_lb, str);
-
-               //
-               //  availability_lb
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else if( sizeWhenDone == 0 )
-                       str = none;
-               else
-                       str = '' + fmt.percentString( ( 100.0 * available ) / sizeWhenDone ) +  '%';
-               setTextContent(e.availability_lb, str);
-
-               //
-               //  downloaded_lb
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       d = f = 0;
-                       for(i=0; t=torrents[i]; ++i) {
-                               d += t.getDownloadedEver();
-                               f += t.getFailedEver();
-                       }
-                       if(f)
-                               str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
-                       else
-                               str = fmt.size(d);
-               }
-               setTextContent(e.downloaded_lb, str);
-
-               //
-               //  uploaded_lb
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       d = u = 0;
-                       if(torrents.length == 1) {
-                               d = torrents[0].getDownloadedEver();
-                               u = torrents[0].getUploadedEver();
-
-                               if (d == 0)
-                                       d = torrents[0].getHaveValid();
-                       }
-                       else {
-                               for(i=0; t=torrents[i]; ++i) {
-                                       d += t.getDownloadedEver();
-                                       u += t.getUploadedEver();
-                               }
-                       }
-                       str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString( Math.ratio(u,d))+')';
-               }
-               setTextContent(e.uploaded_lb, str);
-
-               //
-               // running time
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       allPaused = true;
-                       baseline = torrents[0].getStartDate();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(baseline != t.getStartDate())
-                                       baseline = 0;
-                               if(!t.isStopped())
-                                       allPaused = false;
-                       }
-                       if(allPaused)
-                               str = stateString; // paused || finished
-                       else if(!baseline)
-                               str = mixed;
-                       else
-                               str = fmt.timeInterval(now/1000 - baseline);
-               }
-               setTextContent(e.running_time_lb, str);
-
-               //
-               // remaining time
-               //
-
-               str = '';
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       baseline = torrents[0].getETA();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(baseline != t.getETA()) {
-                                       str = mixed;
-                                       break;
-                               }
-                       }
-               }
-               if(!str.length) {
-                       if(baseline < 0)
-                               str = unknown;
-                       else
-                               str = fmt.timeInterval(baseline);
-               }
-               setTextContent(e.remaining_time_lb, str);
-
-               //
-               // last activity
-               //
-
-               latest = -1;
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       baseline = torrents[0].getLastActivity();
-                       for(i=0; t=torrents[i]; ++i) {
-                               d = t.getLastActivity();
-                               if(latest < d)
-                                       latest = d;
-                       }
-                       d = now/1000 - latest; // seconds since last activity
-                       if(d < 0)
-                               str = none;
-                       else if(d < 5)
-                               str = 'Active now';
-                       else
-                               str = fmt.timeInterval(d) + ' ago';
-               }
-               setTextContent(e.last_activity_lb, str);
-
-               //
-               // error
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       str = torrents[0].getErrorString();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(str != t.getErrorString()) {
-                                       str = mixed;
-                                       break;
-                               }
-                       }
-               }
-               setTextContent(e.error_lb, str || none);
-
-               //
-               // size
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       pieces = 0;
-                       size = 0;
-                       pieceSize = torrents[0].getPieceSize();
-                       for(i=0; t=torrents[i]; ++i) {
-                               pieces += t.getPieceCount();
-                               size += t.getTotalSize();
-                               if(pieceSize != t.getPieceSize())
-                                       pieceSize = 0;
-                       }
-                       if(!size)
-                               str = none;
-                       else if(pieceSize > 0)
-                               str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
-                       else
-                               str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
-               }
-               setTextContent(e.size_lb, str);
-
-               //
-               //  hash
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       str = torrents[0].getHashString();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(str != t.getHashString()) {
-                                       str = mixed;
-                                       break;
-                               }
-                       }
-               }
-               setTextContent(e.hash_lb, str);
-
-               //
-               //  privacy
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       baseline = torrents[0].getPrivateFlag();
-                       str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(baseline != t.getPrivateFlag()) {
-                                       str = mixed;
-                                       break;
-                               }
-                       }
-               }
-               setTextContent(e.privacy_lb, str);
-
-               //
-               //  comment
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       str = torrents[0].getComment();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(str != t.getComment()) {
-                                       str = mixed;
-                                       break;
-                               }
-                       }
-               }
-               if(!str)
-                       str = none;
-               uri = parseUri(str);
-               if (uri.protocol == 'http' || uri.parseUri == 'https') {
-                       str = encodeURI(str);
-                       setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
-               }
-               else
-                       setTextContent(e.comment_lb, str);
-
-               //
-               //  origin
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       mixed_creator = false;
-                       mixed_date = false;
-                       creator = torrents[0].getCreator();
-                       date = torrents[0].getDateCreated();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(creator != t.getCreator())
-                                       mixed_creator = true;
-                               if(date != t.getDateCreated())
-                                       mixed_date = true;
-                       }
-                       var empty_creator = !creator || !creator.length,
-                           empty_date = !date;
-                       if(mixed_creator || mixed_date)
-                               str = mixed;
-                       else if(empty_creator && empty_date)
-                               str = unknown;
-                       else if(empty_date && !empty_creator)
-                               str = 'Created by ' + creator;
-                       else if(empty_creator && !empty_date)
-                               str = 'Created on ' + (new Date(date*1000)).toDateString();
-                       else
-                               str = 'Created by ' + creator + ' on ' + (new Date(date*1000)).toDateString();
-               }
-               setTextContent(e.origin_lb, str);
-
-               //
-               //  foldername
-               //
-
-               if(torrents.length < 1)
-                       str = none;
-               else {
-                       str = torrents[0].getDownloadDir();
-                       for(i=0; t=torrents[i]; ++i) {
-                               if(str != t.getDownloadDir()) {
-                                       str = mixed;
-                                       break;
-                               }
-                       }
-               }
-               setTextContent(e.foldername_lb, str);
-       },
-
-       /****
-       *****  FILES PAGE
-       ****/
-
-       changeFileCommand = function(fileIndices, command) {
-               var torrentId = data.file_torrent.getId();
-               data.controller.changeFileCommand(torrentId, fileIndices, command);
-       },
-
-       onFileWantedToggled = function(ev, fileIndices, want) {
-               changeFileCommand(fileIndices, want?'files-wanted':'files-unwanted');
-       },
-
-       onFilePriorityToggled = function(ev, fileIndices, priority) {
-               var command;
-               switch(priority) {
-                       case -1: command = 'priority-low'; break;
-                       case  1: command = 'priority-high'; break;
-                       default: command = 'priority-normal'; break;
-               }
-               changeFileCommand(fileIndices, command);
-       },
-
-       onNameClicked = function(ev, fileRow, fileIndices) {
-               $(fileRow.getElement()).siblings().slideToggle();
-       },
-
-       clearFileList = function() {
-               $(data.elements.file_list).empty();
-               delete data.file_torrent;
-               delete data.file_torrent_n;
-               delete data.file_rows;
-       },
-
-       createFileTreeModel = function (tor) {
-               var i, j, n, name, tokens, walk, tree, token, sub,
-                   leaves = [ ],
-                   tree = { children: { }, file_indices: [ ] };
-
-               n = tor.getFileCount();
-               for (i=0; i<n; ++i) {
-                       name = tor.getFile(i).name;
-                       tokens = name.split('/');
-                       walk = tree;
-                       for (j=0; j<tokens.length; ++j) {
-                               token = tokens[j];
-                               sub = walk.children[token];
-                               if (!sub) {
-                                       walk.children[token] = sub = {
-                                               name: token,
-                                               parent: walk,
-                                               children: { },
-                                               file_indices: [ ],
-                                               depth: j
-                                       };
-                               }
-                               walk = sub;
-                       }
-                       walk.file_index = i;
-                       delete walk.children;
-                       leaves.push (walk);
-               }
-
-               for (i=0; i<leaves.length; ++i) {
-                       walk = leaves[i];
-                       j = walk.file_index;
-                       do {
-                               walk.file_indices.push (j);
-                               walk = walk.parent;
-                       } while (walk);
-               }
-
-               return tree;
-       },
-
-       addNodeToView = function (tor, parent, sub, i) {
-               var row;
-               row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i%2);
-               data.file_rows.push(row);
-               parent.appendChild(row.getElement());
-               $(row).bind('wantedToggled',onFileWantedToggled);
-               $(row).bind('priorityToggled',onFilePriorityToggled);
-               $(row).bind('nameClicked',onNameClicked);
-       },
-
-       addSubtreeToView = function (tor, parent, sub, i) {
-               var key, div;
-               div = document.createElement('div');
-               if (sub.parent)
-                       addNodeToView (tor, div, sub, i++);
-               if (sub.children)
-                       for (key in sub.children)
-                               i = addSubtreeToView (tor, div, sub.children[key]);
-               parent.appendChild(div);
-               return i;
-       },
-
-       updateFilesPage = function() {
-               var i, n, tor, fragment, tree,
-                   file_list = data.elements.file_list,
-                   torrents = data.torrents;
-
-               // only show one torrent at a time
-               if (torrents.length !== 1) {
-                       clearFileList();
-                       return;
-               }
-
-               tor = torrents[0];
-               n = tor ? tor.getFileCount() : 0;
-               if (tor!=data.file_torrent || n!=data.file_torrent_n) {
-                       // rebuild the file list...
-                       clearFileList();
-                       data.file_torrent = tor;
-                       data.file_torrent_n = n;
-                       data.file_rows = [ ];
-                       fragment = document.createDocumentFragment();
-                       tree = createFileTreeModel (tor);
-                       addSubtreeToView (tor, fragment, tree, 0);
-                       file_list.appendChild (fragment);
-               } else {
-                       // ...refresh the already-existing file list
-                       for (i=0, n=data.file_rows.length; i<n; ++i)
-                               data.file_rows[i].refresh();
-               }
-       },
-
-       /****
-       *****  PEERS PAGE
-       ****/
-
-       updatePeersPage = function() {
-               var i, k, tor, peers, peer, parity,
-                   html = [],
-                   fmt = Transmission.fmt,
-                   peers_list = data.elements.peers_list,
-                   torrents = data.torrents;
-
-               for (k=0; tor=torrents[k]; ++k)
-               {
-                       peers = tor.getPeers();
-                       html.push('<div class="inspector_group">');
-                       if (torrents.length > 1) {
-                               html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
-                       }
-                       if (!peers || !peers.length) {
-                               html.push('<br></div>'); // firefox won't paint the top border if the div is empty
-                               continue;
-                       }
-                       html.push('<table class="peer_list">',
-                                 '<tr class="inspector_peer_entry even">',
-                                 '<th class="encryptedCol"></th>',
-                                 '<th class="upCol">Up</th>',
-                                 '<th class="downCol">Down</th>',
-                                 '<th class="percentCol">%</th>',
-                                 '<th class="statusCol">Status</th>',
-                                 '<th class="addressCol">Address</th>',
-                                 '<th class="clientCol">Client</th>',
-                                 '</tr>');
-                       for (i=0; peer=peers[i]; ++i) {
-                               parity = (i%2) ? 'odd' : 'even';
-                               html.push('<tr class="inspector_peer_entry ', parity, '">',
-                                         '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">'
-                                                                   : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
-                                         '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
-                                         '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
-                                         '<td class="percentCol">', Math.floor(peer.progress*100), '%', '</td>',
-                                         '<td>', fmt.peerStatus(peer.flagStr), '</td>',
-                                         '<td>', sanitizeText(peer.address), '</td>',
-                                         '<td class="clientCol">', sanitizeText(peer.clientName), '</td>',
-                                         '</tr>');
-                       }
-                       html.push('</table></div>');
-               }
-
-               setInnerHTML(peers_list, html.join(''));
-       },
-
-       /****
-       *****  TRACKERS PAGE
-       ****/
-
-       getAnnounceState = function(tracker) {
-               var timeUntilAnnounce, s = '';
-               switch (tracker.announceState) {
-                       case Torrent._TrackerActive:
-                               s = 'Announce in progress';
-                               break;
-                       case Torrent._TrackerWaiting:
-                               timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
-                               if (timeUntilAnnounce < 0) {
-                                   timeUntilAnnounce = 0;
-                               }
-                               s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
-                               break;
-                       case Torrent._TrackerQueued:
-                               s = 'Announce is queued';
-                               break;
-                       case Torrent._TrackerInactive:
-                               s = tracker.isBackup ?
-                                   'Tracker will be used as a backup' :
-                                   'Announce not scheduled';
-                               break;
-                       default:
-                               s = 'unknown announce state: ' + tracker.announceState;
-               }
-               return s;
-       },
-
-       lastAnnounceStatus = function(tracker) {
-
-               var lastAnnounceLabel = 'Last Announce',
-                   lastAnnounce = [ 'N/A' ],
-               lastAnnounceTime;
-
-               if (tracker.hasAnnounced) {
-                       lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
-                       if (tracker.lastAnnounceSucceeded) {
-                               lastAnnounce = [ lastAnnounceTime, ' (got ',  Transmission.fmt.countString('peer','peers',tracker.lastAnnouncePeerCount), ')' ];
-                       } else {
-                               lastAnnounceLabel = 'Announce error';
-                               lastAnnounce = [ (tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime ];
-                       }
-               }
-               return { 'label':lastAnnounceLabel, 'value':lastAnnounce.join('') };
-       },
-
-       lastScrapeStatus = function(tracker) {
-
-               var lastScrapeLabel = 'Last Scrape',
-                   lastScrape = 'N/A',
-               lastScrapeTime;
-
-               if (tracker.hasScraped) {
-                       lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
-                       if (tracker.lastScrapeSucceeded) {
-                               lastScrape = lastScrapeTime;
-                       } else {
-                               lastScrapeLabel = 'Scrape error';
-                               lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
-                       }
-               }
-               return {'label':lastScrapeLabel, 'value':lastScrape};
-       },
-
-       updateTrackersPage = function() {
-               var i, j, tier, tracker, trackers, tor,
-                   html, parity, lastAnnounceStatusHash,
-                   announceState, lastScrapeStatusHash,
-                   na = 'N/A',
-                   trackers_list = data.elements.trackers_list,
-                   torrents = data.torrents;
-
-               // By building up the HTML as as string, then have the browser
-               // turn this into a DOM tree, this is a fast operation.
-               html = [];
-               for (i=0; tor=torrents[i]; ++i)
-               {
-                       html.push ('<div class="inspector_group">');
-
-                       if (torrents.length > 1)
-                               html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
-
-                       tier = -1;
-                       trackers = tor.getTrackers();
-                       for (j=0; tracker=trackers[j]; ++j)
-                       {
-                               if (tier != tracker.tier)
-                               {
-                                       if (tier !== -1) // close previous tier
-                                               html.push('</ul></div>');
-
-                                       tier = tracker.tier;
-
-                                       html.push('<div class="inspector_group_label">',
-                                                 'Tier ', tier+1, '</div>',
-                                                 '<ul class="tier_list">');
-                               }
-
-                               // Display construction
-                               lastAnnounceStatusHash = lastAnnounceStatus(tracker);
-                               announceState = getAnnounceState(tracker);
-                               lastScrapeStatusHash = lastScrapeStatus(tracker);
-                               parity = (j%2) ? 'odd' : 'even';
-                               html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', sanitizeText(tracker.announce), '">',
-                                         sanitizeText(tracker.host || tracker.announce), '</div>',
-                                         '<div class="tracker_activity">',
-                                         '<div>', lastAnnounceStatusHash['label'], ': ', lastAnnounceStatusHash['value'], '</div>',
-                                         '<div>', announceState, '</div>',
-                                         '<div>', lastScrapeStatusHash['label'], ': ', lastScrapeStatusHash['value'], '</div>',
-                                         '</div><table class="tracker_stats">',
-                                         '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
-                                         '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
-                                         '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
-                                         '</table></li>');
-                       }
-                       if (tier !== -1) // close last tier
-                               html.push('</ul></div>');
-
-                       html.push('</div>'); // inspector_group
-               }
-
-               setInnerHTML (trackers_list, html.join(''));
-       },
-
-       initialize = function (controller) {
-
-               var ti = '#torrent_inspector_';
-
-               data.controller = controller;
-
-               $('.inspector-tab').click(onTabClicked);
-
-               data.elements.info_page      = $('#inspector-page-info')[0];
-               data.elements.files_page     = $('#inspector-page-files')[0];
-               data.elements.peers_page     = $('#inspector-page-peers')[0];
-               data.elements.trackers_page  = $('#inspector-page-trackers')[0];
-
-               data.elements.file_list      = $('#inspector_file_list')[0];
-               data.elements.peers_list     = $('#inspector_peers_list')[0];
-               data.elements.trackers_list  = $('#inspector_trackers_list')[0];
-
-               data.elements.have_lb           = $('#inspector-info-have')[0];
-               data.elements.availability_lb   = $('#inspector-info-availability')[0];
-               data.elements.downloaded_lb     = $('#inspector-info-downloaded')[0];
-               data.elements.uploaded_lb       = $('#inspector-info-uploaded')[0];
-               data.elements.state_lb          = $('#inspector-info-state')[0];
-               data.elements.running_time_lb   = $('#inspector-info-running-time')[0];
-               data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
-               data.elements.last_activity_lb  = $('#inspector-info-last-activity')[0];
-               data.elements.error_lb          = $('#inspector-info-error')[0];
-               data.elements.size_lb           = $('#inspector-info-size')[0];
-               data.elements.foldername_lb     = $('#inspector-info-location')[0];
-               data.elements.hash_lb           = $('#inspector-info-hash')[0];
-               data.elements.privacy_lb        = $('#inspector-info-privacy')[0];
-               data.elements.origin_lb         = $('#inspector-info-origin')[0];
-               data.elements.comment_lb        = $('#inspector-info-comment')[0];
-               data.elements.name_lb           = $('#torrent_inspector_name')[0];
-
-               // force initial 'N/A' updates on all the pages
-               updateInspector();
-               updateInfoPage();
-               updatePeersPage();
-               updateTrackersPage();
-               updateFilesPage();
-       };
-
-       /****
-       *****  PUBLIC FUNCTIONS
-       ****/
-
-       this.setTorrents = function (torrents) {
-               var d = data;
-
-               // update the inspector when a selected torrent's data changes.
-               $(d.torrents).unbind('dataChanged.inspector');
-               $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector,this));
-               d.torrents = torrents;
-
-               // periodically ask for updates to the inspector's torrents
-               clearInterval(d.refreshInterval);
-               d.refreshInterval = setInterval($.proxy(refreshTorrents,this), 2000);
-               refreshTorrents();
-
-               // refresh the inspector's UI
-               updateInspector();
-       };
-
-       initialize (controller);
+    var data = {
+            controller: null,
+            elements: {},
+            torrents: []
+        },
+
+        needsExtraInfo = function (torrents) {
+            var i, id, tor;
+
+            for (i = 0; tor = torrents[i]; i++)
+                if (!tor.hasExtraInfo())
+                    return true;
+
+            return false;
+        },
+
+        refreshTorrents = function () {
+            var fields,
+                ids = $.map(data.torrents.slice(0), function (t) {
+                    return t.getId();
+                });
+
+            if (ids && ids.length) {
+                fields = ['id'].concat(Torrent.Fields.StatsExtra);
+
+                if (needsExtraInfo(data.torrents)) {
+                    $.merge(fields, Torrent.Fields.InfoExtra);
+                }
+
+                data.controller.updateTorrents(ids, fields);
+            }
+        },
+
+        onTabClicked = function (ev) {
+            var tab = ev.currentTarget;
+
+            if (isMobileDevice) {
+                ev.stopPropagation();
+            }
+
+            // select this tab and deselect the others
+            $(tab).addClass('selected').siblings().removeClass('selected');
+
+            // show this tab and hide the others
+            $('#' + tab.id.replace('tab', 'page')).show().siblings('.inspector-page').hide();
+
+            updateInspector();
+        },
+
+        updateInspector = function () {
+            var e = data.elements,
+                torrents = data.torrents,
+                name;
+
+            // update the name, which is shown on all the pages
+            if (!torrents || !torrents.length) {
+                name = 'No Selection';
+            } else if (torrents.length === 1) {
+                name = torrents[0].getName();
+            } else {
+                name = '' + torrents.length + ' Transfers Selected';
+            }
+            setTextContent(e.name_lb, name || na);
+
+            // update the visible page
+            if ($(e.info_page).is(':visible')) {
+                updateInfoPage();
+            } else if ($(e.peers_page).is(':visible')) {
+                updatePeersPage();
+            } else if ($(e.trackers_page).is(':visible')) {
+                updateTrackersPage();
+            } else if ($(e.files_page).is(':visible')) {
+                updateFilesPage();
+            }
+        },
+
+        /****
+         *****  GENERAL INFO PAGE
+         ****/
+
+        updateInfoPage = function () {
+            var torrents = data.torrents,
+                e = data.elements,
+                fmt = Transmission.fmt,
+                none = 'None',
+                mixed = 'Mixed',
+                unknown = 'Unknown',
+                isMixed, allPaused, allFinished,
+                str,
+                baseline, it, s, i, t,
+                sizeWhenDone = 0,
+                leftUntilDone = 0,
+                available = 0,
+                haveVerified = 0,
+                haveUnverified = 0,
+                verifiedPieces = 0,
+                stateString,
+                latest,
+                pieces,
+                size,
+                pieceSize,
+                creator, mixed_creator,
+                date, mixed_date,
+                v, u, f, d, pct,
+                uri,
+                now = Date.now();
+
+            //
+            //  state_lb
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                isMixed = false;
+                allPaused = true;
+                allFinished = true;
+
+                baseline = torrents[0].getStatus();
+                for (i = 0; t = torrents[i]; ++i) {
+                    it = t.getStatus();
+                    if (it != baseline) {
+                        isMixed = true;
+                    }
+                    if (!t.isStopped()) {
+                        allPaused = allFinished = false;
+                    }
+                    if (!t.isFinished()) {
+                        allFinished = false;
+                    }
+                }
+                if (isMixed) {
+                    str = mixed;
+                } else if (allFinished) {
+                    str = 'Finished';
+                } else if (allPaused) {
+                    str = 'Paused';
+                } else {
+                    str = torrents[0].getStateString();
+                }
+            }
+            setTextContent(e.state_lb, str);
+            stateString = str;
+
+            //
+            //  have_lb
+            //
+
+            if (torrents.length < 1)
+                str = none;
+            else {
+                baseline = torrents[0].getStatus();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (!t.needsMetaData()) {
+                        haveUnverified += t.getHaveUnchecked();
+                        v = t.getHaveValid();
+                        haveVerified += v;
+                        if (t.getPieceSize()) {
+                            verifiedPieces += v / t.getPieceSize();
+                        }
+                        sizeWhenDone += t.getSizeWhenDone();
+                        leftUntilDone += t.getLeftUntilDone();
+                        available += (t.getHave()) + t.getDesiredAvailable();
+                    }
+                }
+
+                d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
+                str = fmt.percentString(d);
+
+                if (!haveUnverified && !leftUntilDone) {
+                    str = fmt.size(haveVerified) + ' (100%)';
+                } else if (!haveUnverified) {
+                    str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%)';
+                } else {
+                    str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%), ' + fmt.size(haveUnverified) + ' Unverified';
+                }
+            }
+            setTextContent(e.have_lb, str);
+
+            //
+            //  availability_lb
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else if (sizeWhenDone == 0) {
+                str = none;
+            } else {
+                str = '' + fmt.percentString((100.0 * available) / sizeWhenDone) + '%';
+            };
+            setTextContent(e.availability_lb, str);
+
+            //
+            //  downloaded_lb
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                d = f = 0;
+                for (i = 0; t = torrents[i]; ++i) {
+                    d += t.getDownloadedEver();
+                    f += t.getFailedEver();
+                };
+                if (f) {
+                    str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
+                } else {
+                    str = fmt.size(d);
+                };
+            };
+            setTextContent(e.downloaded_lb, str);
+
+            //
+            //  uploaded_lb
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                d = u = 0;
+                if (torrents.length == 1) {
+                    d = torrents[0].getDownloadedEver();
+                    u = torrents[0].getUploadedEver();
+
+                    if (d == 0) {
+                        d = torrents[0].getHaveValid();
+                    };
+                } else {
+                    for (i = 0; t = torrents[i]; ++i) {
+                        d += t.getDownloadedEver();
+                        u += t.getUploadedEver();
+                    };
+                };
+                str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString(Math.ratio(u, d)) + ')';
+            };
+            setTextContent(e.uploaded_lb, str);
+
+            //
+            // running time
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                allPaused = true;
+                baseline = torrents[0].getStartDate();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (baseline != t.getStartDate()) {
+                        baseline = 0;
+                    }
+                    if (!t.isStopped()) {
+                        allPaused = false;
+                    }
+                }
+                if (allPaused) {
+                    str = stateString; // paused || finished}
+                } else if (!baseline) {
+                    str = mixed;
+                } else {
+                    str = fmt.timeInterval(now / 1000 - baseline);
+                }
+            };
+
+            setTextContent(e.running_time_lb, str);
+
+            //
+            // remaining time
+            //
+
+            str = '';
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                baseline = torrents[0].getETA();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (baseline != t.getETA()) {
+                        str = mixed;
+                        break;
+                    }
+                }
+            }
+            if (!str.length) {
+                if (baseline < 0) {
+                    str = unknown;
+                } else {
+                    str = fmt.timeInterval(baseline);
+                }
+            }
+            setTextContent(e.remaining_time_lb, str);
+
+            //
+            // last activity
+            //
+
+            latest = -1;
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                baseline = torrents[0].getLastActivity();
+                for (i = 0; t = torrents[i]; ++i) {
+                    d = t.getLastActivity();
+                    if (latest < d) {
+                        latest = d;
+                    };
+                };
+                d = now / 1000 - latest; // seconds since last activity
+                if (d < 0) {
+                    str = none;
+                } else if (d < 5) {
+                    str = 'Active now';
+                } else {
+                    str = fmt.timeInterval(d) + ' ago';
+                };
+            };
+            setTextContent(e.last_activity_lb, str);
+
+            //
+            // error
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                str = torrents[0].getErrorString();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (str != t.getErrorString()) {
+                        str = mixed;
+                        break;
+                    };
+                };
+            };
+            setTextContent(e.error_lb, str || none);
+
+            //
+            // size
+            //
+
+            if (torrents.length < 1) {
+                {
+                    str = none;
+                };
+            } else {
+                pieces = 0;
+                size = 0;
+                pieceSize = torrents[0].getPieceSize();
+                for (i = 0; t = torrents[i]; ++i) {
+                    pieces += t.getPieceCount();
+                    size += t.getTotalSize();
+                    if (pieceSize != t.getPieceSize()) {
+                        pieceSize = 0;
+                    }
+                };
+                if (!size) {
+                    str = none;
+                } else if (pieceSize > 0) {
+                    str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
+                } else {
+                    str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
+                };
+            };
+            setTextContent(e.size_lb, str);
+
+            //
+            //  hash
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                str = torrents[0].getHashString();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (str != t.getHashString()) {
+                        str = mixed;
+                        break;
+                    };
+                };
+            };
+            setTextContent(e.hash_lb, str);
+
+            //
+            //  privacy
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                baseline = torrents[0].getPrivateFlag();
+                str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (baseline != t.getPrivateFlag()) {
+                        str = mixed;
+                        break;
+                    };
+                };
+            };
+            setTextContent(e.privacy_lb, str);
+
+            //
+            //  comment
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                str = torrents[0].getComment();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (str != t.getComment()) {
+                        str = mixed;
+                        break;
+                    };
+                };
+            };
+            if (!str) {
+                str = none;
+            }
+            uri = parseUri(str);
+            if (uri.protocol == 'http' || uri.parseUri == 'https') {
+                str = encodeURI(str);
+                setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
+            } else {
+                setTextContent(e.comment_lb, str);
+            };
+
+            //
+            //  origin
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                mixed_creator = false;
+                mixed_date = false;
+                creator = torrents[0].getCreator();
+                date = torrents[0].getDateCreated();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (creator != t.getCreator()) {
+                        mixed_creator = true;
+                    };
+                    if (date != t.getDateCreated()) {
+                        mixed_date = true;
+                    };
+                };
+                var empty_creator = !creator || !creator.length;
+                var empty_date = !date;
+                if (mixed_creator || mixed_date) {
+                    str = mixed;
+                } else if (empty_creator && empty_date) {
+                    str = unknown;
+                } else if (empty_date && !empty_creator) {
+                    str = 'Created by ' + creator;
+                } else if (empty_creator && !empty_date) {
+                    str = 'Created on ' + (new Date(date * 1000)).toDateString();
+                } else {
+                    str = 'Created by ' + creator + ' on ' + (new Date(date * 1000)).toDateString();
+                };
+            };
+            setTextContent(e.origin_lb, str);
+
+            //
+            //  foldername
+            //
+
+            if (torrents.length < 1) {
+                str = none;
+            } else {
+                str = torrents[0].getDownloadDir();
+                for (i = 0; t = torrents[i]; ++i) {
+                    if (str != t.getDownloadDir()) {
+                        str = mixed;
+                        break;
+                    };
+                };
+            };
+            setTextContent(e.foldername_lb, str);
+        },
+
+        /****
+         *****  FILES PAGE
+         ****/
+
+        changeFileCommand = function (fileIndices, command) {
+            var torrentId = data.file_torrent.getId();
+            data.controller.changeFileCommand(torrentId, fileIndices, command);
+        },
+
+        onFileWantedToggled = function (ev, fileIndices, want) {
+            changeFileCommand(fileIndices, want ? 'files-wanted' : 'files-unwanted');
+        },
+
+        onFilePriorityToggled = function (ev, fileIndices, priority) {
+            var command;
+            switch (priority) {
+            case -1:
+                command = 'priority-low';
+                break;
+            case 1:
+                command = 'priority-high';
+                break;
+            default:
+                command = 'priority-normal';
+                break;
+            }
+            changeFileCommand(fileIndices, command);
+        },
+
+        onNameClicked = function (ev, fileRow, fileIndices) {
+            $(fileRow.getElement()).siblings().slideToggle();
+        },
+
+        clearFileList = function () {
+            $(data.elements.file_list).empty();
+            delete data.file_torrent;
+            delete data.file_torrent_n;
+            delete data.file_rows;
+        },
+
+        createFileTreeModel = function (tor) {
+            var i, j, n, name, tokens, walk, tree, token, sub,
+                leaves = [],
+                tree = {
+                    children: {},
+                    file_indices: []
+                };
+
+            n = tor.getFileCount();
+            for (i = 0; i < n; ++i) {
+                name = tor.getFile(i).name;
+                tokens = name.split('/');
+                walk = tree;
+                for (j = 0; j < tokens.length; ++j) {
+                    token = tokens[j];
+                    sub = walk.children[token];
+                    if (!sub) {
+                        walk.children[token] = sub = {
+                            name: token,
+                            parent: walk,
+                            children: {},
+                            file_indices: [],
+                            depth: j
+                        };
+                    }
+                    walk = sub;
+                }
+                walk.file_index = i;
+                delete walk.children;
+                leaves.push(walk);
+            }
+
+            for (i = 0; i < leaves.length; ++i) {
+                walk = leaves[i];
+                j = walk.file_index;
+                do {
+                    walk.file_indices.push(j);
+                    walk = walk.parent;
+                } while (walk);
+            }
+
+            return tree;
+        },
+
+        addNodeToView = function (tor, parent, sub, i) {
+            var row;
+            row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i % 2);
+            data.file_rows.push(row);
+            parent.appendChild(row.getElement());
+            $(row).bind('wantedToggled', onFileWantedToggled);
+            $(row).bind('priorityToggled', onFilePriorityToggled);
+            $(row).bind('nameClicked', onNameClicked);
+        },
+
+        addSubtreeToView = function (tor, parent, sub, i) {
+            var key, div;
+            div = document.createElement('div');
+            if (sub.parent) {
+                addNodeToView(tor, div, sub, i++);
+            }
+            if (sub.children) {
+                for (key in sub.children) {
+                    i = addSubtreeToView(tor, div, sub.children[key]);
+                }
+            }
+            parent.appendChild(div);
+            return i;
+        },
+
+        updateFilesPage = function () {
+            var i, n, tor, fragment, tree,
+                file_list = data.elements.file_list,
+                torrents = data.torrents;
+
+            // only show one torrent at a time
+            if (torrents.length !== 1) {
+                clearFileList();
+                return;
+            }
+
+            tor = torrents[0];
+            n = tor ? tor.getFileCount() : 0;
+            if (tor != data.file_torrent || n != data.file_torrent_n) {
+                // rebuild the file list...
+                clearFileList();
+                data.file_torrent = tor;
+                data.file_torrent_n = n;
+                data.file_rows = [];
+                fragment = document.createDocumentFragment();
+                tree = createFileTreeModel(tor);
+                addSubtreeToView(tor, fragment, tree, 0);
+                file_list.appendChild(fragment);
+            } else {
+                // ...refresh the already-existing file list
+                for (i = 0, n = data.file_rows.length; i < n; ++i)
+                    data.file_rows[i].refresh();
+            }
+        },
+
+        /****
+         *****  PEERS PAGE
+         ****/
+
+        updatePeersPage = function () {
+            var i, k, tor, peers, peer, parity,
+                html = [],
+                fmt = Transmission.fmt,
+                peers_list = data.elements.peers_list,
+                torrents = data.torrents;
+
+            for (k = 0; tor = torrents[k]; ++k) {
+                peers = tor.getPeers();
+                html.push('<div class="inspector_group">');
+                if (torrents.length > 1) {
+                    html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
+                }
+                if (!peers || !peers.length) {
+                    html.push('<br></div>'); // firefox won't paint the top border if the div is empty
+                    continue;
+                }
+                html.push('<table class="peer_list">',
+                    '<tr class="inspector_peer_entry even">',
+                    '<th class="encryptedCol"></th>',
+                    '<th class="upCol">Up</th>',
+                    '<th class="downCol">Down</th>',
+                    '<th class="percentCol">%</th>',
+                    '<th class="statusCol">Status</th>',
+                    '<th class="addressCol">Address</th>',
+                    '<th class="clientCol">Client</th>',
+                    '</tr>');
+                for (i = 0; peer = peers[i]; ++i) {
+                    parity = (i % 2) ? 'odd' : 'even';
+                    html.push('<tr class="inspector_peer_entry ', parity, '">',
+                        '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">' : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
+                        '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
+                        '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
+                        '<td class="percentCol">', Math.floor(peer.progress * 100), '%', '</td>',
+                        '<td>', fmt.peerStatus(peer.flagStr), '</td>',
+                        '<td>', sanitizeText(peer.address), '</td>',
+                        '<td class="clientCol">', sanitizeText(peer.clientName), '</td>',
+                        '</tr>');
+                }
+                html.push('</table></div>');
+            }
+
+            setInnerHTML(peers_list, html.join(''));
+        },
+
+        /****
+         *****  TRACKERS PAGE
+         ****/
+
+        getAnnounceState = function (tracker) {
+            var timeUntilAnnounce, s = '';
+            switch (tracker.announceState) {
+            case Torrent._TrackerActive:
+                s = 'Announce in progress';
+                break;
+            case Torrent._TrackerWaiting:
+                timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
+                if (timeUntilAnnounce < 0) {
+                    timeUntilAnnounce = 0;
+                }
+                s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
+                break;
+            case Torrent._TrackerQueued:
+                s = 'Announce is queued';
+                break;
+            case Torrent._TrackerInactive:
+                s = tracker.isBackup ?
+                    'Tracker will be used as a backup' :
+                    'Announce not scheduled';
+                break;
+            default:
+                s = 'unknown announce state: ' + tracker.announceState;
+            }
+            return s;
+        },
+
+        lastAnnounceStatus = function (tracker) {
+
+            var lastAnnounceLabel = 'Last Announce',
+                lastAnnounce = ['N/A'],
+                lastAnnounceTime;
+
+            if (tracker.hasAnnounced) {
+                lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
+                if (tracker.lastAnnounceSucceeded) {
+                    lastAnnounce = [lastAnnounceTime, ' (got ', Transmission.fmt.countString('peer', 'peers', tracker.lastAnnouncePeerCount), ')'];
+                } else {
+                    lastAnnounceLabel = 'Announce error';
+                    lastAnnounce = [(tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime];
+                }
+            }
+            return {
+                'label': lastAnnounceLabel,
+                'value': lastAnnounce.join('')
+            };
+        },
+
+        lastScrapeStatus = function (tracker) {
+
+            var lastScrapeLabel = 'Last Scrape',
+                lastScrape = 'N/A',
+                lastScrapeTime;
+
+            if (tracker.hasScraped) {
+                lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
+                if (tracker.lastScrapeSucceeded) {
+                    lastScrape = lastScrapeTime;
+                } else {
+                    lastScrapeLabel = 'Scrape error';
+                    lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
+                }
+            }
+            return {
+                'label': lastScrapeLabel,
+                'value': lastScrape
+            };
+        },
+
+        updateTrackersPage = function () {
+            var i, j, tier, tracker, trackers, tor,
+                html, parity, lastAnnounceStatusHash,
+                announceState, lastScrapeStatusHash,
+                na = 'N/A',
+                trackers_list = data.elements.trackers_list,
+                torrents = data.torrents;
+
+            // By building up the HTML as as string, then have the browser
+            // turn this into a DOM tree, this is a fast operation.
+            html = [];
+            for (i = 0; tor = torrents[i]; ++i) {
+                html.push('<div class="inspector_group">');
+
+                if (torrents.length > 1) {
+                    html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
+                }
+
+                tier = -1;
+                trackers = tor.getTrackers();
+                for (j = 0; tracker = trackers[j]; ++j) {
+                    if (tier != tracker.tier) {
+                        if (tier !== -1) { // close previous tier
+                            html.push('</ul></div>');
+                        }
+
+                        tier = tracker.tier;
+
+                        html.push('<div class="inspector_group_label">',
+                            'Tier ', tier + 1, '</div>',
+                            '<ul class="tier_list">');
+                    }
+
+                    // Display construction
+                    lastAnnounceStatusHash = lastAnnounceStatus(tracker);
+                    announceState = getAnnounceState(tracker);
+                    lastScrapeStatusHash = lastScrapeStatus(tracker);
+                    parity = (j % 2) ? 'odd' : 'even';
+                    html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', sanitizeText(tracker.announce), '">',
+                        sanitizeText(tracker.host || tracker.announce), '</div>',
+                        '<div class="tracker_activity">',
+                        '<div>', lastAnnounceStatusHash['label'], ': ', lastAnnounceStatusHash['value'], '</div>',
+                        '<div>', announceState, '</div>',
+                        '<div>', lastScrapeStatusHash['label'], ': ', lastScrapeStatusHash['value'], '</div>',
+                        '</div><table class="tracker_stats">',
+                        '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
+                        '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
+                        '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
+                        '</table></li>');
+                }
+                if (tier !== -1) { // close last tier
+                    html.push('</ul></div>');
+                }
+
+                html.push('</div>'); // inspector_group
+            }
+
+            setInnerHTML(trackers_list, html.join(''));
+        },
+
+        initialize = function (controller) {
+
+            var ti = '#torrent_inspector_';
+
+            data.controller = controller;
+
+            $('.inspector-tab').click(onTabClicked);
+
+            data.elements.info_page = $('#inspector-page-info')[0];
+            data.elements.files_page = $('#inspector-page-files')[0];
+            data.elements.peers_page = $('#inspector-page-peers')[0];
+            data.elements.trackers_page = $('#inspector-page-trackers')[0];
+
+            data.elements.file_list = $('#inspector_file_list')[0];
+            data.elements.peers_list = $('#inspector_peers_list')[0];
+            data.elements.trackers_list = $('#inspector_trackers_list')[0];
+
+            data.elements.have_lb = $('#inspector-info-have')[0];
+            data.elements.availability_lb = $('#inspector-info-availability')[0];
+            data.elements.downloaded_lb = $('#inspector-info-downloaded')[0];
+            data.elements.uploaded_lb = $('#inspector-info-uploaded')[0];
+            data.elements.state_lb = $('#inspector-info-state')[0];
+            data.elements.running_time_lb = $('#inspector-info-running-time')[0];
+            data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
+            data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
+            data.elements.error_lb = $('#inspector-info-error')[0];
+            data.elements.size_lb = $('#inspector-info-size')[0];
+            data.elements.foldername_lb = $('#inspector-info-location')[0];
+            data.elements.hash_lb = $('#inspector-info-hash')[0];
+            data.elements.privacy_lb = $('#inspector-info-privacy')[0];
+            data.elements.origin_lb = $('#inspector-info-origin')[0];
+            data.elements.comment_lb = $('#inspector-info-comment')[0];
+            data.elements.name_lb = $('#torrent_inspector_name')[0];
+
+            // force initial 'N/A' updates on all the pages
+            updateInspector();
+            updateInfoPage();
+            updatePeersPage();
+            updateTrackersPage();
+            updateFilesPage();
+        };
+
+    /****
+     *****  PUBLIC FUNCTIONS
+     ****/
+
+    this.setTorrents = function (torrents) {
+        var d = data;
+
+        // update the inspector when a selected torrent's data changes.
+        $(d.torrents).unbind('dataChanged.inspector');
+        $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector, this));
+        d.torrents = torrents;
+
+        // periodically ask for updates to the inspector's torrents
+        clearInterval(d.refreshInterval);
+        d.refreshInterval = setInterval($.proxy(refreshTorrents, this), 2000);
+        refreshTorrents();
+
+        // refresh the inspector's UI
+        updateInspector();
+    };
+
+    initialize(controller);
 };
index 50a4453a3dc96af9daac2d5bee52cc0aa3f9e287..60dfbf2bb4ba867504fd99df8e121f558de130e8 100644 (file)
@@ -1,42 +1,42 @@
 var Notifications = {};
 
 $(document).ready(function () {
-       if (!window.webkitNotifications) {
-               return;
-       }
+    if (!window.webkitNotifications) {
+        return;
+    };
 
-       var notificationsEnabled = (window.webkitNotifications.checkPermission() === 0),
-           toggle = $('#toggle_notifications');
+    var notificationsEnabled = (window.webkitNotifications.checkPermission() === 0)
+    var toggle = $('#toggle_notifications');
 
-       toggle.show();
-       updateMenuTitle();
-       $(transmission).bind('downloadComplete seedingComplete', function (event, torrent) {
-               if (notificationsEnabled) {
-               var title = (event.type == 'downloadComplete' ? 'Download' : 'Seeding') + ' complete',
-                       content = torrent.getName(),
-                       notification;
+    toggle.show();
+    updateMenuTitle();
+    $(transmission).bind('downloadComplete seedingComplete', function (event, torrent) {
+        if (notificationsEnabled) {
+            var title = (event.type == 'downloadComplete' ? 'Download' : 'Seeding') + ' complete',
+                content = torrent.getName(),
+                notification;
 
-               notification = window.webkitNotifications.createNotification('style/transmission/images/logo.png', title, content);
-               notification.show();
-               setTimeout(function () {
-                 notification.cancel();
-               }, 5000);
-       };
-       });
+            notification = window.webkitNotifications.createNotification('style/transmission/images/logo.png', title, content);
+            notification.show();
+            setTimeout(function () {
+                notification.cancel();
+            }, 5000);
+        };
+    });
 
-       function updateMenuTitle() {
-               toggle.html((notificationsEnabled ? 'Disable' : 'Enable') + ' Notifications');
-       }
+    function updateMenuTitle() {
+        toggle.html((notificationsEnabled ? 'Disable' : 'Enable') + ' Notifications');
+    };
 
-       Notifications.toggle = function () {
-               if (window.webkitNotifications.checkPermission() !== 0) {
-                       window.webkitNotifications.requestPermission(function () {
-                               notificationsEnabled = (window.webkitNotifications.checkPermission() === 0);
-                               updateMenuTitle();
-                       });
-               } else {
-                       notificationsEnabled = !notificationsEnabled;
-                       updateMenuTitle();
-               }
-       };
+    Notifications.toggle = function () {
+        if (window.webkitNotifications.checkPermission() !== 0) {
+            window.webkitNotifications.requestPermission(function () {
+                notificationsEnabled = (window.webkitNotifications.checkPermission() === 0);
+                updateMenuTitle();
+            });
+        } else {
+            notificationsEnabled = !notificationsEnabled;
+            updateMenuTitle();
+        };
+    };
 });
index 3261e0cf9363d2a606c0c9f86b69ef82daa7fb85..1db6566b1e5b4172a0bcb67d4f693e44b290ab6f 100644 (file)
 
 function PrefsDialog(remote) {
 
-       var data = {
-           dialog: null,
-           remote: null,
-           elements: { },
-
-           // all the RPC session keys that we have gui controls for
-           keys: [
-               'alt-speed-down',
-               'alt-speed-time-begin',
-               'alt-speed-time-day',
-               'alt-speed-time-enabled',
-               'alt-speed-time-end',
-               'alt-speed-up',
-               'blocklist-enabled',
-               'blocklist-size',
-               'blocklist-url',
-               'dht-enabled',
-               'download-dir',
-               'encryption',
-               'idle-seeding-limit',
-               'idle-seeding-limit-enabled',
-               'lpd-enabled',
-               'peer-limit-global',
-               'peer-limit-per-torrent',
-               'peer-port',
-               'peer-port-random-on-start',
-               'pex-enabled',
-               'port-forwarding-enabled',
-               'rename-partial-files',
-               'seedRatioLimit',
-               'seedRatioLimited',
-               'speed-limit-down',
-               'speed-limit-down-enabled',
-               'speed-limit-up',
-               'speed-limit-up-enabled',
-               'start-added-torrents',
-               'utp-enabled'
-           ],
-
-           // map of keys that are enabled only if a 'parent' key is enabled
-           groups: {
-               'alt-speed-time-enabled': ['alt-speed-time-begin',
-                                          'alt-speed-time-day',
-                                          'alt-speed-time-end' ],
-               'blocklist-enabled': ['blocklist-url',
-                                     'blocklist-update-button' ],
-               'idle-seeding-limit-enabled': [ 'idle-seeding-limit' ],
-               'seedRatioLimited': [ 'seedRatioLimit' ],
-               'speed-limit-down-enabled': [ 'speed-limit-down' ],
-               'speed-limit-up-enabled': [ 'speed-limit-up' ]
-           }
-       },
-
-       initTimeDropDown = function(e)
-       {
-               var i, hour, mins, value, content;
-
-               for (i=0; i<24*4; ++i) {
-                       hour = parseInt(i/4, 10);
-                       mins = ((i%4) * 15);
-                       value = i * 15;
-                       content = hour + ':' + (mins || '00');
-                       e.options[i] = new Option(content, value);
-               }
-       },
-
-       onPortChecked = function(response)
-       {
-               var is_open = response['arguments']['port-is-open'],
-                   text = 'Port is <b>' + (is_open ? 'Open' : 'Closed') + '</b>',
-                   e = data.elements.root.find('#port-label');
-               setInnerHTML(e[0],text);
-       },
-
-       setGroupEnabled = function(parent_key, enabled)
-       {
-               var i, key, keys, root;
-
-               if (parent_key in data.groups)
-               {
-                       root = data.elements.root,
-                       keys = data.groups[parent_key];
-
-                       for (i=0; key=keys[i]; ++i)
-                               root.find('#'+key).attr('disabled',!enabled);
-               }
-       },
-
-       onBlocklistUpdateClicked = function ()
-       {
-               data.remote.updateBlocklist();
-               setBlocklistButtonEnabled(false);
-       },
-       setBlocklistButtonEnabled = function(b)
-       {
-               var e = data.elements.blocklist_button;
-               e.attr('disabled',!b);
-               e.val(b ? 'Update' : 'Updating...');
-       },
-
-       getValue = function(e)
-       {
-               var str;
-
-               switch (e[0].type)
-               {
-                       case 'checkbox':
-                       case 'radio':
-                               return e.prop('checked');
-
-                       case 'text':
-                       case 'url':
-                       case 'email':
-                       case 'number':
-                       case 'search':
-                       case 'select-one':
-                               str = e.val();
-                               if( parseInt(str,10).toString() === str)
-                                       return parseInt(str,10);
-                               if( parseFloat(str).toString() === str)
-                                       return parseFloat(str);
-                               return str;
-
-                       default:
-                               return null;
-               }
-       },
-
-       /* this callback is for controls whose changes can be applied
-          immediately, like checkboxs, radioboxes, and selects */
-       onControlChanged = function(ev)
-       {
-               var o = {};
-               o[ev.target.id] = getValue($(ev.target));
-               data.remote.savePrefs(o);
-       },
-
-       /* these two callbacks are for controls whose changes can't be applied
-          immediately -- like a text entry field -- because it takes many
-          change events for the user to get to the desired result */
-       onControlFocused  = function(ev)
-       {
-               data.oldValue = getValue($(ev.target));
-       },
-       onControlBlurred  = function(ev)
-       {
-               var newValue = getValue($(ev.target));
-               if (newValue !== data.oldValue)
-               {
-                       var o = {};
-                       o[ev.target.id] = newValue;
-                       data.remote.savePrefs(o);
-                       delete data.oldValue;
-               }
-       },
-
-       getDefaultMobileOptions = function()
-       {
-               return {
-                       width: $(window).width(),
-                       height: $(window).height(),
-                       position: [ 'left', 'top' ]
-               };
-       },
-
-       initialize = function (remote)
-       {
-               var i, key, e, o;
-
-               data.remote = remote;
-
-               e = $('#prefs-dialog');
-               data.elements.root = e;
-
-               initTimeDropDown(e.find('#alt-speed-time-begin')[0]);
-               initTimeDropDown(e.find('#alt-speed-time-end')[0]);
-
-               o = isMobileDevice
-                 ? getDefaultMobileOptions()
-                 : { width: 350, height: 400 };
-               o.autoOpen = false;
-               o.show = o.hide = 'fade';
-               o.close = onDialogClosed;
-               e.tabbedDialog(o);
-
-               e = e.find('#blocklist-update-button');
-               data.elements.blocklist_button = e;
-               e.click(onBlocklistUpdateClicked);
-
-               // listen for user input
-               for (i=0; key=data.keys[i]; ++i)
-               {
-                       e = data.elements.root.find('#'+key);
-                       switch (e[0].type)
-                       {
-                               case 'checkbox':
-                               case 'radio':
-                               case 'select-one':
-                                       e.change(onControlChanged);
-                                       break;
-
-                               case 'text':
-                               case 'url':
-                               case 'email':
-                               case 'number':
-                               case 'search':
-                                       e.focus(onControlFocused);
-                                       e.blur(onControlBlurred);
-
-                               default:
-                                       break;
-                       }
-               }
-       },
-
-       getValues = function()
-       {
-               var i, key, val, o={},
-                   keys = data.keys,
-                   root = data.elements.root;
-
-               for (i=0; key=keys[i]; ++i) {
-                       val = getValue(root.find('#'+key));
-                       if (val !== null)
-                               o[key] = val;
-               }
-
-               return o;
-       },
-
-       onDialogClosed = function()
-       {
-               transmission.hideMobileAddressbar();
-
-               $(data.dialog).trigger('closed', getValues());
-       };
-
-       /****
-       *****  PUBLIC FUNCTIONS
-       ****/
-
-       // update the dialog's controls
-       this.set = function (o)
-       {
-               var e, i, key, val, option,
-                   keys = data.keys,
-                   root = data.elements.root;
-
-               setBlocklistButtonEnabled(true);
-
-               for (i=0; key=keys[i]; ++i)
-               {
-                       val = o[key];
-                       e = root.find('#'+key);
-
-                       if (key === 'blocklist-size')
-                       {
-                               // special case -- regular text area
-                               e.text('' + val.toStringWithCommas());
-                       }
-                       else switch (e[0].type)
-                       {
-                               case 'checkbox':
-                               case 'radio':
-                                       e.prop('checked', val);
-                                       setGroupEnabled(key, val);
-                                       break;
-                               case 'text':
-                               case 'url':
-                               case 'email':
-                               case 'number':
-                               case 'search':
-                                       // don't change the text if the user's editing it.
-                                       // it's very annoying when that happens!
-                                       if (e[0] !== document.activeElement)
-                                               e.val(val);
-                                       break;
-                               case 'select-one':
-                                       e.val(val);
-                                       break;
-                               default:
-                                       break;
-                       }
-               }
-       };
-
-       this.show = function ()
-       {
-               transmission.hideMobileAddressbar();
-
-               setBlocklistButtonEnabled(true);
-               data.remote.checkPort(onPortChecked,this);
-               data.elements.root.dialog('open');
-       };
-
-       this.close = function ()
-       {
-               transmission.hideMobileAddressbar();
-               data.elements.root.dialog('close');
-       },
-
-       this.shouldAddedTorrentsStart = function()
-       {
-               return data.elements.root.find('#start-added-torrents')[0].checked;
-       };
-
-       data.dialog = this;
-       initialize (remote);
+    var data = {
+        dialog: null,
+        remote: null,
+        elements: {},
+
+        // all the RPC session keys that we have gui controls for
+        keys: [
+            'alt-speed-down',
+            'alt-speed-time-begin',
+            'alt-speed-time-day',
+            'alt-speed-time-enabled',
+            'alt-speed-time-end',
+            'alt-speed-up',
+            'blocklist-enabled',
+            'blocklist-size',
+            'blocklist-url',
+            'dht-enabled',
+            'download-dir',
+            'encryption',
+            'idle-seeding-limit',
+            'idle-seeding-limit-enabled',
+            'lpd-enabled',
+            'peer-limit-global',
+            'peer-limit-per-torrent',
+            'peer-port',
+            'peer-port-random-on-start',
+            'pex-enabled',
+            'port-forwarding-enabled',
+            'rename-partial-files',
+            'seedRatioLimit',
+            'seedRatioLimited',
+            'speed-limit-down',
+            'speed-limit-down-enabled',
+            'speed-limit-up',
+            'speed-limit-up-enabled',
+            'start-added-torrents',
+            'utp-enabled'
+        ],
+
+        // map of keys that are enabled only if a 'parent' key is enabled
+        groups: {
+            'alt-speed-time-enabled': ['alt-speed-time-begin',
+                'alt-speed-time-day',
+                'alt-speed-time-end'
+            ],
+            'blocklist-enabled': ['blocklist-url',
+                'blocklist-update-button'
+            ],
+            'idle-seeding-limit-enabled': ['idle-seeding-limit'],
+            'seedRatioLimited': ['seedRatioLimit'],
+            'speed-limit-down-enabled': ['speed-limit-down'],
+            'speed-limit-up-enabled': ['speed-limit-up']
+        }
+    };
+
+    var initTimeDropDown = function (e) {
+        var i, hour, mins, value, content;
+
+        for (i = 0; i < 24 * 4; ++i) {
+            hour = parseInt(i / 4, 10);
+            mins = ((i % 4) * 15);
+            value = i * 15;
+            content = hour + ':' + (mins || '00');
+            e.options[i] = new Option(content, value);
+        }
+    };
+
+    var onPortChecked = function (response) {
+        var is_open = response['arguments']['port-is-open'];
+        var text = 'Port is <b>' + (is_open ? 'Open' : 'Closed') + '</b>';
+        var e = data.elements.root.find('#port-label');
+        setInnerHTML(e[0], text);
+    };
+
+    var setGroupEnabled = function (parent_key, enabled) {
+        var i, key, keys, root;
+
+        if (parent_key in data.groups) {
+            root = data.elements.root;
+            keys = data.groups[parent_key];
+
+            for (i = 0; key = keys[i]; ++i) {
+                root.find('#' + key).attr('disabled', !enabled);
+            };
+        };
+    };
+
+    var onBlocklistUpdateClicked = function () {
+        data.remote.updateBlocklist();
+        setBlocklistButtonEnabled(false);
+    };
+
+    var setBlocklistButtonEnabled = function (b) {
+        var e = data.elements.blocklist_button;
+        e.attr('disabled', !b);
+        e.val(b ? 'Update' : 'Updating...');
+    };
+
+    var getValue = function (e) {
+        var str;
+
+        switch (e[0].type) {
+        case 'checkbox':
+        case 'radio':
+            return e.prop('checked');
+
+        case 'text':
+        case 'url':
+        case 'email':
+        case 'number':
+        case 'search':
+        case 'select-one':
+            str = e.val();
+            if (parseInt(str, 10).toString() === str) {
+                return parseInt(str, 10);
+            };
+            if (parseFloat(str).toString() === str) {
+                return parseFloat(str);
+            };
+            return str;
+
+        default:
+            return null;
+        }
+    };
+
+    /* this callback is for controls whose changes can be applied
+       immediately, like checkboxs, radioboxes, and selects */
+    var onControlChanged = function (ev) {
+        var o = {};
+        o[ev.target.id] = getValue($(ev.target));
+        data.remote.savePrefs(o);
+    };
+
+    /* these two callbacks are for controls whose changes can't be applied
+       immediately -- like a text entry field -- because it takes many
+       change events for the user to get to the desired result */
+    var onControlFocused = function (ev) {
+        data.oldValue = getValue($(ev.target));
+    };
+
+    var onControlBlurred = function (ev) {
+        var newValue = getValue($(ev.target));
+        if (newValue !== data.oldValue) {
+            var o = {};
+            o[ev.target.id] = newValue;
+            data.remote.savePrefs(o);
+            delete data.oldValue;
+        }
+    };
+
+    var getDefaultMobileOptions = function () {
+        return {
+            width: $(window).width(),
+            height: $(window).height(),
+            position: ['left', 'top']
+        };
+    };
+
+    var initialize = function (remote) {
+        var i, key, e, o;
+
+        data.remote = remote;
+
+        e = $('#prefs-dialog');
+        data.elements.root = e;
+
+        initTimeDropDown(e.find('#alt-speed-time-begin')[0]);
+        initTimeDropDown(e.find('#alt-speed-time-end')[0]);
+
+        o = isMobileDevice ? getDefaultMobileOptions() : {
+            width: 350,
+            height: 400
+        };
+        o.autoOpen = false;
+        o.show = o.hide = 'fade';
+        o.close = onDialogClosed;
+        e.tabbedDialog(o);
+
+        e = e.find('#blocklist-update-button');
+        data.elements.blocklist_button = e;
+        e.click(onBlocklistUpdateClicked);
+
+        // listen for user input
+        for (i = 0; key = data.keys[i]; ++i) {
+            e = data.elements.root.find('#' + key);
+            switch (e[0].type) {
+            case 'checkbox':
+            case 'radio':
+            case 'select-one':
+                e.change(onControlChanged);
+                break;
+
+            case 'text':
+            case 'url':
+            case 'email':
+            case 'number':
+            case 'search':
+                e.focus(onControlFocused);
+                e.blur(onControlBlurred);
+
+            default:
+                break;
+            };
+        };
+    };
+
+    var getValues = function () {
+        var i, key, val, o = {},
+            keys = data.keys,
+            root = data.elements.root;
+
+        for (i = 0; key = keys[i]; ++i) {
+            val = getValue(root.find('#' + key));
+            if (val !== null) {
+                o[key] = val;
+            };
+        };
+
+        return o;
+    };
+
+    var onDialogClosed = function () {
+        transmission.hideMobileAddressbar();
+
+        $(data.dialog).trigger('closed', getValues());
+    };
+
+    /****
+     *****  PUBLIC FUNCTIONS
+     ****/
+
+    // update the dialog's controls
+    this.set = function (o) {
+        var e, i, key, val, option;
+        var keys = data.keys;
+        var root = data.elements.root;
+
+        setBlocklistButtonEnabled(true);
+
+        for (i = 0; key = keys[i]; ++i) {
+            val = o[key];
+            e = root.find('#' + key);
+
+            if (key === 'blocklist-size') {
+                // special case -- regular text area
+                e.text('' + val.toStringWithCommas());
+            } else switch (e[0].type) {
+            case 'checkbox':
+            case 'radio':
+                e.prop('checked', val);
+                setGroupEnabled(key, val);
+                break;
+            case 'text':
+            case 'url':
+            case 'email':
+            case 'number':
+            case 'search':
+                // don't change the text if the user's editing it.
+                // it's very annoying when that happens!
+                if (e[0] !== document.activeElement) {
+                    e.val(val);
+                };
+                break;
+            case 'select-one':
+                e.val(val);
+                break;
+            default:
+                break;
+            };
+        };
+    };
+
+    this.show = function () {
+        transmission.hideMobileAddressbar();
+
+        setBlocklistButtonEnabled(true);
+        data.remote.checkPort(onPortChecked, this);
+        data.elements.root.dialog('open');
+    };
+
+    this.close = function () {
+        transmission.hideMobileAddressbar();
+        data.elements.root.dialog('close');
+    };
+
+    this.shouldAddedTorrentsStart = function () {
+        return data.elements.root.find('#start-added-torrents')[0].checked;
+    };
+
+    data.dialog = this;
+    initialize(remote);
 };
index a83c56caa870b93dded4ac0e6924113f8084f9cb..bbfc9939aa14d5209924939f3b1122f5031df706 100644 (file)
  */
 
 var RPC = {
-       _DaemonVersion          : 'version',
-       _DownSpeedLimit         : 'speed-limit-down',
-       _DownSpeedLimited       : 'speed-limit-down-enabled',
-       _QueueMoveTop           : 'queue-move-top',
-       _QueueMoveBottom        : 'queue-move-bottom',
-       _QueueMoveUp            : 'queue-move-up',
-       _QueueMoveDown          : 'queue-move-down',
-       _Root                   : '../rpc',
-       _TurtleDownSpeedLimit   : 'alt-speed-down',
-       _TurtleState            : 'alt-speed-enabled',
-       _TurtleUpSpeedLimit     : 'alt-speed-up',
-       _UpSpeedLimit           : 'speed-limit-up',
-       _UpSpeedLimited         : 'speed-limit-up-enabled'
+    _DaemonVersion: 'version',
+    _DownSpeedLimit: 'speed-limit-down',
+    _DownSpeedLimited: 'speed-limit-down-enabled',
+    _QueueMoveTop: 'queue-move-top',
+    _QueueMoveBottom: 'queue-move-bottom',
+    _QueueMoveUp: 'queue-move-up',
+    _QueueMoveDown: 'queue-move-down',
+    _Root: '../rpc',
+    _TurtleDownSpeedLimit: 'alt-speed-down',
+    _TurtleState: 'alt-speed-enabled',
+    _TurtleUpSpeedLimit: 'alt-speed-up',
+    _UpSpeedLimit: 'speed-limit-up',
+    _UpSpeedLimited: 'speed-limit-up-enabled'
 };
 
-function TransmissionRemote(controller)
-{
-       this.initialize(controller);
-       return this;
+function TransmissionRemote(controller) {
+    this.initialize(controller);
+    return this;
 }
 
-TransmissionRemote.prototype =
-{
-       /*
-        * Constructor
-        */
-       initialize: function(controller) {
-               this._controller = controller;
-               this._error = '';
-               this._token = '';
-       },
+TransmissionRemote.prototype = {
+    /*
+     * Constructor
+     */
+    initialize: function (controller) {
+        this._controller = controller;
+        this._error = '';
+        this._token = '';
+    },
 
-       /*
-        * Display an error if an ajax request fails, and stop sending requests
-        * or on a 409, globally set the X-Transmission-Session-Id and resend
-        */
-       ajaxError: function(request, error_string, exception, ajaxObject) {
-               var token,
-                  remote = this;
+    /*
+     * Display an error if an ajax request fails, and stop sending requests
+     * or on a 409, globally set the X-Transmission-Session-Id and resend
+     */
+    ajaxError: function (request, error_string, exception, ajaxObject) {
+        var token;
+        var remote = this;
 
-               // set the Transmission-Session-Id on a 409
-               if (request.status === 409 && (token = request.getResponseHeader('X-Transmission-Session-Id'))){
-                       remote._token = token;
-                       $.ajax(ajaxObject);
-                       return;
-               }
+        // set the Transmission-Session-Id on a 409
+        if (request.status === 409 && (token = request.getResponseHeader('X-Transmission-Session-Id'))) {
+            remote._token = token;
+            $.ajax(ajaxObject);
+            return;
+        };
 
-               remote._error = request.responseText
-                             ? request.responseText.trim().replace(/(<([^>]+)>)/ig,"")
-                             : "";
-               if (!remote._error.length)
-                       remote._error = 'Server not responding';
+        remote._error = request.responseText ? request.responseText.trim().replace(/(<([^>]+)>)/ig, "") : "";
+        if (!remote._error.length) {
+            remote._error = 'Server not responding';
+        };
 
-               dialog.confirm('Connection Failed',
-                       'Could not connect to the server. You may need to reload the page to reconnect.',
-                       'Details',
-                       function() {
-                               alert(remote._error);
-                       },
-                       'Dismiss');
-               remote._controller.togglePeriodicSessionRefresh(false);
-       },
+        dialog.confirm('Connection Failed',
+            'Could not connect to the server. You may need to reload the page to reconnect.',
+            'Details',
+            function () {
+                alert(remote._error);
+            },
+            'Dismiss');
+        remote._controller.togglePeriodicSessionRefresh(false);
+    },
 
-       appendSessionId: function(XHR) {
-               if (this._token) {
-                       XHR.setRequestHeader('X-Transmission-Session-Id', this._token);
-               }
-       },
+    appendSessionId: function (XHR) {
+        if (this._token) {
+            XHR.setRequestHeader('X-Transmission-Session-Id', this._token);
+        };
+    },
 
-       sendRequest: function(data, callback, context, async) {
-               var remote = this;
-               if (typeof async != 'boolean')
-                       async = true;
+    sendRequest: function (data, callback, context, async) {
+        var remote = this;
+        if (typeof async != 'boolean') {
+            async = true;
+        };
 
-               var ajaxSettings = {
-                       url: RPC._Root,
-                       type: 'POST',
-                       contentType: 'json',
-                       dataType: 'json',
-                       cache: false,
-                       data: JSON.stringify(data),
-                       beforeSend: function(XHR){ remote.appendSessionId(XHR); },
-                       error: function(request, error_string, exception){ remote.ajaxError(request, error_string, exception, ajaxSettings); },
-                       success: callback,
-                       context: context,
-                       async: async
-               };
+        var ajaxSettings = {
+            url: RPC._Root,
+            type: 'POST',
+            contentType: 'json',
+            dataType: 'json',
+            cache: false,
+            data: JSON.stringify(data),
+            beforeSend: function (XHR) {
+                remote.appendSessionId(XHR);
+            },
+            error: function (request, error_string, exception) {
+                remote.ajaxError(request, error_string, exception, ajaxSettings);
+            },
+            success: callback,
+            context: context,
+            async: async
+        };
 
-               $.ajax(ajaxSettings);
-       },
+        $.ajax(ajaxSettings);
+    },
 
-       loadDaemonPrefs: function(callback, context, async) {
-               var o = { method: 'session-get' };
-               this.sendRequest(o, callback, context, async);
-       },
+    loadDaemonPrefs: function (callback, context, async) {
+        var o = {
+            method: 'session-get'
+        };
+        this.sendRequest(o, callback, context, async);
+    },
 
-       checkPort: function(callback, context, async) {
-               var o = { method: 'port-test' };
-               this.sendRequest(o, callback, context, async);
-       },
+    checkPort: function (callback, context, async) {
+        var o = {
+            method: 'port-test'
+        };
+        this.sendRequest(o, callback, context, async);
+    },
 
-       renameTorrent: function(torrentIds, oldpath, newname, callback, context) {
-               var o = {
-                       method: 'torrent-rename-path',
-                       arguments: {
-                               'ids': torrentIds,
-                               'path': oldpath,
-                               'name': newname
-                       }
-               };
-               this.sendRequest(o, callback, context);
-       },
+    renameTorrent: function (torrentIds, oldpath, newname, callback, context) {
+        var o = {
+            method: 'torrent-rename-path',
+            arguments: {
+                'ids': torrentIds,
+                'path': oldpath,
+                'name': newname
+            }
+        };
+        this.sendRequest(o, callback, context);
+    },
 
-       loadDaemonStats: function(callback, context, async) {
-               var o = { method: 'session-stats' };
-               this.sendRequest(o, callback, context, async);
-       },
+    loadDaemonStats: function (callback, context, async) {
+        var o = {
+            method: 'session-stats'
+        };
+        this.sendRequest(o, callback, context, async);
+    },
 
-       updateTorrents: function(torrentIds, fields, callback, context) {
-               var o = {
-                       method: 'torrent-get',
-                       arguments: {
-                               'fields': fields
-                       }
-               };
-               if (torrentIds)
-                       o['arguments'].ids = torrentIds;
-               this.sendRequest(o, function(response) {
-                       var args = response['arguments'];
-                       callback.call(context,args.torrents,args.removed);
-               });
-       },
+    updateTorrents: function (torrentIds, fields, callback, context) {
+        var o = {
+            method: 'torrent-get',
+            arguments: {
+                'fields': fields
+            }
+        };
+        if (torrentIds) {
+            o['arguments'].ids = torrentIds;
+        };
+        this.sendRequest(o, function (response) {
+            var args = response['arguments'];
+            callback.call(context, args.torrents, args.removed);
+        });
+    },
 
-       getFreeSpace: function(dir, callback, context) {
-               var remote = this;
-               var o = {
-                       method: 'free-space',
-                       arguments: { path: dir }
-               };
-               this.sendRequest(o, function(response) {
-                       var args = response['arguments'];
-                       callback.call (context, args.path, args['size-bytes']);
-               });
-       },
+    getFreeSpace: function (dir, callback, context) {
+        var remote = this;
+        var o = {
+            method: 'free-space',
+            arguments: {
+                path: dir
+            }
+        };
+        this.sendRequest(o, function (response) {
+            var args = response['arguments'];
+            callback.call(context, args.path, args['size-bytes']);
+        });
+    },
 
-       changeFileCommand: function(torrentId, fileIndices, command) {
-               var remote = this,
-                   args = { ids: [torrentId] };
-               args[command] = fileIndices;
-               this.sendRequest({
-                       arguments: args,
-                       method: 'torrent-set'
-               }, function() {
-                       remote._controller.refreshTorrents([torrentId]);
-               });
-       },
+    changeFileCommand: function (torrentId, fileIndices, command) {
+        var remote = this,
+            args = {
+                ids: [torrentId]
+            };
+        args[command] = fileIndices;
+        this.sendRequest({
+            arguments: args,
+            method: 'torrent-set'
+        }, function () {
+            remote._controller.refreshTorrents([torrentId]);
+        });
+    },
 
-       sendTorrentSetRequests: function(method, torrent_ids, args, callback, context) {
-               if (!args) args = { };
-               args['ids'] = torrent_ids;
-               var o = {
-                       method: method,
-                       arguments: args
-               };
-               this.sendRequest(o, callback, context);
-       },
+    sendTorrentSetRequests: function (method, torrent_ids, args, callback, context) {
+        if (!args) {
+            args = {};
+        };
+        args['ids'] = torrent_ids;
+        var o = {
+            method: method,
+            arguments: args
+        };
+        this.sendRequest(o, callback, context);
+    },
 
-       sendTorrentActionRequests: function(method, torrent_ids, callback, context) {
-               this.sendTorrentSetRequests(method, torrent_ids, null, callback, context);
-       },
+    sendTorrentActionRequests: function (method, torrent_ids, callback, context) {
+        this.sendTorrentSetRequests(method, torrent_ids, null, callback, context);
+    },
 
-       startTorrents: function(torrent_ids, noqueue, callback, context) {
-               var name = noqueue ? 'torrent-start-now' : 'torrent-start';
-               this.sendTorrentActionRequests(name, torrent_ids, callback, context);
-       },
-       stopTorrents: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests('torrent-stop', torrent_ids, callback, context);
-       },
+    startTorrents: function (torrent_ids, noqueue, callback, context) {
+        var name = noqueue ? 'torrent-start-now' : 'torrent-start';
+        this.sendTorrentActionRequests(name, torrent_ids, callback, context);
+    },
+    stopTorrents: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests('torrent-stop', torrent_ids, callback, context);
+    },
 
-       moveTorrents: function(torrent_ids, new_location, callback, context) {
-               var remote = this;
-               this.sendTorrentSetRequests( 'torrent-set-location', torrent_ids,
-                       {"move": true, "location": new_location}, callback, context);
-       },
+    moveTorrents: function (torrent_ids, new_location, callback, context) {
+        var remote = this;
+        this.sendTorrentSetRequests('torrent-set-location', torrent_ids, {
+            "move": true,
+            "location": new_location
+        }, callback, context);
+    },
 
-       removeTorrents: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests('torrent-remove', torrent_ids, callback, context);
-       },
-       removeTorrentsAndData: function(torrents) {
-               var remote = this;
-               var o = {
-                       method: 'torrent-remove',
-                       arguments: {
-                               'delete-local-data': true,
-                               ids: [ ]
-                       }
-               };
+    removeTorrents: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests('torrent-remove', torrent_ids, callback, context);
+    },
+    removeTorrentsAndData: function (torrents) {
+        var remote = this;
+        var o = {
+            method: 'torrent-remove',
+            arguments: {
+                'delete-local-data': true,
+                ids: []
+            }
+        };
 
-               if (torrents) {
-                       for (var i=0, len=torrents.length; i<len; ++i) {
-                               o.arguments.ids.push(torrents[i].getId());
-                       }
-               }
-               this.sendRequest(o, function() {
-                       remote._controller.refreshTorrents();
-               });
-       },
-       verifyTorrents: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests('torrent-verify', torrent_ids, callback, context);
-       },
-       reannounceTorrents: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests('torrent-reannounce', torrent_ids, callback, context);
-       },
-       addTorrentByUrl: function(url, options) {
-               var remote = this;
-               if (url.match(/^[0-9a-f]{40}$/i)) {
-                       url = 'magnet:?xt=urn:btih:'+url;
-               }
-               var o = {
-                       method: 'torrent-add',
-                       arguments: {
-                               paused: (options.paused),
-                               filename: url
-                       }
-               };
-               this.sendRequest(o, function() {
-                       remote._controller.refreshTorrents();
-               });
-       },
-       savePrefs: function(args) {
-               var remote = this;
-               var o = {
-                       method: 'session-set',
-                       arguments: args
-               };
-               this.sendRequest(o, function() {
-                       remote._controller.loadDaemonPrefs();
-               });
-       },
-       updateBlocklist: function() {
-               var remote = this;
-               var o = {
-                       method: 'blocklist-update'
-               };
-               this.sendRequest(o, function() {
-                       remote._controller.loadDaemonPrefs();
-               });
-       },
+        if (torrents) {
+            for (var i = 0, len = torrents.length; i < len; ++i) {
+                o.arguments.ids.push(torrents[i].getId());
+            };
+        };
+        this.sendRequest(o, function () {
+            remote._controller.refreshTorrents();
+        });
+    },
+    verifyTorrents: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests('torrent-verify', torrent_ids, callback, context);
+    },
+    reannounceTorrents: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests('torrent-reannounce', torrent_ids, callback, context);
+    },
+    addTorrentByUrl: function (url, options) {
+        var remote = this;
+        if (url.match(/^[0-9a-f]{40}$/i)) {
+            url = 'magnet:?xt=urn:btih:' + url;
+        }
+        var o = {
+            method: 'torrent-add',
+            arguments: {
+                paused: (options.paused),
+                filename: url
+            }
+        };
+        this.sendRequest(o, function () {
+            remote._controller.refreshTorrents();
+        });
+    },
+    savePrefs: function (args) {
+        var remote = this;
+        var o = {
+            method: 'session-set',
+            arguments: args
+        };
+        this.sendRequest(o, function () {
+            remote._controller.loadDaemonPrefs();
+        });
+    },
+    updateBlocklist: function () {
+        var remote = this;
+        var o = {
+            method: 'blocklist-update'
+        };
+        this.sendRequest(o, function () {
+            remote._controller.loadDaemonPrefs();
+        });
+    },
 
-       // Added queue calls
-       moveTorrentsToTop: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests(RPC._QueueMoveTop, torrent_ids, callback, context);
-       },
-       moveTorrentsToBottom: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests(RPC._QueueMoveBottom, torrent_ids, callback, context);
-       },
-       moveTorrentsUp: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests(RPC._QueueMoveUp, torrent_ids, callback, context);
-       },
-       moveTorrentsDown: function(torrent_ids, callback, context) {
-               this.sendTorrentActionRequests(RPC._QueueMoveDown, torrent_ids, callback, context);
-       }
+    // Added queue calls
+    moveTorrentsToTop: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests(RPC._QueueMoveTop, torrent_ids, callback, context);
+    },
+    moveTorrentsToBottom: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests(RPC._QueueMoveBottom, torrent_ids, callback, context);
+    },
+    moveTorrentsUp: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests(RPC._QueueMoveUp, torrent_ids, callback, context);
+    },
+    moveTorrentsDown: function (torrent_ids, callback, context) {
+        this.sendTorrentActionRequests(RPC._QueueMoveDown, torrent_ids, callback, context);
+    }
 };
index f73f0a6395d8d54911fa43d269df77991dfdaeb8..54a6bc2942bb2443ae90ba8b220dd8eb2571b04e 100644 (file)
  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  */
 
-function TorrentRendererHelper()
-{
-}
-
-TorrentRendererHelper.getProgressInfo = function(controller, t)
-{
-       var pct, extra,
-           s = t.getStatus(),
-           seed_ratio_limit = t.seedRatioLimit(controller);
-
-       if (t.needsMetaData())
-               pct = t.getMetadataPercentComplete() * 100;
-       else if (!t.isDone())
-               pct = Math.round(t.getPercentDone() * 100);
-       else if (seed_ratio_limit > 0 && t.isSeeding()) // don't split up the bar if paused or queued
-               pct = Math.round(t.getUploadRatio() * 100 / seed_ratio_limit);
-       else
-               pct = 100;
-
-       if (s === Torrent._StatusStopped)
-               extra = 'paused';
-       else if (s === Torrent._StatusDownloadWait)
-               extra = 'leeching queued';
-       else if (t.needsMetaData())
-               extra = 'magnet';
-       else if (s === Torrent._StatusDownload)
-               extra = 'leeching';
-       else if (s === Torrent._StatusSeedWait)
-               extra = 'seeding queued';
-       else if (s === Torrent._StatusSeed)
-               extra = 'seeding';
-       else
-               extra = '';
-
-       return {
-               percent: pct,
-               complete: [ 'torrent_progress_bar', 'complete', extra ].join(' '),
-               incomplete: [ 'torrent_progress_bar', 'incomplete', extra ].join(' ')
-       };
+function TorrentRendererHelper() {}
+
+TorrentRendererHelper.getProgressInfo = function (controller, t) {
+    var pct, extra;
+    var s = t.getStatus();
+    var seed_ratio_limit = t.seedRatioLimit(controller);
+
+    if (t.needsMetaData()) {
+        pct = t.getMetadataPercentComplete() * 100;
+    } else if (!t.isDone()) {
+        pct = Math.round(t.getPercentDone() * 100);
+    } else if (seed_ratio_limit > 0 && t.isSeeding()) { // don't split up the bar if paused or queued
+        pct = Math.round(t.getUploadRatio() * 100 / seed_ratio_limit);
+    } else {
+        pct = 100;
+    };
+
+    if (s === Torrent._StatusStopped) {
+        extra = 'paused';
+    } else if (s === Torrent._StatusDownloadWait) {
+        extra = 'leeching queued';
+    } else if (t.needsMetaData()) {
+        extra = 'magnet';
+    } else if (s === Torrent._StatusDownload) {
+        extra = 'leeching';
+    } else if (s === Torrent._StatusSeedWait) {
+        extra = 'seeding queued';
+    } else if (s === Torrent._StatusSeed) {
+        extra = 'seeding';
+    } else {
+        extra = '';
+    };
+
+    return {
+        percent: pct,
+        complete: ['torrent_progress_bar', 'complete', extra].join(' '),
+        incomplete: ['torrent_progress_bar', 'incomplete', extra].join(' ')
+    };
 };
 
-TorrentRendererHelper.createProgressbar = function(classes)
-{
-       var complete, incomplete, progressbar;
+TorrentRendererHelper.createProgressbar = function (classes) {
+    var complete, incomplete, progressbar;
 
-       complete = document.createElement('div');
-       complete.className = 'torrent_progress_bar complete';
+    complete = document.createElement('div');
+    complete.className = 'torrent_progress_bar complete';
 
-       incomplete = document.createElement('div');
-       incomplete.className = 'torrent_progress_bar incomplete';
+    incomplete = document.createElement('div');
+    incomplete.className = 'torrent_progress_bar incomplete';
 
-       progressbar = document.createElement('div');
-       progressbar.className = 'torrent_progress_bar_container ' + classes;
-       progressbar.appendChild(complete);
-       progressbar.appendChild(incomplete);
+    progressbar = document.createElement('div');
+    progressbar.className = 'torrent_progress_bar_container ' + classes;
+    progressbar.appendChild(complete);
+    progressbar.appendChild(incomplete);
 
-       return { 'element': progressbar, 'complete': complete, 'incomplete': incomplete };
+    return {
+        'element': progressbar,
+        'complete': complete,
+        'incomplete': incomplete
+    };
 };
 
-TorrentRendererHelper.renderProgressbar = function(controller, t, progressbar)
-{
-       var e, style, width, display,
-           info = TorrentRendererHelper.getProgressInfo(controller, t);
-
-       // update the complete progressbar
-       e = progressbar.complete;
-       style = e.style;
-       width = '' + info.percent + '%';
-       display = info.percent > 0 ? 'block' : 'none';
-       if (style.width!==width || style.display!==display)
-               $(e).css({ width: ''+info.percent+'%', display: display });
-       if (e.className !== info.complete)
-               e.className = info.complete;
-
-       // update the incomplete progressbar
-       e = progressbar.incomplete;
-       display = (info.percent < 100) ? 'block' : 'none';
-       if (e.style.display !== display)
-               e.style.display = display;
-       if (e.className !== info.incomplete)
-               e.className = info.incomplete;
+TorrentRendererHelper.renderProgressbar = function (controller, t, progressbar) {
+    var e, style, width, display
+    var info = TorrentRendererHelper.getProgressInfo(controller, t);
+
+    // update the complete progressbar
+    e = progressbar.complete;
+    style = e.style;
+    width = '' + info.percent + '%';
+    display = info.percent > 0 ? 'block' : 'none';
+    if (style.width !== width || style.display !== display) {
+        $(e).css({
+            width: '' + info.percent + '%',
+            display: display
+        });
+    };
+
+    if (e.className !== info.complete) {
+        e.className = info.complete;
+    };
+
+    // update the incomplete progressbar
+    e = progressbar.incomplete;
+    display = (info.percent < 100) ? 'block' : 'none';
+
+    if (e.style.display !== display) {
+        e.style.display = display;
+    };
+
+    if (e.className !== info.incomplete) {
+        e.className = info.incomplete;
+    };
 };
 
-TorrentRendererHelper.formatUL = function(t)
-{
-       return '↑ ' + Transmission.fmt.speedBps(t.getUploadSpeed());
+TorrentRendererHelper.formatUL = function (t) {
+    return '↑ ' + Transmission.fmt.speedBps(t.getUploadSpeed());
 };
 
-TorrentRendererHelper.formatDL = function(t)
-{
-       return '↓ ' + Transmission.fmt.speedBps(t.getDownloadSpeed());
+TorrentRendererHelper.formatDL = function (t) {
+    return '↓ ' + Transmission.fmt.speedBps(t.getDownloadSpeed());
 };
 
 /****
-*****
-*****
-****/
-
-function TorrentRendererFull()
-{
-}
-TorrentRendererFull.prototype =
-{
-       createRow: function()
-       {
-               var root, name, peers, progressbar, details, image, button;
-
-               root = document.createElement('li');
-               root.className = 'torrent';
-
-               name = document.createElement('div');
-               name.className = 'torrent_name';
-
-               peers = document.createElement('div');
-               peers.className = 'torrent_peer_details';
-
-               progressbar = TorrentRendererHelper.createProgressbar('full');
-
-               details = document.createElement('div');
-               details.className = 'torrent_progress_details';
-
-               image = document.createElement('div');
-               button = document.createElement('a');
-               button.appendChild(image);
-
-               root.appendChild(name);
-               root.appendChild(peers);
-               root.appendChild(button);
-               root.appendChild(progressbar.element);
-               root.appendChild(details);
-
-               root._name_container = name;
-               root._peer_details_container = peers;
-               root._progress_details_container = details;
-               root._progressbar = progressbar;
-               root._pause_resume_button_image = image;
-               root._toggle_running_button = button;
-
-               return root;
-       },
-
-       getPeerDetails: function(t)
-       {
-               var err,
-                   peer_count,
-                   webseed_count,
-                   fmt = Transmission.fmt;
-
-               if ((err = t.getErrorMessage()))
-                       return err;
-
-               if (t.isDownloading())
-               {
-                       peer_count = t.getPeersConnected();
-                       webseed_count = t.getWebseedsSendingToUs();
-
-                       if (webseed_count && peer_count)
-                       {
-                               // Downloading from 2 of 3 peer(s) and 2 webseed(s)
-                               return [ 'Downloading from',
-                                        t.getPeersSendingToUs(),
-                                        'of',
-                                        fmt.countString('peer','peers',peer_count),
-                                        'and',
-                                        fmt.countString('web seed','web seeds',webseed_count),
-                                        '-',
-                                        TorrentRendererHelper.formatDL(t),
-                                        TorrentRendererHelper.formatUL(t) ].join(' ');
-                       }
-                       else if (webseed_count)
-                       {
-                               // Downloading from 2 webseed(s)
-                               return [ 'Downloading from',
-                                        fmt.countString('web seed','web seeds',webseed_count),
-                                        '-',
-                                        TorrentRendererHelper.formatDL(t),
-                                        TorrentRendererHelper.formatUL(t) ].join(' ');
-                       }
-                       else
-                       {
-                               // Downloading from 2 of 3 peer(s)
-                               return [ 'Downloading from',
-                                        t.getPeersSendingToUs(),
-                                        'of',
-                                        fmt.countString('peer','peers',peer_count),
-                                        '-',
-                                        TorrentRendererHelper.formatDL(t),
-                                        TorrentRendererHelper.formatUL(t) ].join(' ');
-                       }
-               }
-
-               if (t.isSeeding())
-                       return [ 'Seeding to',
-                                t.getPeersGettingFromUs(),
-                                'of',
-                                fmt.countString ('peer','peers',t.getPeersConnected()),
-                                '-',
-                                TorrentRendererHelper.formatUL(t) ].join(' ');
-
-               if (t.isChecking())
-                       return [ 'Verifying local data (',
-                                Transmission.fmt.percentString(100.0 * t.getRecheckProgress()),
-                                '% tested)' ].join('');
-
-               return t.getStateString();
-       },
-
-       getProgressDetails: function(controller, t)
-       {
-               if (t.needsMetaData()) {
-                       var MetaDataStatus = "retrieving";
-                       if (t.isStopped())
-                               MetaDataStatus = "needs";
-                       var percent = 100 * t.getMetadataPercentComplete();
-                       return [ "Magnetized transfer - " + MetaDataStatus + " metadata (",
-                                Transmission.fmt.percentString(percent),
-                                "%)" ].join('');
-               }
-
-               var c,
-                   sizeWhenDone = t.getSizeWhenDone(),
-                   totalSize = t.getTotalSize(),
-                   is_done = t.isDone() || t.isSeeding();
-
-               if (is_done) {
-                       if (totalSize === sizeWhenDone) // seed: '698.05 MiB'
-                               c = [ Transmission.fmt.size(totalSize) ];
-                       else // partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
-                               c = [ Transmission.fmt.size(sizeWhenDone),
-                                     ' of ',
-                                     Transmission.fmt.size(t.getTotalSize()),
-                                     ' (', t.getPercentDoneStr(), '%)' ];
-                       // append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
-                       c.push(', uploaded ',
-                              Transmission.fmt.size(t.getUploadedEver()),
-                              ' (Ratio ',
-                              Transmission.fmt.ratioString(t.getUploadRatio()),
-                              ')');
-               } else { // not done yet
-                       c = [ Transmission.fmt.size(sizeWhenDone - t.getLeftUntilDone()),
-                             ' of ', Transmission.fmt.size(sizeWhenDone),
-                             ' (', t.getPercentDoneStr(), '%)' ];
-               }
-
-               // maybe append eta
-               if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller)>0)) {
-                       c.push(' - ');
-                       var eta = t.getETA();
-                       if (eta < 0 || eta >= (999*60*60) /* arbitrary */)
-                               c.push('remaining time unknown');
-                       else
-                               c.push(Transmission.fmt.timeInterval(t.getETA()),
-                                      ' remaining');
-               }
-
-               return c.join('');
-       },
-
-       render: function(controller, t, root)
-       {
-               // name
-               setTextContent(root._name_container, t.getName());
-
-               // progressbar
-               TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
-
-               // peer details
-               var has_error = t.getError() !== Torrent._ErrNone;
-               var e = root._peer_details_container;
-               $(e).toggleClass('error',has_error);
-               setTextContent(e, this.getPeerDetails(t));
-
-               // progress details
-               e = root._progress_details_container;
-               setTextContent(e, this.getProgressDetails(controller, t));
-
-               // pause/resume button
-               var is_stopped = t.isStopped();
-               e = root._pause_resume_button_image;
-               e.alt = is_stopped ? 'Resume' : 'Pause';
-               e.className = is_stopped ? 'torrent_resume' : 'torrent_pause';
-       }
+ *****
+ *****
+ ****/
+
+function TorrentRendererFull() {};
+TorrentRendererFull.prototype = {
+    createRow: function () {
+        var root, name, peers, progressbar, details, image, button;
+
+        root = document.createElement('li');
+        root.className = 'torrent';
+
+        name = document.createElement('div');
+        name.className = 'torrent_name';
+
+        peers = document.createElement('div');
+        peers.className = 'torrent_peer_details';
+
+        progressbar = TorrentRendererHelper.createProgressbar('full');
+
+        details = document.createElement('div');
+        details.className = 'torrent_progress_details';
+
+        image = document.createElement('div');
+        button = document.createElement('a');
+        button.appendChild(image);
+
+        root.appendChild(name);
+        root.appendChild(peers);
+        root.appendChild(button);
+        root.appendChild(progressbar.element);
+        root.appendChild(details);
+
+        root._name_container = name;
+        root._peer_details_container = peers;
+        root._progress_details_container = details;
+        root._progressbar = progressbar;
+        root._pause_resume_button_image = image;
+        root._toggle_running_button = button;
+
+        return root;
+    },
+
+    getPeerDetails: function (t) {
+        var err,
+            peer_count,
+            webseed_count,
+            fmt = Transmission.fmt;
+
+        if ((err = t.getErrorMessage())) {
+            return err;
+        };
+
+        if (t.isDownloading()) {
+            peer_count = t.getPeersConnected();
+            webseed_count = t.getWebseedsSendingToUs();
+
+            if (webseed_count && peer_count) {
+                // Downloading from 2 of 3 peer(s) and 2 webseed(s)
+                return ['Downloading from',
+                    t.getPeersSendingToUs(),
+                    'of',
+                    fmt.countString('peer', 'peers', peer_count),
+                    'and',
+                    fmt.countString('web seed', 'web seeds', webseed_count),
+                    '-',
+                    TorrentRendererHelper.formatDL(t),
+                    TorrentRendererHelper.formatUL(t)
+                ].join(' ');
+            } else if (webseed_count) {
+                // Downloading from 2 webseed(s)
+                return ['Downloading from',
+                    fmt.countString('web seed', 'web seeds', webseed_count),
+                    '-',
+                    TorrentRendererHelper.formatDL(t),
+                    TorrentRendererHelper.formatUL(t)
+                ].join(' ');
+            } else {
+                // Downloading from 2 of 3 peer(s)
+                return ['Downloading from',
+                    t.getPeersSendingToUs(),
+                    'of',
+                    fmt.countString('peer', 'peers', peer_count),
+                    '-',
+                    TorrentRendererHelper.formatDL(t),
+                    TorrentRendererHelper.formatUL(t)
+                ].join(' ');
+            };
+        };
+
+        if (t.isSeeding()) {
+            return ['Seeding to', t.getPeersGettingFromUs(), 'of', fmt.countString('peer', 'peers', t.getPeersConnected()), '-', TorrentRendererHelper.formatUL(t)].join(' ');
+        };
+
+        if (t.isChecking()) {
+            return ['Verifying local data (', Transmission.fmt.percentString(100.0 * t.getRecheckProgress()), '% tested)'].join('');
+        }
+
+        return t.getStateString();
+    },
+
+    getProgressDetails: function (controller, t) {
+        if (t.needsMetaData()) {
+            var MetaDataStatus = "retrieving";
+            if (t.isStopped()) {
+                MetaDataStatus = "needs";
+            };
+            var percent = 100 * t.getMetadataPercentComplete();
+            return ["Magnetized transfer - " + MetaDataStatus + " metadata (",
+                Transmission.fmt.percentString(percent),
+                "%)"
+            ].join('');
+        }
+
+        var c;
+        var sizeWhenDone = t.getSizeWhenDone();
+        var totalSize = t.getTotalSize();
+        var is_done = t.isDone() || t.isSeeding();
+
+        if (is_done) {
+            if (totalSize === sizeWhenDone) {
+                // seed: '698.05 MiB'
+                c = [Transmission.fmt.size(totalSize)];
+            } else { // partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
+                c = [Transmission.fmt.size(sizeWhenDone), ' of ', Transmission.fmt.size(t.getTotalSize()), ' (', t.getPercentDoneStr(), '%)'];
+            };
+            // append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
+            c.push(', uploaded ',
+                Transmission.fmt.size(t.getUploadedEver()),
+                ' (Ratio ',
+                Transmission.fmt.ratioString(t.getUploadRatio()),
+                ')');
+        } else { // not done yet
+            c = [Transmission.fmt.size(sizeWhenDone - t.getLeftUntilDone()),
+                ' of ', Transmission.fmt.size(sizeWhenDone),
+                ' (', t.getPercentDoneStr(), '%)'
+            ];
+        };
+
+        // maybe append eta
+        if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller) > 0)) {
+            c.push(' - ');
+            var eta = t.getETA();
+            if (eta < 0 || eta >= (999 * 60 * 60) /* arbitrary */ ) {
+                c.push('remaining time unknown');
+            } else {
+                c.push(Transmission.fmt.timeInterval(t.getETA()), ' remaining');
+            };
+        };
+
+        return c.join('');
+    },
+
+    render: function (controller, t, root) {
+        // name
+        setTextContent(root._name_container, t.getName());
+
+        // progressbar
+        TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
+
+        // peer details
+        var has_error = t.getError() !== Torrent._ErrNone;
+        var e = root._peer_details_container;
+        $(e).toggleClass('error', has_error);
+        setTextContent(e, this.getPeerDetails(t));
+
+        // progress details
+        e = root._progress_details_container;
+        setTextContent(e, this.getProgressDetails(controller, t));
+
+        // pause/resume button
+        var is_stopped = t.isStopped();
+        e = root._pause_resume_button_image;
+        e.alt = is_stopped ? 'Resume' : 'Pause';
+        e.className = is_stopped ? 'torrent_resume' : 'torrent_pause';
+    }
 };
 
 /****
-*****
-*****
-****/
-
-function TorrentRendererCompact()
-{
-}
-TorrentRendererCompact.prototype =
-{
-       createRow: function()
-       {
-               var progressbar, details, name, root;
-
-               progressbar = TorrentRendererHelper.createProgressbar('compact');
-
-               details = document.createElement('div');
-               details.className = 'torrent_peer_details compact';
-
-               name = document.createElement('div');
-               name.className = 'torrent_name compact';
-
-               root = document.createElement('li');
-               root.appendChild(progressbar.element);
-               root.appendChild(details);
-               root.appendChild(name);
-               root.className = 'torrent compact';
-               root._progressbar = progressbar;
-               root._details_container = details;
-               root._name_container = name;
-               return root;
-       },
-
-       getPeerDetails: function(t)
-       {
-               var c;
-               if ((c = t.getErrorMessage()))
-                       return c;
-               if (t.isDownloading()) {
-                       var have_dn = t.getDownloadSpeed() > 0,
-                           have_up = t.getUploadSpeed() > 0;
-                       if (!have_up && !have_dn)
-                               return 'Idle';
-                       var s = '';
-                       if (have_dn)
-                               s += TorrentRendererHelper.formatDL(t);
-                       if (have_dn && have_up)
-                               s += ' '
-                       if (have_up)
-                               s += TorrentRendererHelper.formatUL(t);
-                       return s;
-               }
-               if (t.isSeeding())
-                       return [ 'Ratio: ',
-                                Transmission.fmt.ratioString(t.getUploadRatio()),
-                                ', ',
-                                TorrentRendererHelper.formatUL(t) ].join('');
-               return t.getStateString();
-       },
-
-       render: function(controller, t, root)
-       {
-               // name
-               var is_stopped = t.isStopped();
-               var e = root._name_container;
-               $(e).toggleClass('paused', is_stopped);
-               setTextContent(e, t.getName());
-
-               // peer details
-               var has_error = t.getError() !== Torrent._ErrNone;
-               e = root._details_container;
-               $(e).toggleClass('error', has_error);
-               setTextContent(e, this.getPeerDetails(t));
-
-               // progressbar
-               TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
-       }
+ *****
+ *****
+ ****/
+
+function TorrentRendererCompact() {};
+TorrentRendererCompact.prototype = {
+    createRow: function () {
+        var progressbar, details, name, root;
+
+        progressbar = TorrentRendererHelper.createProgressbar('compact');
+
+        details = document.createElement('div');
+        details.className = 'torrent_peer_details compact';
+
+        name = document.createElement('div');
+        name.className = 'torrent_name compact';
+
+        root = document.createElement('li');
+        root.appendChild(progressbar.element);
+        root.appendChild(details);
+        root.appendChild(name);
+        root.className = 'torrent compact';
+        root._progressbar = progressbar;
+        root._details_container = details;
+        root._name_container = name;
+        return root;
+    },
+
+    getPeerDetails: function (t) {
+        var c;
+        if ((c = t.getErrorMessage())) {
+            return c;
+        };
+        if (t.isDownloading()) {
+            var have_dn = t.getDownloadSpeed() > 0;
+            var have_up = t.getUploadSpeed() > 0;
+
+            if (!have_up && !have_dn) {
+                return 'Idle';
+            };
+            var s = '';
+            if (have_dn) {
+                s += TorrentRendererHelper.formatDL(t);
+            };
+            if (have_dn && have_up) {
+                s += ' ';
+            };
+            if (have_up) {
+                s += TorrentRendererHelper.formatUL(t);
+            };
+            return s;
+        };
+        if (t.isSeeding()) {
+            return ['Ratio: ', Transmission.fmt.ratioString(t.getUploadRatio()), ', ', TorrentRendererHelper.formatUL(t)].join('');
+        };
+        return t.getStateString();
+    },
+
+    render: function (controller, t, root) {
+        // name
+        var is_stopped = t.isStopped();
+        var e = root._name_container;
+        $(e).toggleClass('paused', is_stopped);
+        setTextContent(e, t.getName());
+
+        // peer details
+        var has_error = t.getError() !== Torrent._ErrNone;
+        e = root._details_container;
+        $(e).toggleClass('error', has_error);
+        setTextContent(e, this.getPeerDetails(t));
+
+        // progressbar
+        TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
+    }
 };
 
 /****
-*****
-*****
-****/
-
-function TorrentRow(view, controller, torrent)
-{
-       this.initialize(view, controller, torrent);
-}
-TorrentRow.prototype =
-{
-       initialize: function(view, controller, torrent) {
-               var row = this;
-               this._view = view;
-               this._torrent = torrent;
-               this._element = view.createRow();
-               this.render(controller);
-               $(this._torrent).bind('dataChanged.torrentRowListener',function(){row.render(controller);});
-
-       },
-       getElement: function() {
-               return this._element;
-       },
-       render: function(controller) {
-               var tor = this.getTorrent();
-               if (tor)
-                       this._view.render(controller, tor, this.getElement());
-       },
-       isSelected: function() {
-               return this.getElement().className.indexOf('selected') !== -1;
-       },
-
-       getTorrent: function() {
-               return this._torrent;
-       },
-       getTorrentId: function() {
-               return this.getTorrent().getId();
-       }
+ *****
+ *****
+ ****/
+
+function TorrentRow(view, controller, torrent) {
+    this.initialize(view, controller, torrent);
+};
+TorrentRow.prototype = {
+    initialize: function (view, controller, torrent) {
+        var row = this;
+        this._view = view;
+        this._torrent = torrent;
+        this._element = view.createRow();
+        this.render(controller);
+        $(this._torrent).bind('dataChanged.torrentRowListener', function () {
+            row.render(controller);
+        });
+
+    },
+    getElement: function () {
+        return this._element;
+    },
+    render: function (controller) {
+        var tor = this.getTorrent();
+        if (tor) {
+            this._view.render(controller, tor, this.getElement());
+        };
+    },
+    isSelected: function () {
+        return this.getElement().className.indexOf('selected') !== -1;
+    },
+
+    getTorrent: function () {
+        return this._torrent;
+    },
+    getTorrentId: function () {
+        return this.getTorrent().getId();
+    }
 };
index a90760cbc1a44a3a36ac571b84208855e1fa6934..c027c2f0481142e9f227889881ae54300498bcae 100644 (file)
  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  */
 
-function Torrent(data)
-{
-       this.initialize(data);
-}
+function Torrent(data) {
+    this.initialize(data);
+};
 
 /***
-****
-****  Constants
-****
-***/
+ ****
+ ****  Constants
+ ****
+ ***/
 
 // Torrent.fields.status
-Torrent._StatusStopped         = 0;
-Torrent._StatusCheckWait       = 1;
-Torrent._StatusCheck           = 2;
-Torrent._StatusDownloadWait    = 3;
-Torrent._StatusDownload        = 4;
-Torrent._StatusSeedWait        = 5;
-Torrent._StatusSeed            = 6;
+Torrent._StatusStopped = 0;
+Torrent._StatusCheckWait = 1;
+Torrent._StatusCheck = 2;
+Torrent._StatusDownloadWait = 3;
+Torrent._StatusDownload = 4;
+Torrent._StatusSeedWait = 5;
+Torrent._StatusSeed = 6;
 
 // Torrent.fields.seedRatioMode
-Torrent._RatioUseGlobal        = 0;
-Torrent._RatioUseLocal         = 1;
-Torrent._RatioUnlimited        = 2;
+Torrent._RatioUseGlobal = 0;
+Torrent._RatioUseLocal = 1;
+Torrent._RatioUnlimited = 2;
 
 // Torrent.fields.error
-Torrent._ErrNone               = 0;
-Torrent._ErrTrackerWarning     = 1;
-Torrent._ErrTrackerError       = 2;
-Torrent._ErrLocalError         = 3;
+Torrent._ErrNone = 0;
+Torrent._ErrTrackerWarning = 1;
+Torrent._ErrTrackerError = 2;
+Torrent._ErrLocalError = 3;
 
 // TrackerStats' announceState
-Torrent._TrackerInactive       = 0;
-Torrent._TrackerWaiting        = 1;
-Torrent._TrackerQueued         = 2;
-Torrent._TrackerActive         = 3;
-
+Torrent._TrackerInactive = 0;
+Torrent._TrackerWaiting = 1;
+Torrent._TrackerQueued = 2;
+Torrent._TrackerActive = 3;
 
-Torrent.Fields = { };
+Torrent.Fields = {};
 
 // commonly used fields which only need to be loaded once,
 // either on startup or when a magnet finishes downloading its metadata
 // finishes downloading its metadata
 Torrent.Fields.Metadata = [
-       'addedDate',
-       'name',
-       'totalSize'
+    'addedDate',
+    'name',
+    'totalSize'
 ];
 
 // commonly used fields which need to be periodically refreshed
 Torrent.Fields.Stats = [
-       'error',
-       'errorString',
-       'eta',
-       'isFinished',
-       'isStalled',
-       'leftUntilDone',
-       'metadataPercentComplete',
-       'peersConnected',
-       'peersGettingFromUs',
-       'peersSendingToUs',
-       'percentDone',
-       'queuePosition',
-       'rateDownload',
-       'rateUpload',
-       'recheckProgress',
-       'seedRatioMode',
-       'seedRatioLimit',
-       'sizeWhenDone',
-       'status',
-       'trackers',
-       'downloadDir',
-       'uploadedEver',
-       'uploadRatio',
-       'webseedsSendingToUs'
+    'error',
+    'errorString',
+    'eta',
+    'isFinished',
+    'isStalled',
+    'leftUntilDone',
+    'metadataPercentComplete',
+    'peersConnected',
+    'peersGettingFromUs',
+    'peersSendingToUs',
+    'percentDone',
+    'queuePosition',
+    'rateDownload',
+    'rateUpload',
+    'recheckProgress',
+    'seedRatioMode',
+    'seedRatioLimit',
+    'sizeWhenDone',
+    'status',
+    'trackers',
+    'downloadDir',
+    'uploadedEver',
+    'uploadRatio',
+    'webseedsSendingToUs'
 ];
 
 // fields used by the inspector which only need to be loaded once
 Torrent.Fields.InfoExtra = [
-       'comment',
-       'creator',
-       'dateCreated',
-       'files',
-       'hashString',
-       'isPrivate',
-       'pieceCount',
-       'pieceSize'
+    'comment',
+    'creator',
+    'dateCreated',
+    'files',
+    'hashString',
+    'isPrivate',
+    'pieceCount',
+    'pieceSize'
 ];
 
 // fields used in the inspector which need to be periodically refreshed
 Torrent.Fields.StatsExtra = [
-       'activityDate',
-       'corruptEver',
-       'desiredAvailable',
-       'downloadedEver',
-       'fileStats',
-       'haveUnchecked',
-       'haveValid',
-       'peers',
-       'startDate',
-       'trackerStats'
+    'activityDate',
+    'corruptEver',
+    'desiredAvailable',
+    'downloadedEver',
+    'fileStats',
+    'haveUnchecked',
+    'haveValid',
+    'peers',
+    'startDate',
+    'trackerStats'
 ];
 
 /***
-****
-****  Methods
-****
-***/
-
-Torrent.prototype =
-{
-       initialize: function(data)
-       {
-               this.fields = {};
-               this.fieldObservers = {};
-               this.refresh (data);
-       },
-
-       notifyOnFieldChange: function(field, callback) {
-               this.fieldObservers[field] = this.fieldObservers[field] || [];
-               this.fieldObservers[field].push(callback);
-       },
-
-       setField: function(o, name, value)
-       {
-               var i, observer;
-
-               if (o[name] === value)
-                       return false;
-               if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) {
-                       for (i=0; observer=this.fieldObservers[name][i]; ++i) {
-                               observer.call(this, value, o[name], name);
-                       }
-               }
-               o[name] = value;
-               return true;
-       },
-
-       // fields.files is an array of unions of RPC's "files" and "fileStats" objects.
-       updateFiles: function(files)
-       {
-               var changed = false,
-                   myfiles = this.fields.files || [],
-                   keys = [ 'length', 'name', 'bytesCompleted', 'wanted', 'priority' ],
-                   i, f, j, key, myfile;
-
-               for (i=0; f=files[i]; ++i) {
-                       myfile = myfiles[i] || {};
-                       for (j=0; key=keys[j]; ++j)
-                               if(key in f)
-                                       changed |= this.setField(myfile,key,f[key]);
-                       myfiles[i] = myfile;
-               }
-               this.fields.files = myfiles;
-               return changed;
-       },
-
-       collateTrackers: function(trackers)
-       {
-               var i, t, announces = [];
-
-               for (i=0; t=trackers[i]; ++i)
-                       announces.push(t.announce.toLowerCase());
-               return announces.join('\t');
-       },
-
-       refreshFields: function(data)
-       {
-               var key,
-                   changed = false;
-
-               for (key in data) {
-                       switch (key) {
-                               case 'files':
-                               case 'fileStats': // merge files and fileStats together
-                                       changed |= this.updateFiles(data[key]);
-                                       break;
-                               case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
-                                       changed |= this.setField(this.fields,'trackers',data[key]);
-                                       break;
-                               case 'trackers': // ...so only save 'trackers' if we don't have it already
-                                       if (!(key in this.fields))
-                                               changed |= this.setField(this.fields,key,data[key]);
-                                       break;
-                               default:
-                                       changed |= this.setField(this.fields,key,data[key]);
-                       }
-               }
-
-               return changed;
-       },
-
-       refresh: function(data)
-       {
-               if (this.refreshFields(data))
-                       $(this).trigger('dataChanged', this);
-       },
-
-       /****
-       *****
-       ****/
-
-       // simple accessors
-       getComment: function() { return this.fields.comment; },
-       getCreator: function() { return this.fields.creator; },
-       getDateAdded: function() { return this.fields.addedDate; },
-       getDateCreated: function() { return this.fields.dateCreated; },
-       getDesiredAvailable: function() { return this.fields.desiredAvailable; },
-       getDownloadDir: function() { return this.fields.downloadDir; },
-       getDownloadSpeed: function() { return this.fields.rateDownload; },
-       getDownloadedEver: function() { return this.fields.downloadedEver; },
-       getError: function() { return this.fields.error; },
-       getErrorString: function() { return this.fields.errorString; },
-       getETA: function() { return this.fields.eta; },
-       getFailedEver: function(i) { return this.fields.corruptEver; },
-       getFile: function(i) { return this.fields.files[i]; },
-       getFileCount: function() { return this.fields.files ? this.fields.files.length : 0; },
-       getHashString: function() { return this.fields.hashString; },
-       getHave: function() { return this.getHaveValid() + this.getHaveUnchecked() },
-       getHaveUnchecked: function() { return this.fields.haveUnchecked; },
-       getHaveValid: function() { return this.fields.haveValid; },
-       getId: function() { return this.fields.id; },
-       getLastActivity: function() { return this.fields.activityDate; },
-       getLeftUntilDone: function() { return this.fields.leftUntilDone; },
-       getMetadataPercentComplete: function() { return this.fields.metadataPercentComplete; },
-       getName: function() { return this.fields.name || 'Unknown'; },
-       getPeers: function() { return this.fields.peers; },
-       getPeersConnected: function() { return this.fields.peersConnected; },
-       getPeersGettingFromUs: function() { return this.fields.peersGettingFromUs; },
-       getPeersSendingToUs: function() { return this.fields.peersSendingToUs; },
-       getPieceCount: function() { return this.fields.pieceCount; },
-       getPieceSize: function() { return this.fields.pieceSize; },
-       getPrivateFlag: function() { return this.fields.isPrivate; },
-       getQueuePosition: function() { return this.fields.queuePosition; },
-       getRecheckProgress: function() { return this.fields.recheckProgress; },
-       getSeedRatioLimit: function() { return this.fields.seedRatioLimit; },
-       getSeedRatioMode: function() { return this.fields.seedRatioMode; },
-       getSizeWhenDone: function() { return this.fields.sizeWhenDone; },
-       getStartDate: function() { return this.fields.startDate; },
-       getStatus: function() { return this.fields.status; },
-       getTotalSize: function() { return this.fields.totalSize; },
-       getTrackers: function() { return this.fields.trackers; },
-       getUploadSpeed: function() { return this.fields.rateUpload; },
-       getUploadRatio: function() { return this.fields.uploadRatio; },
-       getUploadedEver: function() { return this.fields.uploadedEver; },
-       getWebseedsSendingToUs: function() { return this.fields.webseedsSendingToUs; },
-       isFinished: function() { return this.fields.isFinished; },
-
-       // derived accessors
-       hasExtraInfo: function() { return 'hashString' in this.fields; },
-       isSeeding: function() { return this.getStatus() === Torrent._StatusSeed; },
-       isStopped: function() { return this.getStatus() === Torrent._StatusStopped; },
-       isChecking: function() { return this.getStatus() === Torrent._StatusCheck; },
-       isDownloading: function() { return this.getStatus() === Torrent._StatusDownload; },
-       isQueued: function() { return this.getStatus() === Torrent._StatusDownloadWait ||
-                                     this.getStatus() === Torrent._StatusSeedWait; },
-       isDone: function() { return this.getLeftUntilDone() < 1; },
-       needsMetaData: function(){ return this.getMetadataPercentComplete() < 1; },
-       getActivity: function() { return this.getDownloadSpeed() + this.getUploadSpeed(); },
-       getPercentDoneStr: function() { return Transmission.fmt.percentString(100*this.getPercentDone()); },
-       getPercentDone: function() { return this.fields.percentDone; },
-       getStateString: function() {
-               switch(this.getStatus()) {
-                       case Torrent._StatusStopped:        return this.isFinished() ? 'Seeding complete' : 'Paused';
-                       case Torrent._StatusCheckWait:      return 'Queued for verification';
-                       case Torrent._StatusCheck:          return 'Verifying local data';
-                       case Torrent._StatusDownloadWait:   return 'Queued for download';
-                       case Torrent._StatusDownload:       return 'Downloading';
-                       case Torrent._StatusSeedWait:       return 'Queued for seeding';
-                       case Torrent._StatusSeed:           return 'Seeding';
-                       case null:
-                       case undefined:                     return 'Unknown';
-                       default:                            return 'Error';
-               }
-       },
-       seedRatioLimit: function(controller){
-               switch(this.getSeedRatioMode()) {
-                       case Torrent._RatioUseGlobal: return controller.seedRatioLimit();
-                       case Torrent._RatioUseLocal:  return this.getSeedRatioLimit();
-                       default:                      return -1;
-               }
-       },
-       getErrorMessage: function() {
-               var str = this.getErrorString();
-               switch(this.getError()) {
-                       case Torrent._ErrTrackerWarning:
-                               return 'Tracker returned a warning: ' + str;
-                       case Torrent._ErrTrackerError:
-                               return 'Tracker returned an error: ' + str;
-                       case Torrent._ErrLocalError:
-                               return 'Error: ' + str;
-                       default:
-                               return null;
-               }
-       },
-       getCollatedName: function() {
-               var f = this.fields;
-               if (!f.collatedName && f.name)
-                       f.collatedName = f.name.toLowerCase();
-               return f.collatedName || '';
-       },
-       getCollatedTrackers: function() {
-               var f = this.fields;
-               if (!f.collatedTrackers && f.trackers)
-                       f.collatedTrackers = this.collateTrackers(f.trackers);
-               return f.collatedTrackers || '';
-       },
-
-       /****
-       *****
-       ****/
-
-       testState: function(state)
-       {
-               var s = this.getStatus();
-
-               switch(state)
-               {
-                       case Prefs._FilterActive:
-                               return this.getPeersGettingFromUs() > 0
-                                   || this.getPeersSendingToUs() > 0
-                                   || this.getWebseedsSendingToUs() > 0
-                                   || this.isChecking();
-                       case Prefs._FilterSeeding:
-                               return (s === Torrent._StatusSeed)
-                                   || (s === Torrent._StatusSeedWait);
-                       case Prefs._FilterDownloading:
-                               return (s === Torrent._StatusDownload)
-                                   || (s === Torrent._StatusDownloadWait);
-                       case Prefs._FilterPaused:
-                               return this.isStopped();
-                       case Prefs._FilterFinished:
-                               return this.isFinished();
-                       default:
-                               return true;
-               }
-       },
-
-       /**
-        * @param filter one of Prefs._Filter*
-        * @param search substring to look for, or null
-        * @return true if it passes the test, false if it fails
-        */
-       test: function(state, search, tracker)
-       {
-               // flter by state...
-               var pass = this.testState(state);
-
-               // maybe filter by text...
-               if (pass && search && search.length)
-                       pass = this.getCollatedName().indexOf(search.toLowerCase()) !== -1;
-
-               // maybe filter by tracker...
-               if (pass && tracker && tracker.length)
-                       pass = this.getCollatedTrackers().indexOf(tracker) !== -1;
-
-               return pass;
-       }
+ ****
+ ****  Methods
+ ****
+ ***/
+
+Torrent.prototype = {
+    initialize: function (data) {
+        this.fields = {};
+        this.fieldObservers = {};
+        this.refresh(data);
+    },
+
+    notifyOnFieldChange: function (field, callback) {
+        this.fieldObservers[field] = this.fieldObservers[field] || [];
+        this.fieldObservers[field].push(callback);
+    },
+
+    setField: function (o, name, value) {
+        var i, observer;
+
+        if (o[name] === value) {
+            return false;
+        };
+        if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) {
+            for (i = 0; observer = this.fieldObservers[name][i]; ++i) {
+                observer.call(this, value, o[name], name);
+            };
+        };
+        o[name] = value;
+        return true;
+    },
+
+    // fields.files is an array of unions of RPC's "files" and "fileStats" objects.
+    updateFiles: function (files) {
+        var changed = false;
+        var myfiles = this.fields.files || [];
+        var keys = ['length', 'name', 'bytesCompleted', 'wanted', 'priority'];
+        var i, f, j, key, myfile;
+
+        for (i = 0; f = files[i]; ++i) {
+            myfile = myfiles[i] || {};
+            for (j = 0; key = keys[j]; ++j) {
+                if (key in f) {
+                    changed |= this.setField(myfile, key, f[key]);
+                };
+            };
+            myfiles[i] = myfile;
+        }
+        this.fields.files = myfiles;
+        return changed;
+    },
+
+    collateTrackers: function (trackers) {
+        var i, t, announces = [];
+
+        for (i = 0; t = trackers[i]; ++i) {
+            announces.push(t.announce.toLowerCase());
+        };
+        return announces.join('\t');
+    },
+
+    refreshFields: function (data) {
+        var key;
+        var changed = false;
+
+        for (key in data) {
+            switch (key) {
+            case 'files':
+            case 'fileStats': // merge files and fileStats together
+                changed |= this.updateFiles(data[key]);
+                break;
+            case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
+                changed |= this.setField(this.fields, 'trackers', data[key]);
+                break;
+            case 'trackers': // ...so only save 'trackers' if we don't have it already
+                if (!(key in this.fields)) {
+                    changed |= this.setField(this.fields, key, data[key]);
+                };
+                break;
+            default:
+                changed |= this.setField(this.fields, key, data[key]);
+            };
+        };
+
+        return changed;
+    },
+
+    refresh: function (data) {
+        if (this.refreshFields(data)) {
+            $(this).trigger('dataChanged', this);
+        };
+    },
+
+    /****
+     *****
+     ****/
+
+    // simple accessors
+    getComment: function () {
+        return this.fields.comment;
+    },
+    getCreator: function () {
+        return this.fields.creator;
+    },
+    getDateAdded: function () {
+        return this.fields.addedDate;
+    },
+    getDateCreated: function () {
+        return this.fields.dateCreated;
+    },
+    getDesiredAvailable: function () {
+        return this.fields.desiredAvailable;
+    },
+    getDownloadDir: function () {
+        return this.fields.downloadDir;
+    },
+    getDownloadSpeed: function () {
+        return this.fields.rateDownload;
+    },
+    getDownloadedEver: function () {
+        return this.fields.downloadedEver;
+    },
+    getError: function () {
+        return this.fields.error;
+    },
+    getErrorString: function () {
+        return this.fields.errorString;
+    },
+    getETA: function () {
+        return this.fields.eta;
+    },
+    getFailedEver: function (i) {
+        return this.fields.corruptEver;
+    },
+    getFile: function (i) {
+        return this.fields.files[i];
+    },
+    getFileCount: function () {
+        return this.fields.files ? this.fields.files.length : 0;
+    },
+    getHashString: function () {
+        return this.fields.hashString;
+    },
+    getHave: function () {
+        return this.getHaveValid() + this.getHaveUnchecked()
+    },
+    getHaveUnchecked: function () {
+        return this.fields.haveUnchecked;
+    },
+    getHaveValid: function () {
+        return this.fields.haveValid;
+    },
+    getId: function () {
+        return this.fields.id;
+    },
+    getLastActivity: function () {
+        return this.fields.activityDate;
+    },
+    getLeftUntilDone: function () {
+        return this.fields.leftUntilDone;
+    },
+    getMetadataPercentComplete: function () {
+        return this.fields.metadataPercentComplete;
+    },
+    getName: function () {
+        return this.fields.name || 'Unknown';
+    },
+    getPeers: function () {
+        return this.fields.peers;
+    },
+    getPeersConnected: function () {
+        return this.fields.peersConnected;
+    },
+    getPeersGettingFromUs: function () {
+        return this.fields.peersGettingFromUs;
+    },
+    getPeersSendingToUs: function () {
+        return this.fields.peersSendingToUs;
+    },
+    getPieceCount: function () {
+        return this.fields.pieceCount;
+    },
+    getPieceSize: function () {
+        return this.fields.pieceSize;
+    },
+    getPrivateFlag: function () {
+        return this.fields.isPrivate;
+    },
+    getQueuePosition: function () {
+        return this.fields.queuePosition;
+    },
+    getRecheckProgress: function () {
+        return this.fields.recheckProgress;
+    },
+    getSeedRatioLimit: function () {
+        return this.fields.seedRatioLimit;
+    },
+    getSeedRatioMode: function () {
+        return this.fields.seedRatioMode;
+    },
+    getSizeWhenDone: function () {
+        return this.fields.sizeWhenDone;
+    },
+    getStartDate: function () {
+        return this.fields.startDate;
+    },
+    getStatus: function () {
+        return this.fields.status;
+    },
+    getTotalSize: function () {
+        return this.fields.totalSize;
+    },
+    getTrackers: function () {
+        return this.fields.trackers;
+    },
+    getUploadSpeed: function () {
+        return this.fields.rateUpload;
+    },
+    getUploadRatio: function () {
+        return this.fields.uploadRatio;
+    },
+    getUploadedEver: function () {
+        return this.fields.uploadedEver;
+    },
+    getWebseedsSendingToUs: function () {
+        return this.fields.webseedsSendingToUs;
+    },
+    isFinished: function () {
+        return this.fields.isFinished;
+    },
+
+    // derived accessors
+    hasExtraInfo: function () {
+        return 'hashString' in this.fields;
+    },
+    isSeeding: function () {
+        return this.getStatus() === Torrent._StatusSeed;
+    },
+    isStopped: function () {
+        return this.getStatus() === Torrent._StatusStopped;
+    },
+    isChecking: function () {
+        return this.getStatus() === Torrent._StatusCheck;
+    },
+    isDownloading: function () {
+        return this.getStatus() === Torrent._StatusDownload;
+    },
+    isQueued: function () {
+        return this.getStatus() === Torrent._StatusDownloadWait || this.getStatus() === Torrent._StatusSeedWait;
+    },
+    isDone: function () {
+        return this.getLeftUntilDone() < 1;
+    },
+    needsMetaData: function () {
+        return this.getMetadataPercentComplete() < 1;
+    },
+    getActivity: function () {
+        return this.getDownloadSpeed() + this.getUploadSpeed();
+    },
+    getPercentDoneStr: function () {
+        return Transmission.fmt.percentString(100 * this.getPercentDone());
+    },
+    getPercentDone: function () {
+        return this.fields.percentDone;
+    },
+    getStateString: function () {
+        switch (this.getStatus()) {
+        case Torrent._StatusStopped:
+            return this.isFinished() ? 'Seeding complete' : 'Paused';
+        case Torrent._StatusCheckWait:
+            return 'Queued for verification';
+        case Torrent._StatusCheck:
+            return 'Verifying local data';
+        case Torrent._StatusDownloadWait:
+            return 'Queued for download';
+        case Torrent._StatusDownload:
+            return 'Downloading';
+        case Torrent._StatusSeedWait:
+            return 'Queued for seeding';
+        case Torrent._StatusSeed:
+            return 'Seeding';
+        case null:
+        case undefined:
+            return 'Unknown';
+        default:
+            return 'Error';
+        }
+    },
+    seedRatioLimit: function (controller) {
+        switch (this.getSeedRatioMode()) {
+        case Torrent._RatioUseGlobal:
+            return controller.seedRatioLimit();
+        case Torrent._RatioUseLocal:
+            return this.getSeedRatioLimit();
+        default:
+            return -1;
+        }
+    },
+    getErrorMessage: function () {
+        var str = this.getErrorString();
+        switch (this.getError()) {
+        case Torrent._ErrTrackerWarning:
+            return 'Tracker returned a warning: ' + str;
+        case Torrent._ErrTrackerError:
+            return 'Tracker returned an error: ' + str;
+        case Torrent._ErrLocalError:
+            return 'Error: ' + str;
+        default:
+            return null;
+        }
+    },
+    getCollatedName: function () {
+        var f = this.fields;
+        if (!f.collatedName && f.name) {
+            f.collatedName = f.name.toLowerCase();
+        };
+        return f.collatedName || '';
+    },
+    getCollatedTrackers: function () {
+        var f = this.fields;
+        if (!f.collatedTrackers && f.trackers) {
+            f.collatedTrackers = this.collateTrackers(f.trackers);
+        };
+        return f.collatedTrackers || '';
+    },
+
+    /****
+     *****
+     ****/
+
+    testState: function (state) {
+        var s = this.getStatus();
+
+        switch (state) {
+        case Prefs._FilterActive:
+            return this.getPeersGettingFromUs() > 0 || this.getPeersSendingToUs() > 0 || this.getWebseedsSendingToUs() > 0 || this.isChecking();
+        case Prefs._FilterSeeding:
+            return (s === Torrent._StatusSeed) || (s === Torrent._StatusSeedWait);
+        case Prefs._FilterDownloading:
+            return (s === Torrent._StatusDownload) || (s === Torrent._StatusDownloadWait);
+        case Prefs._FilterPaused:
+            return this.isStopped();
+        case Prefs._FilterFinished:
+            return this.isFinished();
+        default:
+            return true;
+        }
+    },
+
+    /**
+     * @param filter one of Prefs._Filter*
+     * @param search substring to look for, or null
+     * @return true if it passes the test, false if it fails
+     */
+    test: function (state, search, tracker) {
+        // flter by state...
+        var pass = this.testState(state);
+
+        // maybe filter by text...
+        if (pass && search && search.length) {
+            pass = this.getCollatedName().indexOf(search.toLowerCase()) !== -1;
+        };
+
+        // maybe filter by tracker...
+        if (pass && tracker && tracker.length) {
+            pass = this.getCollatedTrackers().indexOf(tracker) !== -1;
+        };
+
+        return pass;
+    }
 };
 
-
 /***
-****
-****  SORTING
-****
-***/
-
-Torrent.compareById = function(ta, tb)
-{
-       return ta.getId() - tb.getId();
+ ****
+ ****  SORTING
+ ****
+ ***/
+
+Torrent.compareById = function (ta, tb) {
+    return ta.getId() - tb.getId();
 };
-Torrent.compareByName = function(ta, tb)
-{
-       return ta.getCollatedName().localeCompare(tb.getCollatedName())
-           || Torrent.compareById(ta, tb);
+Torrent.compareByName = function (ta, tb) {
+    return ta.getCollatedName().localeCompare(tb.getCollatedName()) || Torrent.compareById(ta, tb);
 };
-Torrent.compareByQueue = function(ta, tb)
-{
-       return ta.getQueuePosition() - tb.getQueuePosition();
+Torrent.compareByQueue = function (ta, tb) {
+    return ta.getQueuePosition() - tb.getQueuePosition();
 };
-Torrent.compareByAge = function(ta, tb)
-{
-       var a = ta.getDateAdded(),
-           b = tb.getDateAdded();
+Torrent.compareByAge = function (ta, tb) {
+    var a = ta.getDateAdded();
+    var b = tb.getDateAdded();
 
-       return (b - a) || Torrent.compareByQueue(ta, tb);
+    return (b - a) || Torrent.compareByQueue(ta, tb);
 };
-Torrent.compareByState = function(ta, tb)
-{
-       var a = ta.getStatus(),
-           b = tb.getStatus();
+Torrent.compareByState = function (ta, tb) {
+    var a = ta.getStatus();
+    var b = tb.getStatus();
 
-       return (b - a) || Torrent.compareByQueue(ta, tb);
+    return (b - a) || Torrent.compareByQueue(ta, tb);
 };
-Torrent.compareByActivity = function(ta, tb)
-{
-       var a = ta.getActivity(),
-           b = tb.getActivity();
+Torrent.compareByActivity = function (ta, tb) {
+    var a = ta.getActivity();
+    var b = tb.getActivity();
 
-       return (b - a) || Torrent.compareByState(ta, tb);
+    return (b - a) || Torrent.compareByState(ta, tb);
 };
-Torrent.compareByRatio = function(ta, tb)
-{
-       var a = ta.getUploadRatio(),
-           b = tb.getUploadRatio();
-
-       if (a < b) return 1;
-       if (a > b) return -1;
-       return Torrent.compareByState(ta, tb);
+Torrent.compareByRatio = function (ta, tb) {
+    var a = ta.getUploadRatio();
+    var b = tb.getUploadRatio();
+
+    if (a < b) {
+        return 1;
+    };
+    if (a > b) {
+        return -1;
+    };
+    return Torrent.compareByState(ta, tb);
 };
-Torrent.compareByProgress = function(ta, tb)
-{
-       var a = ta.getPercentDone(),
-           b = tb.getPercentDone();
+Torrent.compareByProgress = function (ta, tb) {
+    var a = ta.getPercentDone();
+    var b = tb.getPercentDone();
 
-       return (a - b) || Torrent.compareByRatio(ta, tb);
+    return (a - b) || Torrent.compareByRatio(ta, tb);
 };
-Torrent.compareBySize = function(ta, tb)
-{
-       var a = ta.getTotalSize(),
-           b = tb.getTotalSize();
+Torrent.compareBySize = function (ta, tb) {
+    var a = ta.getTotalSize();
+    var b = tb.getTotalSize();
 
-       return (a - b) || Torrent.compareByName(ta, tb);
+    return (a - b) || Torrent.compareByName(ta, tb);
 };
 
-Torrent.compareTorrents = function(a, b, sortMethod, sortDirection)
-{
-       var i;
-
-       switch(sortMethod)
-       {
-               case Prefs._SortByActivity:
-                       i = Torrent.compareByActivity(a,b);
-                       break;
-               case Prefs._SortByAge:
-                       i = Torrent.compareByAge(a,b);
-                       break;
-               case Prefs._SortByQueue:
-                       i = Torrent.compareByQueue(a,b);
-                       break;
-               case Prefs._SortByProgress:
-                       i = Torrent.compareByProgress(a,b);
-                       break;
-               case Prefs._SortBySize:
-                       i = Torrent.compareBySize(a,b);
-                       break;
-               case Prefs._SortByState:
-                       i = Torrent.compareByState(a,b);
-                       break;
-               case Prefs._SortByRatio:
-                       i = Torrent.compareByRatio(a,b);
-                       break;
-               default:
-                       i = Torrent.compareByName(a,b);
-                       break;
-       }
-
-       if (sortDirection === Prefs._SortDescending)
-               i = -i;
-
-       return i;
+Torrent.compareTorrents = function (a, b, sortMethod, sortDirection) {
+    var i;
+
+    switch (sortMethod) {
+    case Prefs._SortByActivity:
+        i = Torrent.compareByActivity(a, b);
+        break;
+    case Prefs._SortByAge:
+        i = Torrent.compareByAge(a, b);
+        break;
+    case Prefs._SortByQueue:
+        i = Torrent.compareByQueue(a, b);
+        break;
+    case Prefs._SortByProgress:
+        i = Torrent.compareByProgress(a, b);
+        break;
+    case Prefs._SortBySize:
+        i = Torrent.compareBySize(a, b);
+        break;
+    case Prefs._SortByState:
+        i = Torrent.compareByState(a, b);
+        break;
+    case Prefs._SortByRatio:
+        i = Torrent.compareByRatio(a, b);
+        break;
+    default:
+        i = Torrent.compareByName(a, b);
+        break;
+    };
+
+    if (sortDirection === Prefs._SortDescending) {
+        i = -i;
+    };
+
+    return i;
 };
 
 /**
@@ -473,38 +579,37 @@ Torrent.compareTorrents = function(a, b, sortMethod, sortDirection)
  * @param sortMethod one of Prefs._SortBy*
  * @param sortDirection Prefs._SortAscending or Prefs._SortDescending
  */
-Torrent.sortTorrents = function(torrents, sortMethod, sortDirection)
-{
-       switch(sortMethod)
-       {
-               case Prefs._SortByActivity:
-                       torrents.sort(this.compareByActivity);
-                       break;
-               case Prefs._SortByAge:
-                       torrents.sort(this.compareByAge);
-                       break;
-               case Prefs._SortByQueue:
-                       torrents.sort(this.compareByQueue);
-                       break;
-               case Prefs._SortByProgress:
-                       torrents.sort(this.compareByProgress);
-                       break;
-               case Prefs._SortBySize:
-                       torrents.sort(this.compareBySize);
-                       break;
-               case Prefs._SortByState:
-                       torrents.sort(this.compareByState);
-                       break;
-               case Prefs._SortByRatio:
-                       torrents.sort(this.compareByRatio);
-                       break;
-               default:
-                       torrents.sort(this.compareByName);
-                       break;
-       }
-
-       if (sortDirection === Prefs._SortDescending)
-               torrents.reverse();
-
-       return torrents;
+Torrent.sortTorrents = function (torrents, sortMethod, sortDirection) {
+    switch (sortMethod) {
+    case Prefs._SortByActivity:
+        torrents.sort(this.compareByActivity);
+        break;
+    case Prefs._SortByAge:
+        torrents.sort(this.compareByAge);
+        break;
+    case Prefs._SortByQueue:
+        torrents.sort(this.compareByQueue);
+        break;
+    case Prefs._SortByProgress:
+        torrents.sort(this.compareByProgress);
+        break;
+    case Prefs._SortBySize:
+        torrents.sort(this.compareBySize);
+        break;
+    case Prefs._SortByState:
+        torrents.sort(this.compareByState);
+        break;
+    case Prefs._SortByRatio:
+        torrents.sort(this.compareByRatio);
+        break;
+    default:
+        torrents.sort(this.compareByName);
+        break;
+    };
+
+    if (sortDirection === Prefs._SortDescending) {
+        torrents.reverse();
+    };
+
+    return torrents;
 };
index 82fa1176c251564694cc6aa8ac580a36a5818cf5..45c1b254f5ebef3707ad2a97ec76c791d9c89780 100644 (file)
  * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  */
 
-function Transmission()
-{
-       this.initialize();
+function Transmission() {
+    this.initialize();
 }
 
-Transmission.prototype =
-{
-       /****
-       *****
-       *****  STARTUP
-       *****
-       ****/
-
-       initialize: function()
-       {
-               var e;
-
-               // Initialize the helper classes
-               this.remote = new TransmissionRemote(this);
-               this.inspector = new Inspector(this, this.remote);
-               this.prefsDialog = new PrefsDialog(this.remote);
-               $(this.prefsDialog).bind('closed', $.proxy(this.onPrefsDialogClosed,this));
-
-               this.isMenuEnabled = !isMobileDevice;
-
-               // Initialize the implementation fields
-               this.filterText    = '';
-               this._torrents     = {};
-               this._rows         = [];
-               this.dirtyTorrents = {};
-               this.uriCache      = {};
-
-               // Initialize the clutch preferences
-               Prefs.getClutchPrefs(this);
-
-               // Set up user events
-               $('#toolbar-pause').click($.proxy(this.stopSelectedClicked,this));
-               $('#toolbar-start').click($.proxy(this.startSelectedClicked,this));
-               $('#toolbar-pause-all').click($.proxy(this.stopAllClicked,this));
-               $('#toolbar-start-all').click($.proxy(this.startAllClicked,this));
-               $('#toolbar-remove').click($.proxy(this.removeClicked,this));
-               $('#toolbar-open').click($.proxy(this.openTorrentClicked,this));
-
-               $('#prefs-button').click($.proxy(this.togglePrefsDialogClicked,this));
-
-               $('#upload_confirm_button').click($.proxy(this.confirmUploadClicked,this));
-               $('#upload_cancel_button').click($.proxy(this.hideUploadDialog,this));
-
-               $('#rename_confirm_button').click($.proxy(this.confirmRenameClicked,this));
-               $('#rename_cancel_button').click($.proxy(this.hideRenameDialog,this));
-
-
-               $('#move_confirm_button').click($.proxy(this.confirmMoveClicked,this));
-               $('#move_cancel_button').click($.proxy(this.hideMoveDialog,this));
-
-               $('#turtle-button').click($.proxy(this.toggleTurtleClicked,this));
-               $('#compact-button').click($.proxy(this.toggleCompactClicked,this));
-
-               // tell jQuery to copy the dataTransfer property from events over if it exists
-               jQuery.event.props.push("dataTransfer");
-
-               $('#torrent_upload_form').submit(function() { $('#upload_confirm_button').click(); return false; });
-
-               $('#toolbar-inspector').click($.proxy(this.toggleInspector,this));
-
-               e = $('#filter-mode');
-               e.val(this[Prefs._FilterMode]);
-               e.change($.proxy(this.onFilterModeClicked,this));
-               $('#filter-tracker').change($.proxy(this.onFilterTrackerClicked,this));
-
-               if (!isMobileDevice) {
-                       $(document).bind('keydown', $.proxy(this.keyDown,this) );
-                       $(document).bind('keyup', $.proxy(this.keyUp, this) );
-                       $('#torrent_container').click( $.proxy(this.deselectAll,this) );
-                       $('#torrent_container').bind('dragover', $.proxy(this.dragenter,this));
-                       $('#torrent_container').bind('dragenter', $.proxy(this.dragenter,this));
-                       $('#torrent_container').bind('drop', $.proxy(this.drop,this));
-                       $('#inspector_link').click( $.proxy(this.toggleInspector,this) );
-
-                       this.setupSearchBox();
-                       this.createContextMenu();
-               }
-
-               if (this.isMenuEnabled)
-                       this.createSettingsMenu();
-
-               e = {};
-               e.torrent_list              = $('#torrent_list')[0];
-               e.toolbar_buttons           = $('#toolbar ul li');
-               e.toolbar_pause_button      = $('#toolbar-pause')[0];
-               e.toolbar_start_button      = $('#toolbar-start')[0];
-               e.toolbar_remove_button     = $('#toolbar-remove')[0];
-               this.elements = e;
-
-               // Apply the prefs settings to the gui
-               this.initializeSettings();
-
-               // Get preferences & torrents from the daemon
-               var async = false;
-               this.loadDaemonPrefs(async);
-               this.loadDaemonStats(async);
-               this.initializeTorrents();
-               this.refreshTorrents();
-               this.togglePeriodicSessionRefresh(true);
-
-               this.updateButtonsSoon();
-       },
-
-       loadDaemonPrefs: function(async) {
-               this.remote.loadDaemonPrefs(function(data) {
-                       var o = data['arguments'];
-                       Prefs.getClutchPrefs(o);
-                       this.updateGuiFromSession(o);
-                       this.sessionProperties = o;
-               }, this, async);
-       },
-
-       loadImages: function() {
-               for (var i=0, row; row=arguments[i]; ++i)
-                       jQuery("<img>").attr("src", row);
-       },
-
-       /*
-        * Load the clutch prefs and init the GUI according to those prefs
-        */
-       initializeSettings: function()
-       {
-               Prefs.getClutchPrefs(this);
-
-               if (this.isMenuEnabled)
-               {
-                       $('#sort_by_' + this[Prefs._SortMethod]).selectMenuItem();
-
-                       if (this[Prefs._SortDirection] === Prefs._SortDescending)
-                               $('#reverse_sort_order').selectMenuItem();
-               }
-
-               this.initCompactMode();
-       },
-
-       /*
-        * Set up the search box
-        */
-       setupSearchBox: function()
-       {
-               var tr = this;
-               var search_box = $('#torrent_search');
-               search_box.bind('keyup click', function() {
-                       tr.setFilterText(this.value);
-               });
-               if (!$.browser.safari)
-               {
-                       search_box.addClass('blur');
-                       search_box[0].value = 'Filter';
-                       search_box.bind('blur', function() {
-                               if (this.value === '') {
-                                       $(this).addClass('blur');
-                                       this.value = 'Filter';
-                                       tr.setFilterText(null);
-                               }
-                       }).bind('focus', function() {
-                               if ($(this).is('.blur')) {
-                                       this.value = '';
-                                       $(this).removeClass('blur');
-                               }
-                       });
-               }
-       },
-
-       /**
-        * Create the torrent right-click menu
-        */
-       createContextMenu: function() {
-               var tr = this;
-               var bindings = {
-                       pause_selected:       function() { tr.stopSelectedTorrents(); },
-                       resume_selected:      function() { tr.startSelectedTorrents(false); },
-                       resume_now_selected:  function() { tr.startSelectedTorrents(true); },
-                       move:                 function() { tr.moveSelectedTorrents(false); },
-                       remove:               function() { tr.removeSelectedTorrents(); },
-                       remove_data:          function() { tr.removeSelectedTorrentsAndData(); },
-                       verify:               function() { tr.verifySelectedTorrents(); },
-                       rename:               function() { tr.renameSelectedTorrents(); },
-                       reannounce:           function() { tr.reannounceSelectedTorrents(); },
-                       move_top:             function() { tr.moveTop(); },
-                       move_up:              function() { tr.moveUp(); },
-                       move_down:            function() { tr.moveDown(); },
-                       move_bottom:          function() { tr.moveBottom(); },
-                       select_all:           function() { tr.selectAll(); },
-                       deselect_all:         function() { tr.deselectAll(); }
-               };
-
-               // Set up the context menu
-               $("ul#torrent_list").contextmenu({
-                       delegate: ".torrent",
-                       menu: "#torrent_context_menu",
-                       preventSelect: true,
-                       taphold: true,
-                       show: { effect: "none" },
-                       hide: { effect: "none" },
-                       select: function(event, ui) { bindings[ui.cmd](); },
-                       beforeOpen: $.proxy(function(event, ui) {
-                               var element = $(event.currentTarget);
-                               var i = $('#torrent_list > li').index(element);
-                               if ((i!==-1) && !this._rows[i].isSelected())
-                                       this.setSelectedRow(this._rows[i]);
-
-                               this.calculateTorrentStates(function(s) {
-                                       var tl = $(event.target);
-                                       tl.contextmenu("enableEntry", "pause_selected", s.activeSel > 0);
-                                       tl.contextmenu("enableEntry", "resume_selected", s.pausedSel > 0);
-                                       tl.contextmenu("enableEntry", "resume_now_selected", s.pausedSel > 0 || s.queuedSel > 0);
-                                       tl.contextmenu("enableEntry", "rename", s.sel == 1);
-                               });
-                       }, this)
-               });
-       },
-
-       createSettingsMenu: function() {
-               $("#footer_super_menu").transMenu({
-                       open: function() { $("#settings_menu").addClass("selected"); },
-                       close: function() { $("#settings_menu").removeClass("selected"); },
-                       select: $.proxy(this.onMenuClicked, this)
-               });
-               $("#settings_menu").click(function(event) {
-                       $("#footer_super_menu").transMenu("open");
-               });
-       },
-
-       /****
-       *****
-       ****/
-
-       updateFreeSpaceInAddDialog: function()
-       {
-               var formdir = $('input#add-dialog-folder-input').val();
-               this.remote.getFreeSpace (formdir, this.onFreeSpaceResponse, this);
-       },
-
-       onFreeSpaceResponse: function(dir, bytes)
-       {
-               var e, str, formdir;
-
-               formdir = $('input#add-dialog-folder-input').val();
-               if (formdir == dir)
-               {
-                       e = $('label#add-dialog-folder-label');
-                       if (bytes > 0)
-                               str = '  <i>(' + Transmission.fmt.size(bytes) + ' Free)</i>';
-                       else
-                               str = '';
-                       e.html ('Destination folder' + str + ':');
-               }
-       },
-
-
-       /****
-       *****
-       *****  UTILITIES
-       *****
-       ****/
-
-       getAllTorrents: function()
-       {
-               var torrents = [];
-               for (var key in this._torrents)
-                       torrents.push(this._torrents[key]);
-               return torrents;
-       },
-
-       getTorrentIds: function(torrents)
-       {
-               return $.map(torrents.slice(0), function(t) {return t.getId();});
-       },
-
-       scrollToRow: function(row)
-       {
-               if (isMobileDevice) // FIXME: why?
-                       return;
-
-               var list = $('#torrent_container'),
-                   scrollTop = list.scrollTop(),
-                   innerHeight = list.innerHeight(),
-                   offsetTop = row.getElement().offsetTop,
-                   offsetHeight = $(row.getElement()).outerHeight();
-
-               if (offsetTop < scrollTop)
-                       list.scrollTop(offsetTop);
-               else if (innerHeight + scrollTop < offsetTop + offsetHeight)
-                       list.scrollTop(offsetTop + offsetHeight - innerHeight);
-       },
-
-       seedRatioLimit: function() {
-               var p = this.sessionProperties;
-               if (p && p.seedRatioLimited)
-                       return p.seedRatioLimit;
-               return -1;
-       },
-
-       setPref: function(key, val)
-       {
-               this[key] = val;
-               Prefs.setValue(key, val);
-       },
-
-       /****
-       *****
-       *****  SELECTION
-       *****
-       ****/
-
-       getSelectedRows: function() {
-               return $.grep(this._rows, function(r) {return r.isSelected();});
-       },
-
-       getSelectedTorrents: function() {
-               return $.map(this.getSelectedRows(),function(r) {
-                       return r.getTorrent();
-               });
-       },
-
-       getSelectedTorrentIds: function() {
-               return this.getTorrentIds(this.getSelectedTorrents());
-       },
-
-       setSelectedRow: function(row) {
-               $(this.elements.torrent_list).children('.selected').removeClass('selected');
-               this.selectRow(row);
-       },
-
-       selectRow: function(row) {
-               $(row.getElement()).addClass('selected');
-               this.callSelectionChangedSoon();
-       },
-
-       deselectRow: function(row) {
-               $(row.getElement()).removeClass('selected');
-               this.callSelectionChangedSoon();
-       },
-
-       selectAll: function() {
-               $(this.elements.torrent_list).children().addClass('selected');
-               this.callSelectionChangedSoon();
-       },
-       deselectAll: function() {
-               $(this.elements.torrent_list).children('.selected').removeClass('selected');
-               this.callSelectionChangedSoon();
-               delete this._last_torrent_clicked;
-       },
-
-       indexOfLastTorrent: function() {
-               for (var i=0, r; r=this._rows[i]; ++i)
-                       if (r.getTorrentId() === this._last_torrent_clicked)
-                               return i;
-               return -1;
-       },
-
-       // Select a range from this row to the last clicked torrent
-       selectRange: function(row)
-       {
-               var last = this.indexOfLastTorrent();
-
-               if (last === -1)
-               {
-                       this.selectRow(row);
-               }
-               else // select the range between the prevous & current
-               {
-                       var next = this._rows.indexOf(row);
-                       var min = Math.min(last, next);
-                       var max = Math.max(last, next);
-                       for (var i=min; i<=max; ++i)
-                               this.selectRow(this._rows[i]);
-               }
-
-               this.callSelectionChangedSoon();
-       },
-
-       selectionChanged: function()
-       {
-               this.updateButtonStates();
-
-               this.inspector.setTorrents(this.inspectorIsVisible() ? this.getSelectedTorrents() : []);
-
-               clearTimeout(this.selectionChangedTimer);
-               delete this.selectionChangedTimer;
-
-       },
-
-       callSelectionChangedSoon: function()
-       {
-               if (!this.selectionChangedTimer)
-               {
-                       var callback = $.proxy(this.selectionChanged,this),
-                           msec = 200;
-                       this.selectionChangedTimer = setTimeout(callback, msec);
-               }
-       },
-
-       /*--------------------------------------------
-        *
-        *  E V E N T   F U N C T I O N S
-        *
-        *--------------------------------------------*/
-
-       /*
-        * Process key event
-        */
-       keyDown: function(ev)
-       {
-               var handled = false,
-                   rows = this._rows,
-                   up = ev.keyCode === 38, // up key pressed
-                   dn = ev.keyCode === 40, // down key pressed
-                   shift = ev.keyCode === 16; // shift key pressed
-
-               if ((up || dn) && rows.length)
-               {
-                       var last = this.indexOfLastTorrent(),
-                           i = last,
-                           anchor = this._shift_index,
-                           r,
-                           min = 0,
-                           max = rows.length - 1;
-
-                       if (dn && (i+1 <= max))
-                               ++i;
-                       else if (up && (i-1 >= min))
-                               --i;
-
-                       var r = rows[i];
-
-                       if (anchor >= 0)
-                       {
-                               // user is extending the selection
-                               // with the shift + arrow keys...
-                               if (   ((anchor <= last) && (last < i))
-                                   || ((anchor >= last) && (last > i)))
-                               {
-                                       this.selectRow(r);
-                               }
-                               else if (((anchor >= last) && (i > last))
-                                     || ((anchor <= last) && (last > i)))
-                               {
-                                       this.deselectRow(rows[last]);
-                               }
-                       }
-                       else
-                       {
-                               if (ev.shiftKey)
-                                       this.selectRange(r);
-                               else
-                                       this.setSelectedRow(r);
-                       }
-                       this._last_torrent_clicked = r.getTorrentId();
-                       this.scrollToRow(r);
-                       handled = true;
-               }
-               else if (shift)
-               {
-                       this._shift_index = this.indexOfLastTorrent();
-               }
-
-               return !handled;
-       },
-
-       keyUp: function(ev) {
-               if (ev.keyCode === 16) // shift key pressed
-                       delete this._shift_index;
-       },
-
-       isButtonEnabled: function(ev) {
-               var p = (ev.target || ev.srcElement).parentNode;
-               return p.className!=='disabled'
-                   && p.parentNode.className!=='disabled';
-       },
-
-       stopSelectedClicked: function(ev) {
-               if (this.isButtonEnabled(ev)) {
-                       this.stopSelectedTorrents();
-                       this.hideMobileAddressbar();
-               }
-       },
-
-       startSelectedClicked: function(ev) {
-               if (this.isButtonEnabled(ev)) {
-                       this.startSelectedTorrents(false);
-                       this.hideMobileAddressbar();
-               }
-       },
-
-       stopAllClicked: function(ev) {
-               if (this.isButtonEnabled(ev)) {
-                       this.stopAllTorrents();
-                       this.hideMobileAddressbar();
-               }
-       },
-
-       startAllClicked: function(ev) {
-               if (this.isButtonEnabled(ev)) {
-                       this.startAllTorrents(false);
-                       this.hideMobileAddressbar();
-               }
-       },
-
-       openTorrentClicked: function(ev) {
-               if (this.isButtonEnabled(ev)) {
-                       $('body').addClass('open_showing');
-                       this.uploadTorrentFile();
-                       this.updateButtonStates();
-               }
-       },
-
-       dragenter: function(ev) {
-               if (ev.dataTransfer && ev.dataTransfer.types) {
-                       var types = ["text/uri-list", "text/plain"];
-                       for (var i = 0; i < types.length; ++i) {
-                               // it would be better to look at the links here;
-                               // sadly, with Firefox, trying would throw.
-                               if (ev.dataTransfer.types.contains(types[i])) {
-                                       ev.stopPropagation();
-                                       ev.preventDefault();
-                                       ev.dropEffect = "copy";
-                                       return false;
-                               }
-                       }
-               }
-               else if (ev.dataTransfer) {
-                       ev.dataTransfer.dropEffect = "none";
-               }
-               return true;
-       },
-
-       drop: function(ev)
-       {
-               var i, uri, uris=null,
-                   types = ["text/uri-list", "text/plain"],
-                   paused = this.shouldAddedTorrentsStart();
-
-               if (!ev.dataTransfer || !ev.dataTransfer.types)
-                       return true;
-
-               for (i=0; !uris && i<types.length; ++i)
-                       if (ev.dataTransfer.types.contains(types[i]))
-                               uris = ev.dataTransfer.getData(types[i]).split("\n");
-
-               for (i=0; uri=uris[i]; ++i) {
-                       if (/^#/.test(uri)) // lines which start with "#" are comments
-                               continue;
-                       if (/^[a-z-]+:/i.test(uri)) // close enough to a url
-                               this.remote.addTorrentByUrl(uri, paused);
-               }
-
-               ev.preventDefault();
-               return false;
-       },
-
-       hideUploadDialog: function() {
-               $('body.open_showing').removeClass('open_showing');
-               $('#upload_container').hide();
-               this.updateButtonStates();
-       },
-
-       confirmUploadClicked: function() {
-               this.uploadTorrentFile(true);
-               this.hideUploadDialog();
-       },
-
-       hideMoveDialog: function() {
-               $('#move_container').hide();
-               this.updateButtonStates();
-       },
-
-       confirmMoveClicked: function() {
-               this.moveSelectedTorrents(true);
-               this.hideUploadDialog();
-       },
-
-       hideRenameDialog: function() {
-               $('body.open_showing').removeClass('open_showing');
-               $('#rename_container').hide();
-       },
-
-       confirmRenameClicked: function() {
-               var torrents = this.getSelectedTorrents();
-               this.renameTorrent(torrents[0], $('input#torrent_rename_name').attr('value'));
-               this.hideRenameDialog();
-       },
-
-       removeClicked: function(ev) {
-               if (this.isButtonEnabled(ev)) {
-                       this.removeSelectedTorrents();
-                       this.hideMobileAddressbar();
-               }
-       },
-
-       // turn the periodic ajax session refresh on & off
-       togglePeriodicSessionRefresh: function(enabled) {
-               clearInterval(this.sessionInterval);
-               delete this.sessionInterval;
-               if (enabled) {
-                       var callback = $.proxy(this.loadDaemonPrefs,this),
-                           msec = 8000;
-                       this.sessionInterval = setInterval(callback, msec);
-               }
-       },
-
-       toggleTurtleClicked: function()
-       {
-               var o = {};
-               o[RPC._TurtleState] = !$('#turtle-button').hasClass('selected');
-               this.remote.savePrefs(o);
-       },
-
-       /*--------------------------------------------
-        *
-        *  I N T E R F A C E   F U N C T I O N S
-        *
-        *--------------------------------------------*/
-
-       onPrefsDialogClosed: function() {
-               $('#prefs-button').removeClass('selected');
-       },
-
-       togglePrefsDialogClicked: function(ev)
-       {
-               var e = $('#prefs-button');
-
-               if (e.hasClass('selected'))
-                       this.prefsDialog.close();
-               else {
-                       e.addClass('selected');
-                       this.prefsDialog.show();
-               }
-       },
-
-       setFilterText: function(search) {
-               this.filterText = search ? search.trim() : null;
-               this.refilter(true);
-       },
-
-       setSortMethod: function(sort_method) {
-               this.setPref(Prefs._SortMethod, sort_method);
-               this.refilter(true);
-       },
-
-       setSortDirection: function(direction) {
-               this.setPref(Prefs._SortDirection, direction);
-               this.refilter(true);
-       },
-
-       onMenuClicked: function(event, ui)
-       {
-               var o, dir,
-                   id = ui.id,
-                   remote = this.remote,
-                   element = ui.target;
-
-               if (ui.group == 'sort-mode')
-               {
-                       element.selectMenuItem();
-                       this.setSortMethod(id.replace(/sort_by_/, ''));
-               }
-               else if (element.hasClass('upload-speed'))
-               {
-                       o = {};
-                       o[RPC._UpSpeedLimit] = parseInt(element.text());
-                       o[RPC._UpSpeedLimited] = true;
-                       remote.savePrefs(o);
-               }
-               else if (element.hasClass('download-speed'))
-               {
-                       o = {};
-                       o[RPC._DownSpeedLimit] = parseInt(element.text());
-                       o[RPC._DownSpeedLimited] = true;
-                       remote.savePrefs(o);
-               }
-               else switch (id)
-               {
-                       case 'statistics':
-                               this.showStatsDialog();
-                               break;
-
-                       case 'about-button':
-                               o = 'Transmission ' + this.serverVersion;
-                               $('#about-dialog #about-title').html(o);
-                               $('#about-dialog').dialog({
-                                       title: 'About',
-                                       show: 'fade',
-                                       hide: 'fade'
-                               });
-                               break;
-
-                       case 'homepage':
-                               window.open('http://www.transmissionbt.com/');
-                               break;
-
-                       case 'tipjar':
-                               window.open('http://www.transmissionbt.com/donate.php');
-                               break;
-
-                       case 'unlimited_download_rate':
-                               o = {};
-                               o[RPC._DownSpeedLimited] = false;
-                               remote.savePrefs(o);
-                               break;
-
-                       case 'limited_download_rate':
-                               o = {};
-                               o[RPC._DownSpeedLimited] = true;
-                               remote.savePrefs(o);
-                               break;
-
-                       case 'unlimited_upload_rate':
-                               o = {};
-                               o[RPC._UpSpeedLimited] = false;
-                               remote.savePrefs(o);
-                               break;
-
-                       case 'limited_upload_rate':
-                               o = {};
-                               o[RPC._UpSpeedLimited] = true;
-                               remote.savePrefs(o);
-                               break;
-
-                       case 'reverse_sort_order':
-                               if (element.menuItemIsSelected()) {
-                                       dir = Prefs._SortAscending;
-                                       element.deselectMenuItem();
-                               } else {
-                                       dir = Prefs._SortDescending;
-                                       element.selectMenuItem();
-                               }
-                               this.setSortDirection(dir);
-                               break;
-
-                       case 'toggle_notifications':
-                               Notifications && Notifications.toggle();
-                               break;
-
-                       default:
-                               console.log('unhandled: ' + id);
-                               break;
-
-               }
-       },
-
-
-       onTorrentChanged: function(ev, tor)
-       {
-               // update our dirty fields
-               this.dirtyTorrents[ tor.getId() ] = true;
-
-               // enqueue ui refreshes
-               this.refilterSoon();
-               this.updateButtonsSoon();
-       },
-
-       updateFromTorrentGet: function(updates, removed_ids)
-       {
-               var i, o, t, id, needed, needinfo = [],
-                   callback, fields;
-
-               for (i=0; o=updates[i]; ++i)
-               {
-                       id = o.id;
-                       if ((t = this._torrents[id]))
-                       {
-                               needed = t.needsMetaData();
-                               t.refresh(o);
-                               if (needed && !t.needsMetaData())
-                                       needinfo.push(id);
-                       }
-                       else {
-                               t = this._torrents[id] = new Torrent(o);
-                               this.dirtyTorrents[id] = true;
-                               callback = $.proxy(this.onTorrentChanged,this);
-                               $(t).bind('dataChanged',callback);
-                               // do we need more info for this torrent?
-                               if(!('name' in t.fields) || !('status' in t.fields))
-                                       needinfo.push(id);
-
-                               t.notifyOnFieldChange('status', $.proxy(function (newValue, oldValue) {
-                                       if (oldValue === Torrent._StatusDownload && (newValue == Torrent._StatusSeed || newValue == Torrent._StatusSeedWait)) {
-                                               $(this).trigger('downloadComplete', [t]);
-                                       } else if (oldValue === Torrent._StatusSeed && newValue === Torrent._StatusStopped && t.isFinished()) {
-                                               $(this).trigger('seedingComplete', [t]);
-                                       } else {
-                                               $(this).trigger('statusChange', [t]);
-                                       }
-                               }, this));
-                       }
-               }
-
-               if (needinfo.length) {
-                       // whee, new torrents! get their initial information.
-                       fields = ['id'].concat(Torrent.Fields.Metadata,
-                                              Torrent.Fields.Stats);
-                       this.updateTorrents(needinfo, fields);
-                       this.refilterSoon();
-               }
-
-               if (removed_ids) {
-                       this.deleteTorrents(removed_ids);
-                       this.refilterSoon();
-               }
-       },
-
-       updateTorrents: function(ids, fields)
-       {
-               this.remote.updateTorrents(ids, fields,
-                                          this.updateFromTorrentGet, this);
-       },
-
-       refreshTorrents: function()
-       {
-               var callback = $.proxy(this.refreshTorrents,this),
-                   msec = this[Prefs._RefreshRate] * 1000,
-                   fields = ['id'].concat(Torrent.Fields.Stats);
-
-               // send a request right now
-               this.updateTorrents('recently-active', fields);
-
-               // schedule the next request
-               clearTimeout(this.refreshTorrentsTimeout);
-               this.refreshTorrentsTimeout = setTimeout(callback, msec);
-       },
-
-       initializeTorrents: function()
-       {
-               var fields = ['id'].concat(Torrent.Fields.Metadata,
-                                          Torrent.Fields.Stats);
-               this.updateTorrents(null, fields);
-       },
-
-       onRowClicked: function(ev)
-       {
-               var meta_key = ev.metaKey || ev.ctrlKey,
-                   row = ev.currentTarget.row;
-
-               // handle the per-row "torrent_resume" button
-               if (ev.target.className === 'torrent_resume') {
-                       this.startTorrent(row.getTorrent());
-                       return;
-               }
-
-               // handle the per-row "torrent_pause" button
-               if (ev.target.className === 'torrent_pause') {
-                       this.stopTorrent(row.getTorrent());
-                       return;
-               }
-
-               // Prevents click carrying to parent element
-               // which deselects all on click
-               ev.stopPropagation();
-
-               if (isMobileDevice) {
-                       if (row.isSelected())
-                               this.setInspectorVisible(true);
-                       this.setSelectedRow(row);
-
-               } else if (ev.shiftKey) {
-                       this.selectRange(row);
-                       // Need to deselect any selected text
-                       window.focus();
-
-               // Apple-Click, not selected
-               } else if (!row.isSelected() && meta_key) {
-                       this.selectRow(row);
-
-               // Regular Click, not selected
-               } else if (!row.isSelected()) {
-                       this.setSelectedRow(row);
-
-               // Apple-Click, selected
-               } else if (row.isSelected() && meta_key) {
-                       this.deselectRow(row);
-
-               // Regular Click, selected
-               } else if (row.isSelected()) {
-                       this.setSelectedRow(row);
-               }
-
-               this._last_torrent_clicked = row.getTorrentId();
-       },
-
-       deleteTorrents: function(ids)
-       {
-               var i, id;
-
-               if (ids && ids.length)
-               {
-                       for (i=0; id=ids[i]; ++i) {
-                               this.dirtyTorrents[id] = true;
-                               delete this._torrents[id];
-                       }
-                       this.refilter();
-               }
-       },
-
-       shouldAddedTorrentsStart: function()
-       {
-               return this.prefsDialog.shouldAddedTorrentsStart();
-       },
-
-       /*
-        * Select a torrent file to upload
-        */
-       uploadTorrentFile: function(confirmed)
-       {
-               var i, file,
-                   reader,
-                   fileInput   = $('input#torrent_upload_file'),
-                   folderInput = $('input#add-dialog-folder-input'),
-                   startInput  = $('input#torrent_auto_start'),
-                   urlInput    = $('input#torrent_upload_url');
-
-               if (!confirmed)
-               {
-                       // update the upload dialog's fields
-                       fileInput.attr('value', '');
-                       urlInput.attr('value', '');
-                       startInput.attr('checked', this.shouldAddedTorrentsStart());
-                       folderInput.attr('value', $("#download-dir").val());
-                       folderInput.change($.proxy(this.updateFreeSpaceInAddDialog,this));
-                       this.updateFreeSpaceInAddDialog();
-
-                       // show the dialog
-                       $('#upload_container').show();
-                       urlInput.focus();
-               }
-               else
-               {
-                       var paused = !startInput.is(':checked'),
-                           destination = folderInput.val(),
-                           remote = this.remote;
-
-                       jQuery.each (fileInput[0].files, function(i,file) {
-                               var reader = new FileReader();
-                               reader.onload = function(e) {
-                                       var contents = e.target.result;
-                                       var key = "base64,"
-                                       var index = contents.indexOf (key);
-                                       if (index > -1) {
-                                               var metainfo = contents.substring (index + key.length);
-                                               var o = {
-                                                       'method': 'torrent-add',
-                                                       arguments: {
-                                                               'paused': paused,
-                                                               'download-dir': destination,
-                                                               'metainfo': metainfo
-                                                       }
-                                               };
-                                               remote.sendRequest (o, function(response) {
-                                                       if (response.result != 'success')
-                                                               alert ('Error adding "' + file.name + '": ' + response.result);
-                                               });
-                                       }
-                               }
-                               reader.readAsDataURL (file);
-                       });
-
-                       var url = $('#torrent_upload_url').val();
-                       if (url != '') {
-                               if (url.match(/^[0-9a-f]{40}$/i))
-                                       url = 'magnet:?xt=urn:btih:'+url;
-                               var o = {
-                                       'method': 'torrent-add',
-                                       arguments: {
-                                               'paused': paused,
-                                               'download-dir': destination,
-                                               'filename': url
-                                       }
-                               };
-                               remote.sendRequest (o, function(response) {
-                                       if (response.result != 'success')
-                                               alert ('Error adding "' + url + '": ' + response.result);
-                               });
-                       }
-               }
-       },
-
-       promptSetLocation: function(confirmed, torrents) {
-               if (! confirmed) {
-                       var path;
-                       if (torrents.length === 1) {
-                               path = torrents[0].getDownloadDir();
-                       } else {
-                               path = $("#download-dir").val();
-                       }
-                       $('input#torrent_path').attr('value', path);
-                       $('#move_container').show();
-                       $('#torrent_path').focus();
-               } else {
-                       var ids = this.getTorrentIds(torrents);
-                       this.remote.moveTorrents(
-                               ids,
-                               $("input#torrent_path").val(),
-                               this.refreshTorrents,
-                               this);
-                       $('#move_container').hide();
-               }
-       },
-
-       moveSelectedTorrents: function(confirmed) {
-               var torrents = this.getSelectedTorrents();
-               if (torrents.length)
-                       this.promptSetLocation(confirmed, torrents);
-       },
-
-       removeSelectedTorrents: function() {
-               var torrents = this.getSelectedTorrents();
-               if (torrents.length)
-                       this.promptToRemoveTorrents(torrents);
-       },
-
-       removeSelectedTorrentsAndData: function() {
-               var torrents = this.getSelectedTorrents();
-               if (torrents.length)
-                       this.promptToRemoveTorrentsAndData(torrents);
-       },
-
-       promptToRemoveTorrents: function(torrents) {
-               if (torrents.length === 1)
-               {
-                       var torrent = torrents[0],
-                           header = 'Remove ' + torrent.getName() + '?',
-                           message = 'Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?';
-                       dialog.confirm(header, message, 'Remove', function() {
-                               transmission.removeTorrents(torrents);
-                       });
-               }
-               else
-               {
-                       var header = 'Remove ' + torrents.length + ' transfers?',
-                           message = 'Once removed, continuing the transfers will require the torrent files. Are you sure you want to remove them?';
-                       dialog.confirm(header, message, 'Remove', function() {
-                               transmission.removeTorrents(torrents);
-                       });
-               }
-       },
-
-       promptToRemoveTorrentsAndData:function(torrents)
-       {
-               if (torrents.length === 1)
-               {
-                       var torrent = torrents[0],
-                           header = 'Remove ' + torrent.getName() + ' and delete data?',
-                           message = 'All data downloaded for this torrent will be deleted. Are you sure you want to remove it?';
-                       dialog.confirm(header, message, 'Remove', function() {
-                               transmission.removeTorrentsAndData(torrents);
-                       });
-               }
-               else
-               {
-                       var header = 'Remove ' + torrents.length + ' transfers and delete data?',
-                           message = 'All data downloaded for these torrents will be deleted. Are you sure you want to remove them?';
-                       dialog.confirm(header, message, 'Remove', function() {
-                               transmission.removeTorrentsAndData(torrents);
-                       });
-               }
-       },
-
-       removeTorrents: function(torrents) {
-               var ids = this.getTorrentIds(torrents);
-               this.remote.removeTorrents(ids, this.refreshTorrents, this);
-       },
-
-       removeTorrentsAndData: function(torrents) {
-               this.remote.removeTorrentsAndData(torrents);
-       },
-
-       promptToRenameTorrent: function(torrent) {
-               $('body').addClass('open_showing');
-               $('input#torrent_rename_name').attr('value', torrent.getName());
-               $('#rename_container').show();
-               $('#torrent_rename_name').focus();
-       },
-
-       renameSelectedTorrents: function() {
-               var torrents = this.getSelectedTorrents();
-               if (torrents.length != 1)
-                       dialog.alert("Renaming", "You can rename only one torrent at a time.", "Ok");
-               else
-                       this.promptToRenameTorrent(torrents[0]);
-       },
-
-       onTorrentRenamed: function(response) {
-               var torrent;
-               if ((response.result === 'success') &&
-                   (response.arguments) &&
-                   ((torrent = this._torrents[response.arguments.id])))
-               {
-                       torrent.refresh(response.arguments);
-               }
-       },
-
-       renameTorrent: function (torrent, newname) {
-               var oldpath = torrent.getName();
-               this.remote.renameTorrent([torrent.getId()], oldpath, newname, this.onTorrentRenamed, this);
-       },
-
-       verifySelectedTorrents: function() {
-               this.verifyTorrents(this.getSelectedTorrents());
-       },
-
-       reannounceSelectedTorrents: function() {
-               this.reannounceTorrents(this.getSelectedTorrents());
-       },
-
-       startAllTorrents: function(force) {
-               this.startTorrents(this.getAllTorrents(), force);
-       },
-       startSelectedTorrents: function(force) {
-               this.startTorrents(this.getSelectedTorrents(), force);
-       },
-       startTorrent: function(torrent) {
-               this.startTorrents([ torrent ], false);
-       },
-
-       startTorrents: function(torrents, force) {
-               this.remote.startTorrents(this.getTorrentIds(torrents), force,
-                                         this.refreshTorrents, this);
-       },
-       verifyTorrent: function(torrent) {
-               this.verifyTorrents([ torrent ]);
-       },
-       verifyTorrents: function(torrents) {
-               this.remote.verifyTorrents(this.getTorrentIds(torrents),
-                                          this.refreshTorrents, this);
-       },
-
-       reannounceTorrent: function(torrent) {
-               this.reannounceTorrents([ torrent ]);
-       },
-       reannounceTorrents: function(torrents) {
-               this.remote.reannounceTorrents(this.getTorrentIds(torrents),
-                                              this.refreshTorrents, this);
-       },
-
-       stopAllTorrents: function() {
-               this.stopTorrents(this.getAllTorrents());
-       },
-       stopSelectedTorrents: function() {
-               this.stopTorrents(this.getSelectedTorrents());
-       },
-       stopTorrent: function(torrent) {
-               this.stopTorrents([ torrent ]);
-       },
-       stopTorrents: function(torrents) {
-               this.remote.stopTorrents(this.getTorrentIds(torrents),
-                                        this.refreshTorrents, this);
-       },
-       changeFileCommand: function(torrentId, rowIndices, command) {
-               this.remote.changeFileCommand(torrentId, rowIndices, command);
-       },
-
-       hideMobileAddressbar: function(delaySecs) {
-               if (isMobileDevice && !scroll_timeout) {
-                       var callback = $.proxy(this.doToolbarHide,this),
-                           msec = delaySecs*1000 || 150;
-                       scroll_timeout = setTimeout(callback,msec);
-               }
-       },
-       doToolbarHide: function() {
-               window.scrollTo(0,1);
-               scroll_timeout=null;
-       },
-
-       // Queue
-       moveTop: function() {
-               this.remote.moveTorrentsToTop(this.getSelectedTorrentIds(),
-                                             this.refreshTorrents, this);
-       },
-       moveUp: function() {
-               this.remote.moveTorrentsUp(this.getSelectedTorrentIds(),
-                                          this.refreshTorrents, this);
-       },
-       moveDown: function() {
-               this.remote.moveTorrentsDown(this.getSelectedTorrentIds(),
-                                            this.refreshTorrents, this);
-       },
-       moveBottom: function() {
-               this.remote.moveTorrentsToBottom(this.getSelectedTorrentIds(),
-                                                this.refreshTorrents, this);
-       },
-
-       /***
-       ****
-       ***/
-
-       updateGuiFromSession: function(o)
-       {
-               var limit, limited, e, b, text,
-                   fmt = Transmission.fmt,
-                   menu = $('#footer_super_menu');
-
-               this.serverVersion = o.version;
-
-               this.prefsDialog.set(o);
-
-               if (RPC._TurtleState in o)
-               {
-                       b = o[RPC._TurtleState];
-                       e = $('#turtle-button');
-                       text = [ 'Click to ', (b?'disable':'enable'),
-                                ' Temporary Speed Limits (',
-                                fmt.speed(o[RPC._TurtleUpSpeedLimit]),
-                                ' up,',
-                                fmt.speed(o[RPC._TurtleDownSpeedLimit]),
-                                ' down)' ].join('');
-                       e.toggleClass('selected', b);
-                       e.attr('title', text);
-               }
-
-               if (this.isMenuEnabled && (RPC._DownSpeedLimited in o)
-                                      && (RPC._DownSpeedLimit in o))
-               {
-                       limit = o[RPC._DownSpeedLimit];
-                       limited = o[RPC._DownSpeedLimited];
-
-                       e = menu.find('#limited_download_rate');
-                       e.html('Limit (' + fmt.speed(limit) + ')');
-
-                       if (!limited)
-                               e = menu.find('#unlimited_download_rate');
-                       e.selectMenuItem();
-               }
-
-               if (this.isMenuEnabled && (RPC._UpSpeedLimited in o)
-                                      && (RPC._UpSpeedLimit in o))
-               {
-                       limit = o[RPC._UpSpeedLimit];
-                       limited = o[RPC._UpSpeedLimited];
-
-                       e = menu.find('#limited_upload_rate');
-                       e.html('Limit (' + fmt.speed(limit) + ')');
-
-                       if (!limited)
-                               e = menu.find('#unlimited_upload_rate');
-                       e.selectMenuItem();
-               }
-       },
-
-       updateStatusbar: function()
-       {
-               var u=0, d=0,
-                   i, row,
-                   fmt = Transmission.fmt,
-                   torrents = this.getAllTorrents();
-
-               // up/down speed
-               for (i=0; row=torrents[i]; ++i) {
-                       u += row.getUploadSpeed();
-                       d += row.getDownloadSpeed();
-               }
-
-               $('#speed-up-container').toggleClass('active', u>0 );
-               $('#speed-up-label').text( fmt.speedBps( u ) );
-
-               $('#speed-dn-container').toggleClass('active', d>0 );
-               $('#speed-dn-label').text( fmt.speedBps( d ) );
-
-               // visible torrents
-               $('#filter-count').text( fmt.countString('Transfer','Transfers',this._rows.length ) );
-       },
-
-       setEnabled: function(key, flag)
-       {
-               $(key).toggleClass('disabled', !flag);
-       },
-
-       updateFilterSelect: function()
-       {
-               var i, names, name, str, o,
-                   e = $('#filter-tracker'),
-                   trackers = this.getTrackers();
-
-               // build a sorted list of names
-               names = [];
-               for (name in trackers)
-                       names.push (name);
-               names.sort();
-
-               // build the new html
-               if (!this.filterTracker)
-                       str = '<option value="all" selected="selected">All</option>';
-               else
-                       str = '<option value="all">All</option>';
-               for (i=0; name=names[i]; ++i) {
-                       o = trackers[name];
-                       str += '<option value="' + o.domain + '"';
-                       if (trackers[name].domain === this.filterTracker) str += ' selected="selected"';
-                       str += '>' + name + '</option>';
-               }
-
-               if (!this.filterTrackersStr || (this.filterTrackersStr !== str)) {
-                       this.filterTrackersStr = str;
-                       $('#filter-tracker').html(str);
-               }
-       },
-
-       updateButtonsSoon: function()
-       {
-               if (!this.buttonRefreshTimer)
-               {
-                       var callback = $.proxy(this.updateButtonStates,this),
-                           msec = 100;
-                       this.buttonRefreshTimer = setTimeout(callback, msec);
-               }
-       },
-
-       calculateTorrentStates: function(callback)
-       {
-               var stats = {
-                       total: 0,
-                       active: 0,
-                       paused: 0,
-                       sel: 0,
-                       activeSel: 0,
-                       pausedSel: 0,
-                       queuedSel: 0
-               };
-
-               clearTimeout(this.buttonRefreshTimer);
-               delete this.buttonRefreshTimer;
-
-               for (var i=0, row; row=this._rows[i]; ++i) {
-                       var isStopped = row.getTorrent().isStopped();
-                       var isSelected = row.isSelected();
-                       var isQueued = row.getTorrent().isQueued();
-                       ++stats.total;
-                       if (!isStopped) ++stats.active;
-                       if (isStopped) ++stats.paused;
-                       if (isSelected) ++stats.sel;
-                       if (isSelected && !isStopped) ++stats.activeSel;
-                       if (isSelected && isStopped) ++stats.pausedSel;
-                       if (isSelected && isQueued) ++stats.queuedSel;
-               }
-
-               callback(stats);
-       },
-
-       updateButtonStates: function()
-       {
-               var tr = this,
-               e = this.elements;
-               this.calculateTorrentStates(function(s) {
-                       tr.setEnabled(e.toolbar_pause_button, s.activeSel > 0);
-                       tr.setEnabled(e.toolbar_start_button, s.pausedSel > 0);
-                       tr.setEnabled(e.toolbar_remove_button, s.sel > 0);
-               });
-       },
-
-       /****
-       *****
-       *****  INSPECTOR
-       *****
-       ****/
-
-       inspectorIsVisible: function()
-       {
-               return $('#torrent_inspector').is(':visible');
-       },
-       toggleInspector: function()
-       {
-               this.setInspectorVisible(!this.inspectorIsVisible());
-       },
-       setInspectorVisible: function(visible)
-       {
-               if (visible)
-                       this.inspector.setTorrents(this.getSelectedTorrents());
-
-               // update the ui widgetry
-               $('#torrent_inspector').toggle(visible);
-               $('#toolbar-inspector').toggleClass('selected',visible);
-               this.hideMobileAddressbar();
-               if (isMobileDevice) {
-                       $('body').toggleClass('inspector_showing',visible);
-               } else {
-                       var w = visible ? $('#torrent_inspector').outerWidth() + 1 + 'px' : '0px';
-                       $('#torrent_container')[0].style.right = w;
-               }
-       },
-
-       /****
-       *****
-       *****  FILTER
-       *****
-       ****/
-
-       refilterSoon: function()
-       {
-               if (!this.refilterTimer) {
-                       var tr = this,
-                           callback = function(){tr.refilter(false);},
-                           msec = 100;
-                       this.refilterTimer = setTimeout(callback, msec);
-               }
-       },
-
-       sortRows: function(rows)
-       {
-               var i, tor, row,
-                   id2row = {},
-                   torrents = [];
-
-               for (i=0; row=rows[i]; ++i) {
-                       tor = row.getTorrent();
-                       torrents.push(tor);
-                       id2row[ tor.getId() ] = row;
-               }
-
-               Torrent.sortTorrents(torrents, this[Prefs._SortMethod],
-                                              this[Prefs._SortDirection]);
-
-               for (i=0; tor=torrents[i]; ++i)
-                       rows[i] = id2row[ tor.getId() ];
-       },
-
-       refilter: function(rebuildEverything)
-       {
-               var i, e, id, t, row, tmp, rows, clean_rows, dirty_rows, frag,
-                   sort_mode = this[Prefs._SortMethod],
-                   sort_direction = this[Prefs._SortDirection],
-                   filter_mode = this[Prefs._FilterMode],
-                   filter_text = this.filterText,
-                   filter_tracker = this.filterTracker,
-                   renderer = this.torrentRenderer,
-                   list = this.elements.torrent_list,
-                   old_sel_count = $(list).children('.selected').length;
-
-               this.updateFilterSelect();
-
-               clearTimeout(this.refilterTimer);
-               delete this.refilterTimer;
-
-               if (rebuildEverything) {
-                       $(list).empty();
-                       this._rows = [];
-                       for (id in this._torrents)
-                               this.dirtyTorrents[id] = true;
-               }
-
-               // rows that overlap with dirtyTorrents need to be refiltered.
-               // those that don't are 'clean' and don't need refiltering.
-               clean_rows = [];
-               dirty_rows = [];
-               for (i=0; row=this._rows[i]; ++i) {
-                       if(row.getTorrentId() in this.dirtyTorrents)
-                               dirty_rows.push(row);
-                       else
-                               clean_rows.push(row);
-               }
-
-               // remove the dirty rows from the dom
-               e = [];
-               for (i=0; row=dirty_rows[i]; ++i)
-                       e.push (row.getElement());
-               $(e).detach();
-
-               // drop any dirty rows that don't pass the filter test
-               tmp = [];
-               for (i=0; row=dirty_rows[i]; ++i) {
-                       id = row.getTorrentId();
-                       t = this._torrents[ id ];
-                       if (t && t.test(filter_mode, filter_text, filter_tracker))
-                               tmp.push(row);
-                       delete this.dirtyTorrents[id];
-               }
-               dirty_rows = tmp;
-
-               // make new rows for dirty torrents that pass the filter test
-               // but don't already have a row
-               for (id in this.dirtyTorrents) {
-                       t = this._torrents[id];
-                       if (t && t.test(filter_mode, filter_text, filter_tracker)) {
-                               row = new TorrentRow(renderer, this, t);
-                               e = row.getElement();
-                               e.row = row;
-                               dirty_rows.push(row);
-                               $(e).click($.proxy(this.onRowClicked,this));
-                               $(e).dblclick($.proxy(this.toggleInspector,this));
-                       }
-               }
-
-               // sort the dirty rows
-               this.sortRows (dirty_rows);
-
-               // now we have two sorted arrays of rows
-               // and can do a simple two-way sorted merge.
-               rows = [];
-               var ci=0, cmax=clean_rows.length;
-               var di=0, dmax=dirty_rows.length;
-               frag = document.createDocumentFragment();
-               while (ci!=cmax || di!=dmax)
-               {
-                       var push_clean;
-
-                       if (ci==cmax)
-                               push_clean = false;
-                       else if (di==dmax)
-                               push_clean = true;
-                       else {
-                               var c = Torrent.compareTorrents(
-                                          clean_rows[ci].getTorrent(),
-                                          dirty_rows[di].getTorrent(),
-                                          sort_mode, sort_direction);
-                               push_clean = (c < 0);
-                       }
-
-                       if (push_clean)
-                               rows.push(clean_rows[ci++]);
-                       else {
-                               row = dirty_rows[di++];
-                               e = row.getElement();
-                               if (ci !== cmax)
-                                       list.insertBefore(e, clean_rows[ci].getElement());
-                               else
-                                       frag.appendChild(e);
-                               rows.push(row);
-                       }
-               }
-               list.appendChild(frag);
-
-               // update our implementation fields
-               this._rows = rows;
-               this.dirtyTorrents = {};
-
-               // jquery's even/odd starts with 1 not 0, so invert its logic
-               e = []
-               for (i=0; row=rows[i]; ++i)
-                       e.push(row.getElement());
-               $(e).filter(":odd").addClass('even');
-               $(e).filter(":even").removeClass('even');
-
-               // sync gui
-               this.updateStatusbar();
-               if (old_sel_count !== $(list).children('.selected').length)
-                       this.selectionChanged();
-       },
-
-       setFilterMode: function(mode)
-       {
-               // set the state
-               this.setPref(Prefs._FilterMode, mode);
-
-               // refilter
-               this.refilter(true);
-       },
-
-       onFilterModeClicked: function(ev)
-       {
-               this.setFilterMode($('#filter-mode').val());
-       },
-
-       onFilterTrackerClicked: function(ev)
-       {
-               var tracker = $('#filter-tracker').val();
-               this.setFilterTracker(tracker==='all' ? null : tracker);
-       },
-
-       setFilterTracker: function(domain)
-       {
-               // update which tracker is selected in the popup
-               var key = domain ? this.getReadableDomain(domain) : 'all',
-                   id = '#show-tracker-' + key;
-               $(id).addClass('selected').siblings().removeClass('selected');
-
-               this.filterTracker = domain;
-               this.refilter(true);
-       },
-
-       // example: "tracker.ubuntu.com" returns "ubuntu.com"
-       getDomainName: function(host)
-       {
-               var dot = host.indexOf('.');
-               if (dot !== host.lastIndexOf('.'))
-                       host = host.slice(dot+1);
-               return host;
-       },
-
-       // example: "ubuntu.com" returns "Ubuntu"
-       getReadableDomain: function(name)
-       {
-               if (name.length)
-                       name = name.charAt(0).toUpperCase() + name.slice(1);
-               var dot = name.indexOf('.');
-               if (dot !== -1)
-                       name = name.slice(0, dot);
-               return name;
-       },
-
-       getTrackers: function()
-       {
-               var ret = {};
-
-               var torrents = this.getAllTorrents();
-               for (var i=0, torrent; torrent=torrents[i]; ++i)
-               {
-                       var names = [];
-                       var trackers = torrent.getTrackers();
-                       for (var j=0, tracker; tracker=trackers[j]; ++j)
-                       {
-                               var uri, announce = tracker.announce;
-
-                               if (announce in this.uriCache)
-                                       uri = this.uriCache[announce];
-                               else {
-                                       uri = this.uriCache[announce] = parseUri (announce);
-                                       uri.domain = this.getDomainName (uri.host);
-                                       uri.name = this.getReadableDomain (uri.domain);
-                               }
-
-                               if (!(uri.name in ret))
-                                       ret[uri.name] = { 'uri': uri,
-                                                         'domain': uri.domain,
-                                                         'count': 0 };
-
-                               if (names.indexOf(uri.name) === -1)
-                                       names.push(uri.name);
-                       }
-                       for (var j=0, name; name=names[j]; ++j)
-                               ret[name].count++;
-               }
-
-               return ret;
-       },
-
-       /***
-       ****
-       ****  Compact Mode
-       ****
-       ***/
-
-       toggleCompactClicked: function()
-       {
-               this.setCompactMode(!this[Prefs._CompactDisplayState]);
-       },
-       setCompactMode: function(is_compact)
-       {
-               var key = Prefs._CompactDisplayState,
-                   was_compact = this[key];
-
-               if (was_compact !== is_compact) {
-                       this.setPref(key, is_compact);
-                       this.onCompactModeChanged();
-               }
-       },
-       initCompactMode: function()
-       {
-               this.onCompactModeChanged();
-       },
-       onCompactModeChanged: function()
-       {
-               var compact = this[Prefs._CompactDisplayState];
-
-               // update the ui: footer button
-               $("#compact-button").toggleClass('selected',compact);
-
-               // update the ui: torrent list
-               this.torrentRenderer = compact ? new TorrentRendererCompact()
-                                              : new TorrentRendererFull();
-               this.refilter(true);
-       },
-
-       /***
-       ****
-       ****  Statistics
-       ****
-       ***/
-
-       // turn the periodic ajax stats refresh on & off
-       togglePeriodicStatsRefresh: function(enabled) {
-               clearInterval(this.statsInterval);
-               delete this.statsInterval;
-               if (enabled) {
-                       var callback = $.proxy(this.loadDaemonStats,this),
-                           msec = 5000;
-                       this.statsInterval = setInterval(callback, msec);
-               }
-       },
-
-       loadDaemonStats: function(async) {
-               this.remote.loadDaemonStats(function(data) {
-                       this.updateStats(data['arguments']);
-               }, this, async);
-       },
-
-       // Process new session stats from the server
-       updateStats: function(stats)
-       {
-               var s, ratio,
-                   fmt = Transmission.fmt;
-
-               s = stats["current-stats"];
-               ratio = Math.ratio(s.uploadedBytes,s.downloadedBytes);
-               $('#stats-session-uploaded').html(fmt.size(s.uploadedBytes));
-               $('#stats-session-downloaded').html(fmt.size(s.downloadedBytes));
-               $('#stats-session-ratio').html(fmt.ratioString(ratio));
-               $('#stats-session-duration').html(fmt.timeInterval(s.secondsActive));
-
-               s = stats["cumulative-stats"];
-               ratio = Math.ratio(s.uploadedBytes,s.downloadedBytes);
-               $('#stats-total-count').html(s.sessionCount + " times");
-               $('#stats-total-uploaded').html(fmt.size(s.uploadedBytes));
-               $('#stats-total-downloaded').html(fmt.size(s.downloadedBytes));
-               $('#stats-total-ratio').html(fmt.ratioString(ratio));
-               $('#stats-total-duration').html(fmt.timeInterval(s.secondsActive));
-       },
-
-
-       showStatsDialog: function() {
-               this.loadDaemonStats();
-               this.hideMobileAddressbar();
-               this.togglePeriodicStatsRefresh(true);
-               $('#stats-dialog').dialog({
-                       close: $.proxy(this.onStatsDialogClosed,this),
-                       show: 'fade',
-                       hide: 'fade',
-                       title: 'Statistics'
-               });
-       },
-
-       onStatsDialogClosed: function() {
-               this.hideMobileAddressbar();
-               this.togglePeriodicStatsRefresh(false);
-       }
+Transmission.prototype = {
+    /****
+     *****
+     *****  STARTUP
+     *****
+     ****/
+
+    initialize: function () {
+        var e;
+
+        // Initialize the helper classes
+        this.remote = new TransmissionRemote(this);
+        this.inspector = new Inspector(this, this.remote);
+        this.prefsDialog = new PrefsDialog(this.remote);
+        $(this.prefsDialog).bind('closed', $.proxy(this.onPrefsDialogClosed, this));
+
+        this.isMenuEnabled = !isMobileDevice;
+
+        // Initialize the implementation fields
+        this.filterText = '';
+        this._torrents = {};
+        this._rows = [];
+        this.dirtyTorrents = {};
+        this.uriCache = {};
+
+        // Initialize the clutch preferences
+        Prefs.getClutchPrefs(this);
+
+        // Set up user events
+        $('#toolbar-pause').click($.proxy(this.stopSelectedClicked, this));
+        $('#toolbar-start').click($.proxy(this.startSelectedClicked, this));
+        $('#toolbar-pause-all').click($.proxy(this.stopAllClicked, this));
+        $('#toolbar-start-all').click($.proxy(this.startAllClicked, this));
+        $('#toolbar-remove').click($.proxy(this.removeClicked, this));
+        $('#toolbar-open').click($.proxy(this.openTorrentClicked, this));
+
+        $('#prefs-button').click($.proxy(this.togglePrefsDialogClicked, this));
+
+        $('#upload_confirm_button').click($.proxy(this.confirmUploadClicked, this));
+        $('#upload_cancel_button').click($.proxy(this.hideUploadDialog, this));
+
+        $('#rename_confirm_button').click($.proxy(this.confirmRenameClicked, this));
+        $('#rename_cancel_button').click($.proxy(this.hideRenameDialog, this));
+
+        $('#move_confirm_button').click($.proxy(this.confirmMoveClicked, this));
+        $('#move_cancel_button').click($.proxy(this.hideMoveDialog, this));
+
+        $('#turtle-button').click($.proxy(this.toggleTurtleClicked, this));
+        $('#compact-button').click($.proxy(this.toggleCompactClicked, this));
+
+        // tell jQuery to copy the dataTransfer property from events over if it exists
+        jQuery.event.props.push("dataTransfer");
+
+        $('#torrent_upload_form').submit(function () {
+            $('#upload_confirm_button').click();
+            return false;
+        });
+
+        $('#toolbar-inspector').click($.proxy(this.toggleInspector, this));
+
+        e = $('#filter-mode');
+        e.val(this[Prefs._FilterMode]);
+        e.change($.proxy(this.onFilterModeClicked, this));
+        $('#filter-tracker').change($.proxy(this.onFilterTrackerClicked, this));
+
+        if (!isMobileDevice) {
+            $(document).bind('keydown', $.proxy(this.keyDown, this));
+            $(document).bind('keyup', $.proxy(this.keyUp, this));
+            $('#torrent_container').click($.proxy(this.deselectAll, this));
+            $('#torrent_container').bind('dragover', $.proxy(this.dragenter, this));
+            $('#torrent_container').bind('dragenter', $.proxy(this.dragenter, this));
+            $('#torrent_container').bind('drop', $.proxy(this.drop, this));
+            $('#inspector_link').click($.proxy(this.toggleInspector, this));
+
+            this.setupSearchBox();
+            this.createContextMenu();
+        };
+
+        if (this.isMenuEnabled) {
+            this.createSettingsMenu();
+        };
+
+        e = {};
+        e.torrent_list = $('#torrent_list')[0];
+        e.toolbar_buttons = $('#toolbar ul li');
+        e.toolbar_pause_button = $('#toolbar-pause')[0];
+        e.toolbar_start_button = $('#toolbar-start')[0];
+        e.toolbar_remove_button = $('#toolbar-remove')[0];
+        this.elements = e;
+
+        // Apply the prefs settings to the gui
+        this.initializeSettings();
+
+        // Get preferences & torrents from the daemon
+        var async = false;
+        this.loadDaemonPrefs(async);
+        this.loadDaemonStats(async);
+        this.initializeTorrents();
+        this.refreshTorrents();
+        this.togglePeriodicSessionRefresh(true);
+
+        this.updateButtonsSoon();
+    },
+
+    loadDaemonPrefs: function (async) {
+        this.remote.loadDaemonPrefs(function (data) {
+            var o = data['arguments'];
+            Prefs.getClutchPrefs(o);
+            this.updateGuiFromSession(o);
+            this.sessionProperties = o;
+        }, this, async);
+    },
+
+    loadImages: function () {
+        for (var i = 0, row; row = arguments[i]; ++i) {
+            jQuery("<img>").attr("src", row);
+        };
+    },
+
+    /*
+     * Load the clutch prefs and init the GUI according to those prefs
+     */
+    initializeSettings: function () {
+        Prefs.getClutchPrefs(this);
+
+        if (this.isMenuEnabled) {
+            $('#sort_by_' + this[Prefs._SortMethod]).selectMenuItem();
+
+            if (this[Prefs._SortDirection] === Prefs._SortDescending) {
+                $('#reverse_sort_order').selectMenuItem();
+            };
+        }
+
+        this.initCompactMode();
+    },
+
+    /*
+     * Set up the search box
+     */
+    setupSearchBox: function () {
+        var tr = this;
+        var search_box = $('#torrent_search');
+        search_box.bind('keyup click', function () {
+            tr.setFilterText(this.value);
+        });
+        if (!$.browser.safari) {
+            search_box.addClass('blur');
+            search_box[0].value = 'Filter';
+            search_box.bind('blur', function () {
+                if (this.value === '') {
+                    $(this).addClass('blur');
+                    this.value = 'Filter';
+                    tr.setFilterText(null);
+                };
+            }).bind('focus', function () {
+                if ($(this).is('.blur')) {
+                    this.value = '';
+                    $(this).removeClass('blur');
+                }
+            });
+        }
+    },
+
+    /**
+     * Create the torrent right-click menu
+     */
+    createContextMenu: function () {
+        var tr = this;
+        var bindings = {
+            pause_selected: function () {
+                tr.stopSelectedTorrents();
+            },
+            resume_selected: function () {
+                tr.startSelectedTorrents(false);
+            },
+            resume_now_selected: function () {
+                tr.startSelectedTorrents(true);
+            },
+            move: function () {
+                tr.moveSelectedTorrents(false);
+            },
+            remove: function () {
+                tr.removeSelectedTorrents();
+            },
+            remove_data: function () {
+                tr.removeSelectedTorrentsAndData();
+            },
+            verify: function () {
+                tr.verifySelectedTorrents();
+            },
+            rename: function () {
+                tr.renameSelectedTorrents();
+            },
+            reannounce: function () {
+                tr.reannounceSelectedTorrents();
+            },
+            move_top: function () {
+                tr.moveTop();
+            },
+            move_up: function () {
+                tr.moveUp();
+            },
+            move_down: function () {
+                tr.moveDown();
+            },
+            move_bottom: function () {
+                tr.moveBottom();
+            },
+            select_all: function () {
+                tr.selectAll();
+            },
+            deselect_all: function () {
+                tr.deselectAll();
+            }
+        };
+
+        // Set up the context menu
+        $("ul#torrent_list").contextmenu({
+            delegate: ".torrent",
+            menu: "#torrent_context_menu",
+            preventSelect: true,
+            taphold: true,
+            show: {
+                effect: "none"
+            },
+            hide: {
+                effect: "none"
+            },
+            select: function (event, ui) {
+                bindings[ui.cmd]();
+            },
+            beforeOpen: $.proxy(function (event, ui) {
+                var element = $(event.currentTarget);
+                var i = $('#torrent_list > li').index(element);
+                if ((i !== -1) && !this._rows[i].isSelected()) {
+                    this.setSelectedRow(this._rows[i]);
+                };
+
+                this.calculateTorrentStates(function (s) {
+                    var tl = $(event.target);
+                    tl.contextmenu("enableEntry", "pause_selected", s.activeSel > 0);
+                    tl.contextmenu("enableEntry", "resume_selected", s.pausedSel > 0);
+                    tl.contextmenu("enableEntry", "resume_now_selected", s.pausedSel > 0 || s.queuedSel > 0);
+                    tl.contextmenu("enableEntry", "rename", s.sel == 1);
+                });
+            }, this)
+        });
+    },
+
+    createSettingsMenu: function () {
+        $("#footer_super_menu").transMenu({
+            open: function () {
+                $("#settings_menu").addClass("selected");
+            },
+            close: function () {
+                $("#settings_menu").removeClass("selected");
+            },
+            select: $.proxy(this.onMenuClicked, this)
+        });
+        $("#settings_menu").click(function (event) {
+            $("#footer_super_menu").transMenu("open");
+        });
+    },
+
+    /****
+     *****
+     ****/
+
+    updateFreeSpaceInAddDialog: function () {
+        var formdir = $('input#add-dialog-folder-input').val();
+        this.remote.getFreeSpace(formdir, this.onFreeSpaceResponse, this);
+    },
+
+    onFreeSpaceResponse: function (dir, bytes) {
+        var e, str, formdir;
+
+        formdir = $('input#add-dialog-folder-input').val();
+        if (formdir == dir) {
+            e = $('label#add-dialog-folder-label');
+            if (bytes > 0) {
+                str = '  <i>(' + Transmission.fmt.size(bytes) + ' Free)</i>';
+            } else {
+                str = '';
+            };
+            e.html('Destination folder' + str + ':');
+        }
+    },
+
+    /****
+     *****
+     *****  UTILITIES
+     *****
+     ****/
+
+    getAllTorrents: function () {
+        var torrents = [];
+        for (var key in this._torrents) {
+            torrents.push(this._torrents[key]);
+        };
+        return torrents;
+    },
+
+    getTorrentIds: function (torrents) {
+        return $.map(torrents.slice(0), function (t) {
+            return t.getId();
+        });
+    },
+
+    scrollToRow: function (row) {
+        if (isMobileDevice) {
+            // FIXME: why? return
+            var list = $('#torrent_container');
+            var scrollTop = list.scrollTop();
+            var innerHeight = list.innerHeight();
+            var offsetTop = row.getElement().offsetTop;
+            var offsetHeight = $(row.getElement()).outerHeight();
+
+            if (offsetTop < scrollTop) {
+                list.scrollTop(offsetTop);
+            } else if (innerHeight + scrollTop < offsetTop + offsetHeight) {
+                list.scrollTop(offsetTop + offsetHeight - innerHeight);
+            };
+        };
+    },
+
+    seedRatioLimit: function () {
+        var p = this.sessionProperties;
+        if (p && p.seedRatioLimited) {
+            return p.seedRatioLimit;
+        };
+        return -1;
+    },
+
+    setPref: function (key, val) {
+        this[key] = val;
+        Prefs.setValue(key, val);
+    },
+
+    /****
+     *****
+     *****  SELECTION
+     *****
+     ****/
+
+    getSelectedRows: function () {
+        return $.grep(this._rows, function (r) {
+            return r.isSelected();
+        });
+    },
+
+    getSelectedTorrents: function () {
+        return $.map(this.getSelectedRows(), function (r) {
+            return r.getTorrent();
+        });
+    },
+
+    getSelectedTorrentIds: function () {
+        return this.getTorrentIds(this.getSelectedTorrents());
+    },
+
+    setSelectedRow: function (row) {
+        $(this.elements.torrent_list).children('.selected').removeClass('selected');
+        this.selectRow(row);
+    },
+
+    selectRow: function (row) {
+        $(row.getElement()).addClass('selected');
+        this.callSelectionChangedSoon();
+    },
+
+    deselectRow: function (row) {
+        $(row.getElement()).removeClass('selected');
+        this.callSelectionChangedSoon();
+    },
+
+    selectAll: function () {
+        $(this.elements.torrent_list).children().addClass('selected');
+        this.callSelectionChangedSoon();
+    },
+    deselectAll: function () {
+        $(this.elements.torrent_list).children('.selected').removeClass('selected');
+        this.callSelectionChangedSoon();
+        delete this._last_torrent_clicked;
+    },
+
+    indexOfLastTorrent: function () {
+        for (var i = 0, r; r = this._rows[i]; ++i) {
+            if (r.getTorrentId() === this._last_torrent_clicked) {
+                return i;
+            };
+        };
+        return -1;
+    },
+
+    // Select a range from this row to the last clicked torrent
+    selectRange: function (row) {
+        var last = this.indexOfLastTorrent();
+
+        if (last === -1) {
+            this.selectRow(row);
+        } else { // select the range between the prevous & current
+            var next = this._rows.indexOf(row);
+            var min = Math.min(last, next);
+            var max = Math.max(last, next);
+            for (var i = min; i <= max; ++i) {
+                this.selectRow(this._rows[i]);
+            };
+        }
+
+        this.callSelectionChangedSoon();
+    },
+
+    selectionChanged: function () {
+        this.updateButtonStates();
+
+        this.inspector.setTorrents(this.inspectorIsVisible() ? this.getSelectedTorrents() : []);
+
+        clearTimeout(this.selectionChangedTimer);
+        delete this.selectionChangedTimer;
+
+    },
+
+    callSelectionChangedSoon: function () {
+        if (!this.selectionChangedTimer) {
+            var callback = $.proxy(this.selectionChanged, this),
+                msec = 200;
+            this.selectionChangedTimer = setTimeout(callback, msec);
+        }
+    },
+
+    /*--------------------------------------------
+     *
+     *  E V E N T   F U N C T I O N S
+     *
+     *--------------------------------------------*/
+
+    /*
+     * Process key event
+     */
+    keyDown: function (ev) {
+        var handled = false;
+        var rows = this._rows;
+        var up = ev.keyCode === 38; // up key pressed
+        var dn = ev.keyCode === 40; // down key pressed
+        var shift = ev.keyCode === 16; // shift key pressed
+
+        if ((up || dn) && rows.length) {
+            var last = this.indexOfLastTorrent(),
+                i = last,
+                anchor = this._shift_index,
+                r,
+                min = 0,
+                max = rows.length - 1;
+
+            if (dn && (i + 1 <= max)) {
+                ++i;
+            } else if (up && (i - 1 >= min)) {
+                --i;
+            };
+
+            var r = rows[i];
+
+            if (anchor >= 0) {
+                // user is extending the selection
+                // with the shift + arrow keys...
+                if (((anchor <= last) && (last < i)) || ((anchor >= last) && (last > i))) {
+                    this.selectRow(r);
+                } else if (((anchor >= last) && (i > last)) || ((anchor <= last) && (last > i))) {
+                    this.deselectRow(rows[last]);
+                }
+            } else {
+                if (ev.shiftKey) {
+                    this.selectRange(r);
+                } else {
+                    this.setSelectedRow(r);
+                };
+            }
+            this._last_torrent_clicked = r.getTorrentId();
+            this.scrollToRow(r);
+            handled = true;
+        } else if (shift) {
+            this._shift_index = this.indexOfLastTorrent();
+        }
+
+        return !handled;
+    },
+
+    keyUp: function (ev) {
+        if (ev.keyCode === 16) { // shift key pressed
+            delete this._shift_index;
+        };
+    },
+
+    isButtonEnabled: function (ev) {
+        var p = (ev.target || ev.srcElement).parentNode;
+        return p.className !== 'disabled' && p.parentNode.className !== 'disabled';
+    },
+
+    stopSelectedClicked: function (ev) {
+        if (this.isButtonEnabled(ev)) {
+            this.stopSelectedTorrents();
+            this.hideMobileAddressbar();
+        }
+    },
+
+    startSelectedClicked: function (ev) {
+        if (this.isButtonEnabled(ev)) {
+            this.startSelectedTorrents(false);
+            this.hideMobileAddressbar();
+        }
+    },
+
+    stopAllClicked: function (ev) {
+        if (this.isButtonEnabled(ev)) {
+            this.stopAllTorrents();
+            this.hideMobileAddressbar();
+        }
+    },
+
+    startAllClicked: function (ev) {
+        if (this.isButtonEnabled(ev)) {
+            this.startAllTorrents(false);
+            this.hideMobileAddressbar();
+        }
+    },
+
+    openTorrentClicked: function (ev) {
+        if (this.isButtonEnabled(ev)) {
+            $('body').addClass('open_showing');
+            this.uploadTorrentFile();
+            this.updateButtonStates();
+        }
+    },
+
+    dragenter: function (ev) {
+        if (ev.dataTransfer && ev.dataTransfer.types) {
+            var types = ["text/uri-list", "text/plain"];
+            for (var i = 0; i < types.length; ++i) {
+                // it would be better to look at the links here;
+                // sadly, with Firefox, trying would throw.
+                if (ev.dataTransfer.types.contains(types[i])) {
+                    ev.stopPropagation();
+                    ev.preventDefault();
+                    ev.dropEffect = "copy";
+                    return false;
+                }
+            }
+        } else if (ev.dataTransfer) {
+            ev.dataTransfer.dropEffect = "none";
+        }
+        return true;
+    },
+
+    drop: function (ev) {
+        var i, uri;
+        var uris = null;
+        var types = ["text/uri-list", "text/plain"];
+        var paused = this.shouldAddedTorrentsStart();
+
+        if (!ev.dataTransfer || !ev.dataTransfer.types) {
+            return true;
+        };
+
+        for (i = 0; !uris && i < types.length; ++i) {
+            if (ev.dataTransfer.types.contains(types[i])) {
+                uris = ev.dataTransfer.getData(types[i]).split("\n");
+            };
+        };
+
+        for (i = 0; uri = uris[i]; ++i) {
+            if (/^#/.test(uri)) { // lines which start with "#" are comments
+                continue;
+            };
+            if (/^[a-z-]+:/i.test(uri)) { // close enough to a url
+                this.remote.addTorrentByUrl(uri, paused);
+            };
+        };
+
+        ev.preventDefault();
+        return false;
+    },
+
+    hideUploadDialog: function () {
+        $('body.open_showing').removeClass('open_showing');
+        $('#upload_container').hide();
+        this.updateButtonStates();
+    },
+
+    confirmUploadClicked: function () {
+        this.uploadTorrentFile(true);
+        this.hideUploadDialog();
+    },
+
+    hideMoveDialog: function () {
+        $('#move_container').hide();
+        this.updateButtonStates();
+    },
+
+    confirmMoveClicked: function () {
+        this.moveSelectedTorrents(true);
+        this.hideUploadDialog();
+    },
+
+    hideRenameDialog: function () {
+        $('body.open_showing').removeClass('open_showing');
+        $('#rename_container').hide();
+    },
+
+    confirmRenameClicked: function () {
+        var torrents = this.getSelectedTorrents();
+        this.renameTorrent(torrents[0], $('input#torrent_rename_name').attr('value'));
+        this.hideRenameDialog();
+    },
+
+    removeClicked: function (ev) {
+        if (this.isButtonEnabled(ev)) {
+            this.removeSelectedTorrents();
+            this.hideMobileAddressbar();
+        };
+    },
+
+    // turn the periodic ajax session refresh on & off
+    togglePeriodicSessionRefresh: function (enabled) {
+        clearInterval(this.sessionInterval);
+        delete this.sessionInterval;
+        if (enabled) {
+            var callback = $.proxy(this.loadDaemonPrefs, this);
+            var msec = 8000;
+
+            this.sessionInterval = setInterval(callback, msec);
+        };
+    },
+
+    toggleTurtleClicked: function () {
+        var o = {};
+        o[RPC._TurtleState] = !$('#turtle-button').hasClass('selected');
+        this.remote.savePrefs(o);
+    },
+
+    /*--------------------------------------------
+     *
+     *  I N T E R F A C E   F U N C T I O N S
+     *
+     *--------------------------------------------*/
+
+    onPrefsDialogClosed: function () {
+        $('#prefs-button').removeClass('selected');
+    },
+
+    togglePrefsDialogClicked: function (ev) {
+        var e = $('#prefs-button');
+
+        if (e.hasClass('selected'))
+            this.prefsDialog.close();
+        else {
+            e.addClass('selected');
+            this.prefsDialog.show();
+        }
+    },
+
+    setFilterText: function (search) {
+        this.filterText = search ? search.trim() : null;
+        this.refilter(true);
+    },
+
+    setSortMethod: function (sort_method) {
+        this.setPref(Prefs._SortMethod, sort_method);
+        this.refilter(true);
+    },
+
+    setSortDirection: function (direction) {
+        this.setPref(Prefs._SortDirection, direction);
+        this.refilter(true);
+    },
+
+    onMenuClicked: function (event, ui) {
+        var o, dir;
+        var id = ui.id;
+        var remote = this.remote;
+        var element = ui.target;
+
+        if (ui.group == 'sort-mode') {
+            element.selectMenuItem();
+            this.setSortMethod(id.replace(/sort_by_/, ''));
+        } else if (element.hasClass('upload-speed')) {
+            o = {};
+            o[RPC._UpSpeedLimit] = parseInt(element.text());
+            o[RPC._UpSpeedLimited] = true;
+            remote.savePrefs(o);
+        } else if (element.hasClass('download-speed')) {
+            o = {};
+            o[RPC._DownSpeedLimit] = parseInt(element.text());
+            o[RPC._DownSpeedLimited] = true;
+            remote.savePrefs(o);
+        } else {
+            switch (id) {
+            case 'statistics':
+                this.showStatsDialog();
+                break;
+
+            case 'about-button':
+                o = 'Transmission ' + this.serverVersion;
+                $('#about-dialog #about-title').html(o);
+                $('#about-dialog').dialog({
+                    title: 'About',
+                    show: 'fade',
+                    hide: 'fade'
+                });
+                break;
+
+            case 'homepage':
+                window.open('http://www.transmissionbt.com/');
+                break;
+
+            case 'tipjar':
+                window.open('http://www.transmissionbt.com/donate.php');
+                break;
+
+            case 'unlimited_download_rate':
+                o = {};
+                o[RPC._DownSpeedLimited] = false;
+                remote.savePrefs(o);
+                break;
+
+            case 'limited_download_rate':
+                o = {};
+                o[RPC._DownSpeedLimited] = true;
+                remote.savePrefs(o);
+                break;
+
+            case 'unlimited_upload_rate':
+                o = {};
+                o[RPC._UpSpeedLimited] = false;
+                remote.savePrefs(o);
+                break;
+
+            case 'limited_upload_rate':
+                o = {};
+                o[RPC._UpSpeedLimited] = true;
+                remote.savePrefs(o);
+                break;
+
+            case 'reverse_sort_order':
+                if (element.menuItemIsSelected()) {
+                    dir = Prefs._SortAscending;
+                    element.deselectMenuItem();
+                } else {
+                    dir = Prefs._SortDescending;
+                    element.selectMenuItem();
+                }
+                this.setSortDirection(dir);
+                break;
+
+            case 'toggle_notifications':
+                Notifications && Notifications.toggle();
+                break;
+
+            default:
+                console.log('unhandled: ' + id);
+                break;
+            };
+        };
+    },
+
+    onTorrentChanged: function (ev, tor) {
+        // update our dirty fields
+        this.dirtyTorrents[tor.getId()] = true;
+
+        // enqueue ui refreshes
+        this.refilterSoon();
+        this.updateButtonsSoon();
+    },
+
+    updateFromTorrentGet: function (updates, removed_ids) {
+        var i, o, t, id, needed, callback, fields;
+        var needinfo = [];
+
+        for (i = 0; o = updates[i]; ++i) {
+            id = o.id;
+            if ((t = this._torrents[id])) {
+                needed = t.needsMetaData();
+                t.refresh(o);
+                if (needed && !t.needsMetaData()) {
+                    needinfo.push(id);
+                };
+            } else {
+                t = this._torrents[id] = new Torrent(o);
+                this.dirtyTorrents[id] = true;
+                callback = $.proxy(this.onTorrentChanged, this);
+                $(t).bind('dataChanged', callback);
+                // do we need more info for this torrent?
+                if (!('name' in t.fields) || !('status' in t.fields))
+                    needinfo.push(id);
+
+                t.notifyOnFieldChange('status', $.proxy(function (newValue, oldValue) {
+                    if (oldValue === Torrent._StatusDownload && (newValue == Torrent._StatusSeed || newValue == Torrent._StatusSeedWait)) {
+                        $(this).trigger('downloadComplete', [t]);
+                    } else if (oldValue === Torrent._StatusSeed && newValue === Torrent._StatusStopped && t.isFinished()) {
+                        $(this).trigger('seedingComplete', [t]);
+                    } else {
+                        $(this).trigger('statusChange', [t]);
+                    }
+                }, this));
+            }
+        }
+
+        if (needinfo.length) {
+            // whee, new torrents! get their initial information.
+            fields = ['id'].concat(Torrent.Fields.Metadata,
+                Torrent.Fields.Stats);
+            this.updateTorrents(needinfo, fields);
+            this.refilterSoon();
+        }
+
+        if (removed_ids) {
+            this.deleteTorrents(removed_ids);
+            this.refilterSoon();
+        }
+    },
+
+    updateTorrents: function (ids, fields) {
+        this.remote.updateTorrents(ids, fields, this.updateFromTorrentGet, this);
+    },
+
+    refreshTorrents: function () {
+        var callback = $.proxy(this.refreshTorrents, this);
+        var msec = this[Prefs._RefreshRate] * 1000;
+        var fields = ['id'].concat(Torrent.Fields.Stats);
+
+        // send a request right now
+        this.updateTorrents('recently-active', fields);
+
+        // schedule the next request
+        clearTimeout(this.refreshTorrentsTimeout);
+        this.refreshTorrentsTimeout = setTimeout(callback, msec);
+    },
+
+    initializeTorrents: function () {
+        var fields = ['id'].concat(Torrent.Fields.Metadata, Torrent.Fields.Stats);
+        this.updateTorrents(null, fields);
+    },
+
+    onRowClicked: function (ev) {
+        var meta_key = ev.metaKey || ev.ctrlKey,
+            row = ev.currentTarget.row;
+
+        // handle the per-row "torrent_resume" button
+        if (ev.target.className === 'torrent_resume') {
+            this.startTorrent(row.getTorrent());
+            return;
+        }
+
+        // handle the per-row "torrent_pause" button
+        if (ev.target.className === 'torrent_pause') {
+            this.stopTorrent(row.getTorrent());
+            return;
+        }
+
+        // Prevents click carrying to parent element
+        // which deselects all on click
+        ev.stopPropagation();
+
+        if (isMobileDevice) {
+            if (row.isSelected())
+                this.setInspectorVisible(true);
+            this.setSelectedRow(row);
+
+        } else if (ev.shiftKey) {
+            this.selectRange(row);
+            // Need to deselect any selected text
+            window.focus();
+
+            // Apple-Click, not selected
+        } else if (!row.isSelected() && meta_key) {
+            this.selectRow(row);
+
+            // Regular Click, not selected
+        } else if (!row.isSelected()) {
+            this.setSelectedRow(row);
+
+            // Apple-Click, selected
+        } else if (row.isSelected() && meta_key) {
+            this.deselectRow(row);
+
+            // Regular Click, selected
+        } else if (row.isSelected()) {
+            this.setSelectedRow(row);
+        }
+
+        this._last_torrent_clicked = row.getTorrentId();
+    },
+
+    deleteTorrents: function (ids) {
+        var i, id;
+
+        if (ids && ids.length) {
+            for (i = 0; id = ids[i]; ++i) {
+                this.dirtyTorrents[id] = true;
+                delete this._torrents[id];
+            };
+            this.refilter();
+        };
+    },
+
+    shouldAddedTorrentsStart: function () {
+        return this.prefsDialog.shouldAddedTorrentsStart();
+    },
+
+    /*
+     * Select a torrent file to upload
+     */
+    uploadTorrentFile: function (confirmed) {
+        var i, file, reader;
+        var fileInput = $('input#torrent_upload_file');
+        var folderInput = $('input#add-dialog-folder-input');
+        var startInput = $('input#torrent_auto_start');
+        var urlInput = $('input#torrent_upload_url');
+
+        if (!confirmed) {
+            // update the upload dialog's fields
+            fileInput.attr('value', '');
+            urlInput.attr('value', '');
+            startInput.attr('checked', this.shouldAddedTorrentsStart());
+            folderInput.attr('value', $("#download-dir").val());
+            folderInput.change($.proxy(this.updateFreeSpaceInAddDialog, this));
+            this.updateFreeSpaceInAddDialog();
+
+            // show the dialog
+            $('#upload_container').show();
+            urlInput.focus();
+        } else {
+            var paused = !startInput.is(':checked');
+            var destination = folderInput.val();
+            var remote = this.remote;
+
+            jQuery.each(fileInput[0].files, function (i, file) {
+                var reader = new FileReader();
+                reader.onload = function (e) {
+                    var contents = e.target.result;
+                    var key = "base64,"
+                    var index = contents.indexOf(key);
+                    if (index > -1) {
+                        var metainfo = contents.substring(index + key.length);
+                        var o = {
+                            method: 'torrent-add',
+                            arguments: {
+                                'paused': paused,
+                                'download-dir': destination,
+                                'metainfo': metainfo
+                            }
+                        };
+                        remote.sendRequest(o, function (response) {
+                            if (response.result != 'success')
+                                alert('Error adding "' + file.name + '": ' + response.result);
+                        });
+                    }
+                };
+                reader.readAsDataURL(file);
+            });
+
+            var url = $('#torrent_upload_url').val();
+            if (url != '') {
+                if (url.match(/^[0-9a-f]{40}$/i)) {
+                    url = 'magnet:?xt=urn:btih:' + url;
+                };
+                var o = {
+                    'method': 'torrent-add',
+                    arguments: {
+                        'paused': paused,
+                        'download-dir': destination,
+                        'filename': url
+                    }
+                };
+                remote.sendRequest(o, function (response) {
+                    if (response.result != 'success') {
+                        alert('Error adding "' + url + '": ' + response.result);
+                    };
+                });
+            }
+        }
+    },
+
+    promptSetLocation: function (confirmed, torrents) {
+        if (!confirmed) {
+            var path;
+            if (torrents.length === 1) {
+                path = torrents[0].getDownloadDir();
+            } else {
+                path = $("#download-dir").val();
+            }
+            $('input#torrent_path').attr('value', path);
+            $('#move_container').show();
+            $('#torrent_path').focus();
+        } else {
+            var ids = this.getTorrentIds(torrents);
+            this.remote.moveTorrents(ids, $("input#torrent_path").val(), this.refreshTorrents, this);
+            $('#move_container').hide();
+        }
+    },
+
+    moveSelectedTorrents: function (confirmed) {
+        var torrents = this.getSelectedTorrents();
+        if (torrents.length) {
+            this.promptSetLocation(confirmed, torrents);
+        };
+    },
+
+    removeSelectedTorrents: function () {
+        var torrents = this.getSelectedTorrents();
+        if (torrents.length) {
+            this.promptToRemoveTorrents(torrents);
+        };
+    },
+
+    removeSelectedTorrentsAndData: function () {
+        var torrents = this.getSelectedTorrents();
+        if (torrents.length) {
+            this.promptToRemoveTorrentsAndData(torrents);
+        };
+    },
+
+    promptToRemoveTorrents: function (torrents) {
+        if (torrents.length === 1) {
+            var torrent = torrents[0];
+            var header = 'Remove ' + torrent.getName() + '?';
+            var message = 'Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?';
+
+            dialog.confirm(header, message, 'Remove', function () {
+                transmission.removeTorrents(torrents);
+            });
+        } else {
+            var header = 'Remove ' + torrents.length + ' transfers?';
+            var message = 'Once removed, continuing the transfers will require the torrent files. Are you sure you want to remove them?';
+
+            dialog.confirm(header, message, 'Remove', function () {
+                transmission.removeTorrents(torrents);
+            });
+        }
+    },
+
+    promptToRemoveTorrentsAndData: function (torrents) {
+        if (torrents.length === 1) {
+            var torrent = torrents[0];
+            var header = 'Remove ' + torrent.getName() + ' and delete data?';
+            var message = 'All data downloaded for this torrent will be deleted. Are you sure you want to remove it?';
+
+            dialog.confirm(header, message, 'Remove', function () {
+                transmission.removeTorrentsAndData(torrents);
+            });
+        } else {
+            var header = 'Remove ' + torrents.length + ' transfers and delete data?';
+            var message = 'All data downloaded for these torrents will be deleted. Are you sure you want to remove them?';
+
+            dialog.confirm(header, message, 'Remove', function () {
+                transmission.removeTorrentsAndData(torrents);
+            });
+        }
+    },
+
+    removeTorrents: function (torrents) {
+        var ids = this.getTorrentIds(torrents);
+        this.remote.removeTorrents(ids, this.refreshTorrents, this);
+    },
+
+    removeTorrentsAndData: function (torrents) {
+        this.remote.removeTorrentsAndData(torrents);
+    },
+
+    promptToRenameTorrent: function (torrent) {
+        $('body').addClass('open_showing');
+        $('input#torrent_rename_name').attr('value', torrent.getName());
+        $('#rename_container').show();
+        $('#torrent_rename_name').focus();
+    },
+
+    renameSelectedTorrents: function () {
+        var torrents = this.getSelectedTorrents();
+        if (torrents.length != 1) {
+            dialog.alert("Renaming", "You can rename only one torrent at a time.", "Ok");
+        } else {
+            this.promptToRenameTorrent(torrents[0]);
+        };
+    },
+
+    onTorrentRenamed: function (response) {
+        var torrent;
+        if ((response.result === 'success') && (response.arguments) && ((torrent = this._torrents[response.arguments.id]))) {
+            torrent.refresh(response.arguments);
+        }
+    },
+
+    renameTorrent: function (torrent, newname) {
+        var oldpath = torrent.getName();
+        this.remote.renameTorrent([torrent.getId()], oldpath, newname, this.onTorrentRenamed, this);
+    },
+
+    verifySelectedTorrents: function () {
+        this.verifyTorrents(this.getSelectedTorrents());
+    },
+
+    reannounceSelectedTorrents: function () {
+        this.reannounceTorrents(this.getSelectedTorrents());
+    },
+
+    startAllTorrents: function (force) {
+        this.startTorrents(this.getAllTorrents(), force);
+    },
+    startSelectedTorrents: function (force) {
+        this.startTorrents(this.getSelectedTorrents(), force);
+    },
+    startTorrent: function (torrent) {
+        this.startTorrents([torrent], false);
+    },
+
+    startTorrents: function (torrents, force) {
+        this.remote.startTorrents(this.getTorrentIds(torrents), force, this.refreshTorrents, this);
+    },
+    verifyTorrent: function (torrent) {
+        this.verifyTorrents([torrent]);
+    },
+    verifyTorrents: function (torrents) {
+        this.remote.verifyTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this);
+    },
+
+    reannounceTorrent: function (torrent) {
+        this.reannounceTorrents([torrent]);
+    },
+    reannounceTorrents: function (torrents) {
+        this.remote.reannounceTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this);
+    },
+
+    stopAllTorrents: function () {
+        this.stopTorrents(this.getAllTorrents());
+    },
+    stopSelectedTorrents: function () {
+        this.stopTorrents(this.getSelectedTorrents());
+    },
+    stopTorrent: function (torrent) {
+        this.stopTorrents([torrent]);
+    },
+    stopTorrents: function (torrents) {
+        this.remote.stopTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this);
+    },
+    changeFileCommand: function (torrentId, rowIndices, command) {
+        this.remote.changeFileCommand(torrentId, rowIndices, command);
+    },
+
+    hideMobileAddressbar: function (delaySecs) {
+        if (isMobileDevice && !scroll_timeout) {
+            var callback = $.proxy(this.doToolbarHide, this);
+            var msec = delaySecs * 1000 || 150;
+            scroll_timeout = setTimeout(callback, msec);
+        };
+    },
+    doToolbarHide: function () {
+        window.scrollTo(0, 1);
+        scroll_timeout = null;
+    },
+
+    // Queue
+    moveTop: function () {
+        this.remote.moveTorrentsToTop(this.getSelectedTorrentIds(), this.refreshTorrents, this);
+    },
+    moveUp: function () {
+        this.remote.moveTorrentsUp(this.getSelectedTorrentIds(), this.refreshTorrents, this);
+    },
+    moveDown: function () {
+        this.remote.moveTorrentsDown(this.getSelectedTorrentIds(), this.refreshTorrents, this);
+    },
+    moveBottom: function () {
+        this.remote.moveTorrentsToBottom(this.getSelectedTorrentIds(), this.refreshTorrents, this);
+    },
+
+    /***
+     ****
+     ***/
+
+    updateGuiFromSession: function (o) {
+        var limit, limited, e, b, text;
+        var fmt = Transmission.fmt;
+        var menu = $('#footer_super_menu');
+
+        this.serverVersion = o.version;
+
+        this.prefsDialog.set(o);
+
+        if (RPC._TurtleState in o) {
+            b = o[RPC._TurtleState];
+            e = $('#turtle-button');
+            text = ['Click to ', (b ? 'disable' : 'enable'), ' Temporary Speed Limits (', fmt.speed(o[RPC._TurtleUpSpeedLimit]), ' up,', fmt.speed(o[RPC._TurtleDownSpeedLimit]), ' down)'].join('');
+            e.toggleClass('selected', b);
+            e.attr('title', text);
+        }
+
+        if (this.isMenuEnabled && (RPC._DownSpeedLimited in o) && (RPC._DownSpeedLimit in o)) {
+            limit = o[RPC._DownSpeedLimit];
+            limited = o[RPC._DownSpeedLimited];
+
+            e = menu.find('#limited_download_rate');
+            e.html('Limit (' + fmt.speed(limit) + ')');
+
+            if (!limited) {
+                e = menu.find('#unlimited_download_rate');
+            };
+            e.selectMenuItem();
+        }
+
+        if (this.isMenuEnabled && (RPC._UpSpeedLimited in o) && (RPC._UpSpeedLimit in o)) {
+            limit = o[RPC._UpSpeedLimit];
+            limited = o[RPC._UpSpeedLimited];
+
+            e = menu.find('#limited_upload_rate');
+            e.html('Limit (' + fmt.speed(limit) + ')');
+
+            if (!limited) {
+                e = menu.find('#unlimited_upload_rate');
+            };
+            e.selectMenuItem();
+        }
+    },
+
+    updateStatusbar: function () {
+        var i, row;
+        var u = 0;
+        var d = 0;
+        var fmt = Transmission.fmt;
+        var torrents = this.getAllTorrents();
+
+        // up/down speed
+        for (i = 0; row = torrents[i]; ++i) {
+            u += row.getUploadSpeed();
+            d += row.getDownloadSpeed();
+        }
+
+        $('#speed-up-container').toggleClass('active', u > 0);
+        $('#speed-up-label').text(fmt.speedBps(u));
+
+        $('#speed-dn-container').toggleClass('active', d > 0);
+        $('#speed-dn-label').text(fmt.speedBps(d));
+
+        // visible torrents
+        $('#filter-count').text(fmt.countString('Transfer', 'Transfers', this._rows.length));
+    },
+
+    setEnabled: function (key, flag) {
+        $(key).toggleClass('disabled', !flag);
+    },
+
+    updateFilterSelect: function () {
+        var i, names, name, str, o;
+        var e = $('#filter-tracker');
+        var trackers = this.getTrackers();
+
+        // build a sorted list of names
+        names = [];
+        for (name in trackers) {
+            names.push(name);
+        };
+        names.sort();
+
+        // build the new html
+        if (!this.filterTracker) {
+            str = '<option value="all" selected="selected">All</option>';
+        } else {
+            str = '<option value="all">All</option>';
+        };
+        for (i = 0; name = names[i]; ++i) {
+            o = trackers[name];
+            str += '<option value="' + o.domain + '"';
+            if (trackers[name].domain === this.filterTracker) {
+                str += ' selected="selected"';
+            };
+            str += '>' + name + '</option>';
+        }
+
+        if (!this.filterTrackersStr || (this.filterTrackersStr !== str)) {
+            this.filterTrackersStr = str;
+            $('#filter-tracker').html(str);
+        }
+    },
+
+    updateButtonsSoon: function () {
+        if (!this.buttonRefreshTimer) {
+            var callback = $.proxy(this.updateButtonStates, this);
+            var msec = 100;
+
+            this.buttonRefreshTimer = setTimeout(callback, msec);
+        }
+    },
+
+    calculateTorrentStates: function (callback) {
+        var stats = {
+            total: 0,
+            active: 0,
+            paused: 0,
+            sel: 0,
+            activeSel: 0,
+            pausedSel: 0,
+            queuedSel: 0
+        };
+
+        clearTimeout(this.buttonRefreshTimer);
+        delete this.buttonRefreshTimer;
+
+        for (var i = 0, row; row = this._rows[i]; ++i) {
+            var isStopped = row.getTorrent().isStopped();
+            var isSelected = row.isSelected();
+            var isQueued = row.getTorrent().isQueued();
+            ++stats.total;
+            if (!isStopped) {
+                ++stats.active;
+            };
+            if (isStopped) {
+                ++stats.paused;
+            };
+            if (isSelected) {
+                ++stats.sel;
+            };
+            if (isSelected && !isStopped) {
+                ++stats.activeSel;
+            };
+            if (isSelected && isStopped) {
+                ++stats.pausedSel;
+            };
+            if (isSelected && isQueued) {
+                ++stats.queuedSel;
+            };
+        };
+
+        callback(stats);
+    },
+
+    updateButtonStates: function () {
+        var tr = this;
+        var e = this.elements;
+
+        this.calculateTorrentStates(function (s) {
+            tr.setEnabled(e.toolbar_pause_button, s.activeSel > 0);
+            tr.setEnabled(e.toolbar_start_button, s.pausedSel > 0);
+            tr.setEnabled(e.toolbar_remove_button, s.sel > 0);
+        });
+    },
+
+    /****
+     *****
+     *****  INSPECTOR
+     *****
+     ****/
+
+    inspectorIsVisible: function () {
+        return $('#torrent_inspector').is(':visible');
+    },
+    toggleInspector: function () {
+        this.setInspectorVisible(!this.inspectorIsVisible());
+    },
+    setInspectorVisible: function (visible) {
+        if (visible) {
+            this.inspector.setTorrents(this.getSelectedTorrents());
+        };
+
+        // update the ui widgetry
+        $('#torrent_inspector').toggle(visible);
+        $('#toolbar-inspector').toggleClass('selected', visible);
+        this.hideMobileAddressbar();
+        if (isMobileDevice) {
+            $('body').toggleClass('inspector_showing', visible);
+        } else {
+            var w = visible ? $('#torrent_inspector').outerWidth() + 1 + 'px' : '0px';
+            $('#torrent_container')[0].style.right = w;
+        }
+    },
+
+    /****
+     *****
+     *****  FILTER
+     *****
+     ****/
+
+    refilterSoon: function () {
+        if (!this.refilterTimer) {
+            var tr = this,
+                callback = function () {
+                    tr.refilter(false);
+                },
+                msec = 100;
+            this.refilterTimer = setTimeout(callback, msec);
+        }
+    },
+
+    sortRows: function (rows) {
+        var i, tor, row,
+            id2row = {},
+            torrents = [];
+
+        for (i = 0; row = rows[i]; ++i) {
+            tor = row.getTorrent();
+            torrents.push(tor);
+            id2row[tor.getId()] = row;
+        }
+
+        Torrent.sortTorrents(torrents, this[Prefs._SortMethod],
+            this[Prefs._SortDirection]);
+
+        for (i = 0; tor = torrents[i]; ++i) {
+            rows[i] = id2row[tor.getId()];
+        };
+    },
+
+    refilter: function (rebuildEverything) {
+        var i, e, id, t, row, tmp, rows, clean_rows, dirty_rows, frag;
+        var sort_mode = this[Prefs._SortMethod];
+        var sort_direction = this[Prefs._SortDirection];
+        var filter_mode = this[Prefs._FilterMode];
+        var filter_text = this.filterText;
+        var filter_tracker = this.filterTracker;
+        var renderer = this.torrentRenderer;
+        var list = this.elements.torrent_list;
+
+        old_sel_count = $(list).children('.selected').length;
+
+        this.updateFilterSelect();
+
+        clearTimeout(this.refilterTimer);
+        delete this.refilterTimer;
+
+        if (rebuildEverything) {
+            $(list).empty();
+            this._rows = [];
+            for (id in this._torrents) {
+                this.dirtyTorrents[id] = true;
+            };
+        }
+
+        // rows that overlap with dirtyTorrents need to be refiltered.
+        // those that don't are 'clean' and don't need refiltering.
+        clean_rows = [];
+        dirty_rows = [];
+        for (i = 0; row = this._rows[i]; ++i) {
+            if (row.getTorrentId() in this.dirtyTorrents) {
+                dirty_rows.push(row);
+            } else {
+                clean_rows.push(row);
+            };
+        }
+
+        // remove the dirty rows from the dom
+        e = [];
+        for (i = 0; row = dirty_rows[i]; ++i) {
+            e.push(row.getElement());
+        };
+        $(e).detach();
+
+        // drop any dirty rows that don't pass the filter test
+        tmp = [];
+        for (i = 0; row = dirty_rows[i]; ++i) {
+            id = row.getTorrentId();
+            t = this._torrents[id];
+            if (t && t.test(filter_mode, filter_text, filter_tracker)) {
+                tmp.push(row);
+            };
+            delete this.dirtyTorrents[id];
+        }
+        dirty_rows = tmp;
+
+        // make new rows for dirty torrents that pass the filter test
+        // but don't already have a row
+        for (id in this.dirtyTorrents) {
+            t = this._torrents[id];
+            if (t && t.test(filter_mode, filter_text, filter_tracker)) {
+                row = new TorrentRow(renderer, this, t);
+                e = row.getElement();
+                e.row = row;
+                dirty_rows.push(row);
+                $(e).click($.proxy(this.onRowClicked, this));
+                $(e).dblclick($.proxy(this.toggleInspector, this));
+            }
+        }
+
+        // sort the dirty rows
+        this.sortRows(dirty_rows);
+
+        // now we have two sorted arrays of rows
+        // and can do a simple two-way sorted merge.
+        rows = [];
+        var ci = 0,
+            cmax = clean_rows.length;
+        var di = 0,
+            dmax = dirty_rows.length;
+        frag = document.createDocumentFragment();
+        while (ci != cmax || di != dmax) {
+            var push_clean;
+
+            if (ci == cmax) {
+                push_clean = false;
+            } else if (di == dmax) {
+                push_clean = true;
+            } else {
+                var c = Torrent.compareTorrents(clean_rows[ci].getTorrent(), dirty_rows[di].getTorrent(), sort_mode, sort_direction);
+                push_clean = (c < 0);
+            }
+
+            if (push_clean) {
+                rows.push(clean_rows[ci++]);
+            } else {
+                row = dirty_rows[di++];
+                e = row.getElement();
+
+                if (ci !== cmax) {
+                    list.insertBefore(e, clean_rows[ci].getElement());
+                } else {
+                    frag.appendChild(e);
+                };
+
+                rows.push(row);
+            }
+        }
+        list.appendChild(frag);
+
+        // update our implementation fields
+        this._rows = rows;
+        this.dirtyTorrents = {};
+
+        // jquery's even/odd starts with 1 not 0, so invert its logic
+        e = []
+        for (i = 0; row = rows[i]; ++i) {
+            e.push(row.getElement());
+        };
+        $(e).filter(":odd").addClass('even');
+        $(e).filter(":even").removeClass('even');
+
+        // sync gui
+        this.updateStatusbar();
+        if (old_sel_count !== $(list).children('.selected').length) {
+            this.selectionChanged();
+        };
+    },
+
+    setFilterMode: function (mode) {
+        // set the state
+        this.setPref(Prefs._FilterMode, mode);
+
+        // refilter
+        this.refilter(true);
+    },
+
+    onFilterModeClicked: function (ev) {
+        this.setFilterMode($('#filter-mode').val());
+    },
+
+    onFilterTrackerClicked: function (ev) {
+        var tracker = $('#filter-tracker').val();
+        this.setFilterTracker(tracker === 'all' ? null : tracker);
+    },
+
+    setFilterTracker: function (domain) {
+        // update which tracker is selected in the popup
+        var key = domain ? this.getReadableDomain(domain) : 'all';
+        var id = '#show-tracker-' + key;
+
+        $(id).addClass('selected').siblings().removeClass('selected');
+
+        this.filterTracker = domain;
+        this.refilter(true);
+    },
+
+    // example: "tracker.ubuntu.com" returns "ubuntu.com"
+    getDomainName: function (host) {
+        var dot = host.indexOf('.');
+        if (dot !== host.lastIndexOf('.')) {
+            host = host.slice(dot + 1);
+        };
+
+        return host;
+    },
+
+    // example: "ubuntu.com" returns "Ubuntu"
+    getReadableDomain: function (name) {
+        if (name.length) {
+            name = name.charAt(0).toUpperCase() + name.slice(1);
+        };
+        var dot = name.indexOf('.');
+        if (dot !== -1) {
+            name = name.slice(0, dot);
+        };
+        return name;
+    },
+
+    getTrackers: function () {
+        var ret = {};
+
+        var torrents = this.getAllTorrents();
+        for (var i = 0, torrent; torrent = torrents[i]; ++i) {
+            var names = [];
+            var trackers = torrent.getTrackers();
+
+            for (var j = 0, tracker; tracker = trackers[j]; ++j) {
+                var uri, announce = tracker.announce;
+
+                if (announce in this.uriCache) {
+                    uri = this.uriCache[announce];
+                } else {
+                    uri = this.uriCache[announce] = parseUri(announce);
+                    uri.domain = this.getDomainName(uri.host);
+                    uri.name = this.getReadableDomain(uri.domain);
+                };
+
+                if (!(uri.name in ret)) {
+                    ret[uri.name] = {
+                        'uri': uri,
+                        'domain': uri.domain,
+                        'count': 0
+                    };
+                };
+
+                if (names.indexOf(uri.name) === -1) {
+                    names.push(uri.name);
+                };
+            }
+
+            for (var j = 0, name; name = names[j]; ++j) {
+                ret[name].count++;
+            };
+        }
+
+        return ret;
+    },
+
+    /***
+     ****
+     ****  Compact Mode
+     ****
+     ***/
+
+    toggleCompactClicked: function () {
+        this.setCompactMode(!this[Prefs._CompactDisplayState]);
+    },
+    setCompactMode: function (is_compact) {
+        var key = Prefs._CompactDisplayState;
+        var was_compact = this[key];
+
+        if (was_compact !== is_compact) {
+            this.setPref(key, is_compact);
+            this.onCompactModeChanged();
+        };
+    },
+    initCompactMode: function () {
+        this.onCompactModeChanged();
+    },
+    onCompactModeChanged: function () {
+        var compact = this[Prefs._CompactDisplayState];
+
+        // update the ui: footer button
+        $("#compact-button").toggleClass('selected', compact);
+
+        // update the ui: torrent list
+        this.torrentRenderer = compact ? new TorrentRendererCompact() : new TorrentRendererFull();
+        this.refilter(true);
+    },
+
+    /***
+     ****
+     ****  Statistics
+     ****
+     ***/
+
+    // turn the periodic ajax stats refresh on & off
+    togglePeriodicStatsRefresh: function (enabled) {
+        clearInterval(this.statsInterval);
+        delete this.statsInterval;
+
+        if (enabled) {
+            var callback = $.proxy(this.loadDaemonStats, this);
+            var msec = 5000;
+
+            this.statsInterval = setInterval(callback, msec);
+        };
+    },
+
+    loadDaemonStats: function (async) {
+        this.remote.loadDaemonStats(function (data) {
+            this.updateStats(data['arguments']);
+        }, this, async);
+    },
+
+    // Process new session stats from the server
+    updateStats: function (stats) {
+        var s, ratio;
+        var fmt = Transmission.fmt;
+
+        s = stats["current-stats"];
+        ratio = Math.ratio(s.uploadedBytes, s.downloadedBytes);
+        $('#stats-session-uploaded').html(fmt.size(s.uploadedBytes));
+        $('#stats-session-downloaded').html(fmt.size(s.downloadedBytes));
+        $('#stats-session-ratio').html(fmt.ratioString(ratio));
+        $('#stats-session-duration').html(fmt.timeInterval(s.secondsActive));
+
+        s = stats["cumulative-stats"];
+        ratio = Math.ratio(s.uploadedBytes, s.downloadedBytes);
+        $('#stats-total-count').html(s.sessionCount + " times");
+        $('#stats-total-uploaded').html(fmt.size(s.uploadedBytes));
+        $('#stats-total-downloaded').html(fmt.size(s.downloadedBytes));
+        $('#stats-total-ratio').html(fmt.ratioString(ratio));
+        $('#stats-total-duration').html(fmt.timeInterval(s.secondsActive));
+    },
+
+    showStatsDialog: function () {
+        this.loadDaemonStats();
+        this.hideMobileAddressbar();
+        this.togglePeriodicStatsRefresh(true);
+        $('#stats-dialog').dialog({
+            close: $.proxy(this.onStatsDialogClosed, this),
+            show: 'fade',
+            hide: 'fade',
+            title: 'Statistics'
+        });
+    },
+
+    onStatsDialogClosed: function () {
+        this.hideMobileAddressbar();
+        this.togglePeriodicStatsRefresh(false);
+    }
 };